Entity Resolution: One Thing, Many Names
Sam K., Samuel Kim, sam@acme.com. Deciding what's the same thing is harder and more important than it looks.
RAG vs Knowledge Graphs: Two Kinds of AI Memory Lookup ended on a crack in the stack: none of it works if one person exists as two separate nodes. This part fixes that crack. Tracing and debugging, from Debugging Agent Systems: Traces, Replays, Root Causes, help you find a broken reasoning step. But one of the most underrated problems in AI systems sits underneath the reasoning: giving the agent a clean view of who and what it's reasoning about. That's entity resolution. In Mailmind, it's the layer that decides whether "Sam K.", "Samuel Kim", and sam.kim@work.com are one person or three. Get it wrong and every downstream answer that depends on "Sam" silently fragments.
The opposite mistake is worse. Merge two different real people into a single canonical node and your assistant will confuse Alex Chen the recruiter with Alex Chen the investor for weeks. Every thread with one Alex leaks into the other. The agent's accuracy drops with no obvious error message. A false merge poisons retrieval, memory, and every summary that crosses the merged node. Avoiding that means building a pipeline that is paranoid about sameness.
Why is deciding sameness so hard?
Think of a hospital with three record systems, each spelling a patient's name a different way. Without resolution, a doctor sees a third of the history. Mailmind faces the same mess. Senders appear as "John D.", "John Doe", "john@company.com", and "j.doe@personal.net" across threads. A simple string match fails on nicknames, typos, and abbreviations. Add cross-language forms and you get "Giovanni Rossi" vs "John Rossi".
The hardest part is ambiguity. Two real people genuinely named Sam Kim might both email you. The system must keep them separate even if their names overlap heavily. At the same time, related but distinct identities, like a person's work self and personal self, might need to be linked for an "everything about Sam" query but kept distinct for an "only work Sam" filter. That tension between merging too much and too little is the core challenge.
How does the resolution pipeline work?
Mailmind processes every new contact through a six-stage pipeline that avoids the naive trap of comparing everything with everything. Suppose Mailmind knows 100,000 contacts. Deduplicating that list by comparing every pair would take about 5 billion comparisons, because the work grows with the square of the number of records (that is what "O(n²)" means). Even checking one new contact against all of them means 100,000 comparisons, and most of those pairs are obviously different people. Instead, the system groups records into cheap, rough buckets and only compares within those buckets.
The stages go like this. First, blocking splits the world into coarse groups. Second, candidate generation picks plausible matches inside each block. Third, feature extraction computes similarity signals across multiple dimensions. Fourth, scoring combines those signals into a single confidence score. Fifth, decision thresholds sort matched pairs into auto-merge, human review, or keep-separate bins. Sixth, when a merge is confirmed, the system fuses all information into one canonical node and keeps an audit trail that is never rewritten.
How does blocking avoid comparing every pair?
Blocking uses cheap rules, tuned to miss as few real matches as possible, to drastically shrink the comparison space. For email contacts, Mailmind's blocks include the sender domain, the first two letters of the first name plus the domain, or a phone area code if present. A record with domain @acme.com only gets compared against other records in the same domain block. A record with no shared block is never considered.
- Compare against all 100,000 contacts
- About 100,000 comparisons per new contact
- About 5 billion comparisons to clean the whole list
- Only compare within relevant blocks
- A few hundred candidates per new contact
- Fast enough to run as each email arrives
The price of blocking is that real matches can be missed if the block key is too strict. For example, if someone changes employers, the domain block no longer captures them. Mailmind compensates by using several overlapping blocks and by running a slower, periodic sweep that reconsiders records with no matches. The result is a practical balance: many cheap comparisons and very few that need deep similarity scoring.
How are features extracted?
Once candidates are generated, the system extracts numeric signals from raw attributes. Name similarity uses several measures at once: Levenshtein edit distance on normalized strings (how many letter edits turn one name into the other), Jaccard similarity over the words in each name (how many words two names share, out of all the words in either), and phonetic encodings that catch names that sound alike, such as Soundex, which gives "Smith" and "Smyth" the same code (S530). Soundex keeps the first letter as is, so it does not match "Katherine" (K365) with "Catherine" (C365), as the National Archives' Soundex guide shows: the code always starts with the name's first letter. Attribute overlap checks for identical phone numbers, mailing addresses, or company names. Relationship overlap measures how many shared contacts appear in both threads. Context similarity looks at signature patterns, writing style, or recurring meeting rooms.
Each feature vector becomes a row of numbers. No single feature is strong enough to trigger a merge. The system treats them as clues that reinforce each other. A 90% name match plus an identical phone number is strong evidence. A 90% name match with no other overlapping signals is suspicious.
How are scores combined?
Mailmind uses a lightweight gradient-boosted classifier (a small, fast machine learning model that learns from labeled examples) trained on historical edge cases. The model takes a feature vector for a candidate pair and outputs a probability that they are the same entity. Rule-weighted scoring served the first version, but the classifier adapts to the specific noise patterns of this inbox. LLM (large language model) judgment is also an option for tricky pairs. Sending the two profiles to a small language model with a few-shot prompt yields a flexible verdict, but costs far more per comparison. So it's reserved for pairs that land in the review band.
The scoring layer is not one-size-fits-all. Different entity types get different models. A person merge classifier weighs relationship overlap more heavily. A company merge classifier relies on address and industry hard blockers. This separation keeps the model from learning a single brittle threshold that works for nothing.
How does the decision threshold work?
Scores from the classifier land in one of three bands. Above 0.95, the system auto-merges. Between 0.65 and 0.95, the pair goes to a human review queue, surfaced inside Mailmind's admin panel as "possible duplicate: Sam Kim." Below 0.65, the pair stays separate. The boundaries were set by measuring precision and recall on a golden set of known duplicate pairs and deliberately keeping the auto-merge band narrow. These numbers are Mailmind's choices; yours come from your own golden set.
The three bands are an old idea. Fellegi and Sunter's 1969 paper A Theory for Record Linkage set out the classic model: compare how likely a pair's pattern of agreements is among true matches and among non-matches, then use two thresholds to split pairs into links, possible links for a person to review, and non-links. Open-source tools still build on it. Splink, for example, is a Python package for probabilistic record linkage based on the Fellegi-Sunter model (version 5 as of October 2026). dedupe is an older Python library that uses machine learning for fuzzy matching and entity resolution; its code was last updated in July 2025.
Human reviewers see a side-by-side diff of the two records: aliases, email addresses, recent threads, and any hard blocker warnings. They accept, reject, or link the identities. Every human decision feeds back into the training set so the classifier improves over time. False merges are still possible, but the middle band catches most of them before they corrupt the graph.
How does Mailmind store a resolved person?
Once a merge is confirmed, the system creates one canonical node with a stable ID. The original records become aliases pointing to the same node, never deleted. This design means a query for "Sam K." always routes to the canonical node. The canonical node carries a primary display name, a list of aliases, a verified phone number and email when available, and references to any linked identity nodes.
Linking handles the work-self and personal-self problem. Mailmind creates a separate "identity" sub-node for each different email domain and links both to the same person. A query that scopes to work threads follows the work identity link. A query that wants everything about Sam follows the person node. This linked structure avoids two bad outcomes: merging identities that should stay separate and duplicating a person because they use two email addresses.
| Property | Value |
|---|---|
| Canonical person ID | UUID v7 |
| Primary display name | Derived from the most frequent verified name |
| Aliases | Set of all observed name strings |
| Hard identifiers | Verified phone, verified email |
| Linked identities | Work identity, personal identity, and so on |
| Audit log | Timestamped merge and unmerge events |
| Review status | auto_merged, human_reviewed, unmerged |
Why are hard blockers more important than similarity?
Hard blockers are rules that forbid a merge no matter how high the similarity score. Two "Acme Industries" in different countries and unrelated industries are almost certainly different companies. These rules are coarse, but they prevent the most catastrophic failures. Mailmind uses two strengths of blocker, because people do change employers and do use work and personal addresses:
- Blocks an auto-merge, but not a reviewed link: different company domains, or zero shared contacts across months of threads. A person may still confirm that the two records belong to one human, and Mailmind then links them as identities of one person.
- Blocks both a merge and a link: different verified phone numbers. Two people with different verified phones are treated as two people unless someone corrects the data itself.
Similarity is seductive. A 0.98 name match feels like a slam dunk. But without a shared unique identifier, it can still be a trap. The system enforces the rule "no shared unique identifier means no auto-merge." If two Sam Kim records share no verified phone or verified email and have never appeared in the same thread, the pair goes straight to human review, no matter how similar their names look. That one rule catches the two-different-Alex-Chen problem before it poisons every downstream answer.
How does incremental resolution keep up?
A full rebuild of the entity graph for every new email would waste compute and introduce lag. Mailmind runs incremental resolution. When a new contact arrives, the system computes its blocks, fetches only the candidates in those blocks, scores the pairs, and decides. Unmatched external records that don't link to any known entity are staged in a pending pool and revisited later when more signals accumulate.
This incremental path is light enough to run inline as each email arrives. Every night, a batch job reprocesses the pending pool, re-scores previously reviewed pairs that have gained new signals (like a new shared contact), and runs a full pass on low-activity blocks that might contain missed matches. The combination of fast inline resolution and overnight re-evaluation keeps the assistant current without slowing down the inbox.
Frequently Asked Questions
Q: Can't an LLM just decide if two contacts are the same?
An LLM can, and it handles messy natural language better than a classifier. Mailmind reserves that path for pairs in the human review band where the cost is acceptable. Using it for every pair would blow the latency and cost budgets. A focused classifier on pre-extracted features gives you most of the value at a small fraction of the cost.
Q: What happens when a user manually splits a merged person?
The system records an unmerge event. It creates two new canonical nodes, reassigns edges by original source record (threads, calendar events, and so on), and flags all previously generated outputs that crossed the merged node for regeneration. The audit log preserves the full history so the team can diagnose what went wrong.
Q: How do you handle a person who changes email domains?
If the new email has no other matching signals, it creates a separate identity node that sits in the pending pool. Over time, if that identity starts sharing threads, contacts, or meeting patterns with an existing person, the nightly batch job proposes a link, and once confirmed it becomes a linked identity under the same canonical node. The different domain alone never blocks that link. A different verified phone number does.
Q: Does blocking miss matches across different blocks?
Yes. That's a deliberate tradeoff. The nightly sweep compares records that ended up in disjoint blocks but share fuzzy name and relationship signals. Many false negatives from blocking are caught by that sweep. Real-time inline resolution accepts a small false negative rate to keep latency low.
Q: How do you measure resolution quality?
A golden set of manually curated duplicate and non-duplicate pairs, built from historical decisions, acts as a holdout. Precision, recall, and F1 on that set are monitored daily. Drift detection fires if the classifier's performance shifts, usually when the mix of contacts changes. Shadow mode runs the current and candidate models side by side on live traffic without affecting merges, flagging disagreements for review.
Test yourself
A Mailmind user reports that for three weeks, all summaries about "Alex Chen" mixed threads from two different people. Investigation reveals a single false merge fused two Alex Chens six weeks ago. One Alex is a recruiter at a staffing firm. The other is a venture investor. They share no threads, no phone numbers, and no company domain. The merge happened because both signed emails with "Alex" and their full names matched exactly. How would you fix the immediate mess and prevent it from happening again?
Answer: First, unmerge the node into two canonical entities. Every edge (email threads, calendar invites, receipts) must be reassigned to the correct Alex based on the original sender address or other source metadata. Any agent outputs generated while the merge was active are flagged for regeneration. Second, audit the classifier's feature weights. The false merge reveals overreliance on name similarity without enough weight on relationship overlap and hard identifiers. Enforce the rule that no shared unique identifier means no auto-merge: two records with zero shared threads, no verified phone and no overlapping organization go to human review, whatever the name score. Raise the weight of relationship overlap in the scoring function and tighten the auto-merge threshold. Finally, add the false pair to the golden evaluation set so the retrained model has a direct example of the failure mode. Hard blockers, retraining and a growing golden set make this class of failure rarer.
Next: Graph Databases for AI: When Relationships Are the Data. It covers the database underneath a knowledge graph like Mailmind's, where these resolved people and their relationships live.
If you want this kind of breakdown every week, how real systems actually work under the hood, subscribe to Internals Decoded at internalsdecoded.com.
Sources
- National Archives' Soundex guide
- A Theory for Record Linkage
- github.com · Splink
- github.com · Dedupe