Identity Graph
How Kanoniv builds and maintains the many-to-one identity mapping — and how you control it with merge and split.
The Graph Model
Kanoniv maintains an identity graph where:
- Nodes are external entities (records from source systems)
- Edges connect external entities to canonical entities
- Each canonical entity is a hub linking multiple external records
┌─────────────────────┐
│ Canonical Entity │
│ 550e8400-... │
│ │
│ name: John Doe │
│ email: john@acme │
│ plan: enterprise │
└──────────┬──────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ crm:contact_1 │ │ billing:cus_42 │ │ mktg:lead_789 │
│ │ │ │ │ │
│ John Doe │ │ Jonathan Doe │ │ J. Doe │
│ [email protected] │ │ [email protected] │ │ [email protected] │
│ 555-0101 │ │ plan:enterprise│ │ source:webinar │
└────────────────┘ └────────────────┘ └────────────────┘Three different systems, three different records, three different names — but the same person. The identity graph connects them to a single canonical entity with survivorship rules determining which values win.
How the Graph Gets Built
The graph is built automatically during reconciliation based on the matching rules in your spec.
Step 1: Reconcile
from kanoniv import Spec, Source, reconcile
spec = Spec.from_file("customer-spec.yaml")
sources = [
Source.from_csv("crm", "crm.csv"),
Source.from_csv("billing", "billing.csv"),
]
result = reconcile(sources, spec)Step 2: Inspect Clusters
The result contains clusters — groups of record IDs that resolved to the same canonical entity:
for i, cluster in enumerate(result.clusters):
print(f"Cluster {i + 1}: {cluster}")Cluster 1: ['crm:1', 'billing:cus_001']
Cluster 2: ['crm:2', 'billing:cus_002']
Cluster 3: ['crm:3', 'billing:cus_003']
Cluster 4: ['crm:4']
Cluster 5: ['billing:cus_006']Clusters 1–3 are merged pairs (the engine matched them above the threshold). Clusters 4 and 5 are singletons — records with no match in the other source.
Step 3: Inspect Golden Records
Each cluster produces a golden record with survivorship applied:
for record in result.golden_records:
print(record){
'name': 'John Doe', # from crm (source_priority winner)
'email': '[email protected]', # from both (identical)
'phone': '555-0101', # from crm (only source with phone)
'card_last4': '4242', # from billing (only source with card)
'plan': 'enterprise', # from billing (only source with plan)
}Step 4: Understand Why
Every cluster comes with a decision explaining which rules fired:
for decision in result.decisions:
print(decision){
'left': 'crm:1',
'right': 'billing:cus_001',
'score': 1.0,
'outcome': 'match',
'rules': [{'name': 'email_exact', 'score': 1.0, 'field': 'email'}]
}Score 1.0, matched on email_exact — that's why crm:1 and billing:cus_001 ended up in the same cluster.
Merge: Combining Identities
Merge is the most common graph mutation. It happens in two ways:
Automatic Merge (Engine)
During reconciliation, the engine scores every candidate pair against your spec's matching rules. Pairs scoring above the match threshold are merged automatically:
# In your spec
decision:
thresholds:
match: 0.9 # auto-merge above this
review: 0.7 # flag for review between 0.7–0.9When two records merge, the engine:
- Links both external entities to the same canonical ID
- Applies survivorship rules to build the golden record
- Logs the decision with confidence score and matched rules
Manual Merge (Force Merge)
Sometimes the engine doesn't catch a match - different email domains, typos in every field, or a confidence score that falls just below the threshold. Force merge lets a human override the engine's decision.
What happens after a force merge:
BEFORE AFTER
crm:contact_123 -> can_001 crm:contact_123 -> can_001
name: John Doe billing:cus_456 -> can_001 (now linked)
email: [email protected]
Golden Record:
billing:cus_456 -> can_002 name: John Doe <- crm (priority)
name: Jonathan Doe email: [email protected] <- crm (priority)
email: [email protected] plan: enterprise <- billing (only source)
plan: enterprise card_last4: 4242 <- billing (only source)The old can_002 is absorbed into can_001. Survivorship re-runs to produce an updated golden record.
Split: Correcting False Positives
Split is the inverse of merge — it separates records that were incorrectly linked. False positives happen when two different people share a field value (same name, shared company email alias, recycled phone number).
Example: Two John Smiths
Your CRM has two contacts named "John Smith" at Acme Corp. One is the CFO, the other is a sales rep. The engine merged them because they share name + company:
INCORRECTLY MERGED
crm:contact_100 -> can_500 <- John Smith, CFO, [email protected]
crm:contact_200 -> can_500 <- John Smith, Sales Rep, [email protected]
billing:cus_777 -> can_500 <- John Smith, enterprise planThe golden record now has data from three external records, but two of them are different people. A split override separates them:
BEFORE (incorrect) AFTER (corrected)
crm:contact_100 -> can_500 crm:contact_100 -> can_500 (CFO)
crm:contact_200 -> can_500 billing:cus_777 -> can_500 (CFO's billing)
billing:cus_777 -> can_500
crm:contact_200 -> can_901 (new canonical for sales rep)A new canonical (can_901) is created for the split-out record. The original canonical's golden record is recomputed without the removed record's data.
Preventing Re-Merge with Locks
After splitting, the engine might re-merge the same records on the next reconciliation run (they still share name + company). A lock override prevents this permanently. The engine will skip this pair even if it scores above the match threshold.
Graph Integrity
The identity graph enforces strict structural rules:
- No orphans — Every external entity links to exactly one canonical
- No cycles — The graph is a forest of trees rooted at canonicals
- Idempotent — Running the same reconciliation twice produces the same graph
- Audit trail — Every mutation (merge, split, lock) is logged with timestamp, actor, and reason
Audit Trail
Every graph mutation is permanently recorded - merges, splits, locks, and field overrides. Each event includes a timestamp, actor, and reason, so you can always answer "who changed this record, when, and why?"
Next Steps
- Matching Rules — Configure the rules that build the graph
- Survivorship — Control which values win when records merge
- Overrides — All override types (merge, split, lock, field pin)
- First Reconciliation — Build your first identity graph from CSV data
Need real-time graph queries, overrides API, and audit history?
Kanoniv Cloud adds a persistent identity graph with sub-millisecond resolution, search, and a full overrides API.
