Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowThe most expensive bug in a knowledge graph is one you never notice: the same real-world entity appearing as two, three, or fifty distinct nodes. "Tobias Weiss Consulting GmbH", "Tobias-Weiss Consulting", and "TWI GmbH" are one organization in the world and three nodes in your graph. Every relationship you attach to one split misses the others, and queries that should return a connected subgraph silently return fragments. Entity resolution is the discipline that prevents this — and at graph scale it is a pipeline problem, not a one-off dedup pass.
In a relational store a duplicate row is a nuisance; you might double-count
an aggregate. In a graph, a duplicate node is a disconnect. Same-entity
nodes no longer share neighbors: "Tobi's customer" is linked to A, B's
history is a separate island, and nothing joins them. Every traversal, every
recommendation, every PageRank-style computation is wrong in a way that is
harder to spot than a wrong number in a row — because the graph looks
connected even while the real connections are missing.
Graph quality therefore starts with identity: get "one real thing = one node" right, and everything downstream has a chance.
Resolution at scale is three stages, and each one earns its own tooling.
Comparing every pair of N records is O(N²) and hopeless beyond a few thousand rows. Blocking cuts the field: group candidates that might match on cheap signals — normalized name, postal code, first token, domain — and only compare within blocks.
block key: lower(city) | lower(first_token(name)) | normalized_domain
Blocking converts "does this entity exist?" into a short, bounded candidate list per incoming record.
Within a block, score candidate pairs on a weighted mix of signals: string similarity (token-based, tolerant of reordering), exact identifiers (VAT, registry number, domain), and relational context (do they share neighbors?). The output is a confidence score and a threshold decision:
score > trust → merge now
trust ≥ score > review → queue for human or LLM-assisted review
score ≤ review → new node
The crucial production habit is keeping a threshold band instead of a
single cutoff. Fully-automatic merges above trust, a review queue in the
middle, and confident new entities below. A knife-edge cutoff either
pollutes the graph with false merges or re-creates duplicates.
A merge is not "delete one node". It is a versioned, reversible operation:
pick a canonical node, re-point all its relationships, migrate properties,
and record why (the blocking key, the score, the signals) in an audit
trail. If a merge is wrong, you must be able to split it back. In a property
graph, prefer materializing canonical identity as a distinct node or a
consistent canonical_id property rather than silently mutating the loser.
Trees and tables resolve entities by attributes. Graph resolution adds a signal relational stores lack: shared neighbors. Two person records with different spellings but identical co-authors, employers, or associations are almost certainly the same person. Neighbor overlap is the highest-precision feature you have, and it's the one a flat dedup pass can't see.
The payoff is a graph where a query for an organization returns one connected story, not fragments. Identity is the load-bearing layer of every knowledge graph — and entity resolution is how you keep it honest at scale.