Member Junction
    Preparing search index...

    Modernized duplicate record detection engine.

    Supports:

    • List-based batch detection (getDuplicateRecords)
    • View/filter/full-entity batch detection (vector-first approach)
    • Single-record duplicate check (CheckSingleRecord)
    • Entry-time check of a new record's unsaved values, flag only (CheckRecordValues)
    • Hybrid search via RRF when vector DB supports it
    • Optional post-retrieval reranking via MJ's BaseReranker
    • Configurable topK, thresholds, and progress reporting

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    Accessors

    Methods

    Constructors

    • Parameters

      • Optionalprovider: IMetadataProvider

        Optional metadata provider to bind this instance (and all of its metadata/view operations) to. Server-side callers servicing per-request connections MUST pass their request-scoped provider (e.g. this.ProviderToUse from a BaseEntity subclass) — relying on the global default silently uses the wrong connection in multi-provider scenarios. Falls back to the global default provider when omitted.

      Returns DuplicateRecordDetector

    Properties

    _currentUser: UserInfo
    _metadata: Metadata
    _runView: RunView

    Accessors

    • get CurrentUser(): UserInfo

      Returns UserInfo

    • set CurrentUser(user: UserInfo): void

      Parameters

      Returns void

    • get Metadata(): IMetadataProvider

      Provider-aware metadata access: returns the explicit provider when one was supplied (constructor or Provider setter), otherwise the global default. All metadata operations in this class hierarchy go through this getter so a bound instance never leaks onto the global provider.

      Returns IMetadataProvider

    • get Provider(): IMetadataProvider

      Optional metadata provider override. Pass the provider to the constructor (preferred) or set instance.Provider = providerToUse before invoking helper methods in multi-provider contexts. Setting it rebinds the internal RunView instance so view execution rides the same provider. Falls back to the global default when unset.

      The fallback is the global provider itself, never the Metadata wrapper: consumers type-check what they get here (e.g. VectorDBBase.TryWireColocatedHost needs an IColocatedVectorHost), and the wrapper fails those checks.

      Returns IMetadataProvider

    • set Provider(value: IMetadataProvider): void

      Parameters

      Returns void

    • get RunView(): RunView

      Returns RunView

    Methods

    • Write the LLM verdict + run id onto a single match row's generated columns. The recommendation / confidence / reasoning are taken from THIS candidate's own verdict (judged independently), so a false-positive candidate reads NotDuplicate even when another candidate in the same set is a confident Merge. Falls back to the set-level summary only when the reasoner returned no per-candidate verdict for this record. The proposed survivor + field map stay set-level (they describe the merge of the set's true duplicates). The run id is the set's, unless this candidate's verdict came from a run of its own.

      Parameters

      Returns void

    • Builds a SQL filter from composite keys. Values are sanitized to prevent SQL injection.

      Parameters

      Returns string

    • How many neighbors to query for so that TopK survive the record filter: TopK times RECORD_FILTER_TOPK_FACTOR when a filter is set, TopK otherwise.

      Parameters

      • topK: number
      • recordFilter: string

      Returns number

    • Returns true when PageRecordsByEntityID can serve the given entity via keyset (seek) pagination — i.e. the entity has a single-column PK on a comparable type. Callers iterating large tables should consult this and pass AfterKey when true.

      Parameters

      • entityID: string | number

      Returns boolean

    • Check values a person is entering for a new record against the entity's existing records, and flag the plausible duplicates. It only flags: nothing is saved, merged or blocked.

      1. The switch. The check runs only for an entity with an Active entity document that has EnableLLMReasoning on and whose ReasoningMode is 'Decision' or 'DecisionThenPrompt' (see FindEntryCheckDocument). Without one the result is NotConfigured and nothing else runs.
      2. Candidates. The values, each text cut to DUPLICATE_ENTRY_CHECK_MAX_FIELD_TEXT_LENGTH, become an unsaved entity object, rendered by the document's template, embedded, and queried with the same TopK and threshold as CheckSingleRecord.
      3. Permission. The vector index does not know who is asking, so the candidates are narrowed to those the context user can read, with one RunView as that user.
      4. Decision. One DecisionReasoningProvider.DecideCandidates call over the readable candidates, in either mode: the prompt half of 'DecisionThenPrompt' is too slow for entry, and it can recommend a merge. Its state is bounded by DUPLICATE_ENTRY_CHECK_MAX_DECISION_FIELDS and the field-text limit. A candidate is flagged when the provider bands it Uncertain, by its own threshold, on the probability calibrated for the model that answered. A candidate the decision gave no answer for is flagged. A failed decision flags nothing, and neither does a decision from a model with no calibration: its probabilities can't be banded, and flagging every candidate would be the vector threshold alone. Both give a Failed result with the reason.

      Budget. The whole check is bounded by options.TimeoutMS (DUPLICATE_ENTRY_CHECK_SERVER_BUDGET_MS by default) and by options.CancellationToken. No step starts once either has fired, the check stops waiting for a running step, and the decision call receives both, so the model call is aborted. A stopped check is Failed.

      Never throws: any failure is a Failed result with the reason.

      Parameters

      • entityName: string

        the entity the new record belongs to

      • values: Record<string, unknown>

        the entered values by field name; fields the entity does not have are ignored

      • contextUser: UserInfo

        the person entering the record. Required: the candidates are narrowed to the records this user can read, and without a user the check fails rather than skip that step.

      • Optionaloptions: DuplicateEntryCheckOptions

        overrides for TopK and the potential-match threshold, the budget and cancellation

      Returns Promise<DuplicateEntryCheckResult>

      the flagged candidates, most probable first, or why there are none

    • Remove matches whose target record no longer exists in the source entity.

      Vector indexes (e.g. Pinecone) retain embeddings keyed by each record's composite key via a deterministic vector ID. When records are deleted — or re-seeded with new primary keys — their old vectors linger as "ghosts". A nearest-neighbour query then happily returns those ghosts, often the record's OWN former vector (identical name, ~0.98 score), which to a user looks exactly like a record matching itself. The per-record self-match filter can't catch these because the ghost carries the OLD id.

      We verify every distinct match key for the batch against the live table in a single batched query and drop any that don't resolve. Keys are compared in compact URL-segment form, so this works for a single-column key of any name and for composite keys.

      With the entity document's record filter, a match whose record fails the filter is dropped the same way: its vector may predate the filter, or the record may have changed since sync.

      Mutates queryResults in place.

      Parameters

      • queryResults: RecordQueryResult[]
      • entityInfo: EntityInfo
      • OptionalrecordFilter: string

        the entity document's record filter (GetRecordFilter), if any

      Returns Promise<void>

    • The entity document that turns the entry-time check on for an entity: an Active document with EnableLLMReasoning on whose ReasoningMode is 'Decision' or 'DecisionThenPrompt'. The check makes a decision-model call on every pause in typing, so switching reasoning off on the document turns the check off too, as it turns off reasoning in batch runs (IsReasoningGateOpen). When several qualify, the oldest wins (earliest __mj_CreatedAt, then lowest ID), so adding a document never silently changes which one an entity uses.

      Parameters

      • entityName: string

      Returns Promise<MJEntityDocumentEntity>

      the document, or null when the check is not configured for the entity

    • Resolve the embedding model to generate vectors with.

      Pass an id wherever one exists. The width of an embedding is a property of the model, and the width an index accepts is fixed when the index is created, so which model this returns decides whether the vectors fit. The no-id branch cannot know that and is a last resort only.

      The no-id branch used to be a bare find() over AIEngine.Instance.Models, which BaseAIEngine.Config loads with no OrderBy and no IsActive filter. So it returned whichever Embeddings row the view happened to hand back first — a different model on a different database, or after an unrelated insert, and quite possibly a deactivated one. An arbitrary answer is worse than a wrong one: the same code produces different widths in different environments and nothing in the failure names the model that was chosen.

      It is now deterministic — active models first, then highest PowerRank, then name — and it says out loud which model it picked and that nobody asked for it.

      Parameters

      • Optionalid: string

      Returns MJAIModelEntityExtended

    • The entity document's record filter (Configuration.recordFilter.extraFilter): the records that take part in duplicate detection. A batch run checks only records that pass it, and every run offers only candidates that pass it. CheckSingleRecord and the entry-time check still check the record they are given, since the caller asked about that record. Vector sync applies the same filter to the records it vectorizes.

      Parameters

      Returns string

      the predicate, or null when none is set

    • Decide whether a single candidate is eligible for automatic merge.

      Back-compat path (EnableLLMReasoning = false): eligible iff the vector score meets the absolute threshold — byte-for-byte the original behavior. AutomationLevel is ignored.

      Reasoning path (EnableLLMReasoning = true): AutomationLevel governs.

      • ReviewAll / LLMGated → never auto-merge (everything goes to human review).
      • AutoMergeAboveAbsolute → at/above the absolute threshold AND the set's LLM recommendation is 'Merge' AND this candidate's own verdict is 'Merge'. A set-level 'Merge' only says that SOME candidate is a duplicate, so it never merges a candidate the reasoner judged otherwise, or returned no verdict for.

      Parameters

      Returns boolean

    • Narrow a list's or view's record ids to those that pass the entity document's record filter, so a record the filter excludes is never checked whatever the run's source.

      Parameters

      • ids: string[]
      • entityInfo: EntityInfo
      • OptionalrecordFilter: string

      Returns Promise<string[]>

      the ids unchanged when there is no filter, otherwise those that pass it, in order

    • Populate a candidate's composite key from a vector match's RecordID, tolerant of the two formats different vector providers emit:

      • Concatenated ("ID|", or "F1|v1||F2|v2" for composite PKs) — e.g. providers that stored the full key. Parsed via CompositeKey.LoadFromConcatenatedString.
      • Bare primary-key value ("") — what the in-process SimpleVectorServiceProvider returns, since it stores MJ: Entity Record Documents.RecordID as the raw PK value. For this form we rebuild the key using the source record's PK field name(s).

      Getting this wrong leaves KeyValuePairs empty (the bare value has no field delimiter, so LoadFromConcatenatedString no-ops), which silently breaks self-match filtering, candidate field-delta loading (the reasoner sees an empty candidate), and MatchRecordID persistence.

      Parameters

      Returns void

    • Return the set of record keys (compact URL segments, normalized for case-insensitive UUID comparison) that actually exist in the entity, for the given candidate ids — themselves compact segments. A single-column key uses one IN (...) per chunk; a composite key needs one (F1=.. AND F2=..) term per record. Batched to stay within reasonable query sizes.

      Parameters

      • entityInfo: EntityInfo
      • pkField: string

        the single key column — read only when the entity has exactly one primary key; a composite key's columns come from entityInfo.PrimaryKeys.

      • ids: string[]
      • OptionalrecordFilter: string

        the entity document's record filter, if any: only records that also pass it count as existing

      Returns Promise<Set<string>>

      when a record filter is set and can't be applied — failing open would let the records it excludes through

    • The display names of the candidates the context user can read, from one RunView over the candidate keys run as that user, so entity permissions and row-level security apply. A candidate missing from the map must not be shown. When the RunView fails the map is empty: a candidate is shown only when the user is known to be able to read it.

      Parameters

      Returns Promise<Map<string, string>>

      display name by normalized compact record id (NormalizeUUID)

    • Load record IDs directly from the entity, optionally filtered. Selects only the primary key column(s) with ResultType: 'simple' for efficiency.

      Parameters

      Returns Promise<string[]>

    • Resolve the ids to check. Every id is a compact CompositeKey URL segment — the bare value for a single-column key (whatever the column is called), F1|v1||F2|v2 for a composite key — which is the form MJ: List Details.RecordID already holds; CompositeKey.FromURLSegment rebuilds the real key from it for any entity.

      Every loader reads with IgnoreMaxRows. Without it RunView falls back to the entity's UserViewMaxRows (1,000 by default) and returns only the first page of ids. The run then sets TotalItemCount from that page, so it checks a fraction of the records and still reports itself complete: "1000 of 1000" on a 61,671-record entity.

      Parameters

      Returns Promise<string[]>

    • Each record's row as vector sync reads it — RunView with ResultType: 'simple' and every column — in the order of records, so the template renders the values sync rendered. BaseEntity.GetAll() differs: it turns date fields into Date objects, for one.

      An unsaved record (the entry-time check), or one whose row is no longer there, renders from its own values.

      Parameters

      Returns Promise<Record<string, unknown>[]>

    • Query the vector DB for duplicates of each record, with concurrency control.

      Creates one async task per record, then executes them via RunWithConcurrency with the specified concurrency limit. Each task embeds and queries a single record against the vector index.

      Supports hybrid search (vector + keyword) with RRF fusion when the vector DB provider supports it (checked via SupportsHybridSearch).

      Post-query, results are:

      1. Parsed from raw vector matches into typed PotentialDuplicate objects
      2. Filtered to remove self-matches (same record as source)
      3. Filtered by the potential match threshold (from options or entity document)

      Parameters

      • records: BaseEntity<unknown>[]

        The source records to find duplicates for

      • vectors: number[][]

        Pre-computed embedding vectors, one per record (same index order)

      • templateTexts: string[]

        Rendered template texts for hybrid keyword search

      • entityDocument: MJEntityDocumentEntity

        The entity document providing thresholds and configuration

      • topK: number

        Number of nearest neighbors to retrieve per record

      • options: DuplicateDetectionOptions

        Detection options including threshold overrides

      • concurrency: number

        Max parallel vector queries

      Returns Promise<RecordQueryResult[]>

      One RecordQueryResult per input record

    • Extract a display label from a candidate's vector metadata snapshot, if present.

      Parameters

      • Optionalmetadata: Record<string, string>

      Returns string

    • Resolve a record's display label: prefer the name loaded from the record (via the delta builder), then a secondary fallback (e.g. a candidate's vector-metadata name), then the raw record-id key as a last resort. Guards against the loaded label itself being the record key (the engine's own fallback) so we still try the secondary source.

      Parameters

      • labels: Map<string, string>
      • recordID: string
      • fallback: string

      Returns string

    • Run LLM reasoning for one source record's matched set, ONCE, only when:

      1. the entity document has EnableLLMReasoning = true, AND
      2. the set is non-empty, AND
      3. the set's top MatchProbability >= the (non-null) ReasoningThreshold.

      Returns undefined in every other case — including when reasoning is disabled — so the surrounding persist/merge path stays byte-for-byte identical to the vector-only behavior. A failed reasoning call returns an output with Success = false (never throws), so one bad set never aborts the run.

      Parameters

      Returns Promise<SetReasoning>

    • Type Parameters

      Parameters

      • entityName: string
      • extraFilter: string

      Returns Promise<T>

    • Saving an Entity in any vector related package needs the CurrentUser property to be set on the entity So this is a simple wrapper to set it before saving

      Parameters

      Returns Promise<boolean>

    • Keep at most TopK candidates per result, in the order the query ranked them. Mutates in place.

      Parameters

      • queryResults: RecordQueryResult[]
      • topK: number

      Returns void