insigz docs
Engineering

Entity Resolution

How raw observations become attached to the right Entity — the anti-fork problem — via a two-stage deterministic then probabilistic matcher.

How raw observations become attached to the right Entity (the "anti-fork" problem): never create a duplicate entity for something we already track, never wrongly merge two real entities. Product doc 02 and 06 reference this; here is the runtime.

The two-stage matcher

observation ──► STAGE 1: deterministic match (strong keys)
                    │ mmsi / imo / lei / isin / exact gov id
                    ├── hit ──► attach to existing entity (confidence 1.0)
                    └── miss ─► STAGE 2: probabilistic match (similarity)
                                    score = weighted blend of:
                                      name similarity (Jaro-Winkler / trigram)
                                      geospatial proximity (within plausible track)
                                      temporal plausibility
                                      shared attributes (flag, type, owner)
                                    ├── score ≥ HIGH (auto)   ──► attach
                                    ├── LOW ≤ score < HIGH     ──► MERGE-REVIEW queue (human)
                                    └── score < LOW            ──► create new entity
  • Deterministic keys (mmsi/imo/lei/isin/gov id) live in entity_aliases with a unique constraint, so a strong-key match is a single indexed lookup and can never fork. name_norm and other probabilistic signals are NOT stored as unique aliases — a normalized name is not unique (two real entities can share one), so it is only a similarity feature in Stage 2, never a unique deterministic key. (This reconciles the entity_aliases PK in 20-canonical-model.md: only deterministic key types go there.)
  • Probabilistic scoring is a transparent weighted sum (not a black box) so every match is explainable — the analyst can see why two records were joined.
  • The HIGH/LOW thresholds are tunable per entity type and per tenant.

Resolution timing (decided)

Stage 1 (deterministic) runs inline in the ingest transaction — it's a single indexed lookup, cheap, and lets the observation attach immediately. Stage 2 (probabilistic, candidate generation + scoring) runs asynchronously via the outbox → a resolution worker, because similarity search over many candidates is too heavy for the write path. This reconciles 10-system-architecture.md (which shows resolution after the observation insert): deterministic is synchronous, probabilistic is deferred.

The MERGE-REVIEW queue

Ambiguous matches (LOW ≤ score < HIGH) never auto-merge — they stage for a human, exactly the Q9/Q10 human-commit doctrine applied to data. A reviewer sees the two candidates side by side, the score breakdown, and the cited observations, then Accepts / Rejects / Splits. This is the same review pattern as the Proposal Inbox (product §13). Decisions are logged and feed back as labels to tune thresholds.

Merges and splits

  • Merge: pick a surviving entity_id; repoint observations/events/aliases; keep a merge_log row recording the absorbed id, the actor, and the timestamp (reversible).
  • Split: if a merge was wrong, the merge_log lets you reconstruct the original partition and re-resolve from the observations.
  • Because observations are immutable and carry their own ids, resolution is always replayable — you can re-run the matcher over history with new thresholds.

Implementation notes

  • Run resolution synchronously in the ingest transaction for deterministic matches (cheap), and asynchronously (outbox → worker) for probabilistic scoring that needs similarity search over many candidates.
  • Candidate generation for Stage 2: trigram index (pg_trgm) on canonical_name + a geospatial pre-filter (PostGIS) to bound the candidate set before scoring.
  • Keep the matcher in packages/canonical so the same code resolves at ingest and during a backfill/replay.

Gaps to close

  • Set per-type weights + HIGH/LOW thresholds and a labeling loop to tune them.
  • Decide auto-merge audit retention and whether merges require a second approver for designated entities.
  • Define the candidate-generation budget (max candidates scored) for very large entity sets.