Neo4j Entity Resolution: Methods & Guide

The article

Query your table as a graph.Petabytes. Subsecond. No data movement.

Free downloadBook demo

Neo4j Entity Resolution: Methods & Guide

An entity resolution pipeline needs to explain why two records belong together and preserve enough evidence to reverse that decision. Neo4j makes those connections queryable: source records can share identifiers, carry candidate-match relationships, and point to a canonical entity without losing their original identities.

This guide explains the architecture, matching techniques, and Cypher patterns behind that approach. A customer-record example shows how to separate shared evidence from accepted matches, then extends that distinction into pipeline design and operational limitations.

What is entity resolution in Neo4j?

Entity resolution identifies records that refer to the same real-world person, organization, product, or other entity. Within one dataset it supports deduplication; across datasets it supports record linkage. In Neo4j, a property graph represents the records, their identifying evidence, and the resolution decisions.

Consider a CRM entry for Maya Chen and a support-system entry for Maia Chen. Their names differ, but their email address and phone number agree. A third record for Leo Chen shares the phone number. The first pair deserves investigation; the shared phone alone could indicate a household.

Keep two concepts separate in the model. A Record represents a source observation. An Entity represents the identity that the resolution process assigns to one or more records. Neo4j stores and queries this model; your matching policy determines when an assignment is justified.

How does entity resolution work in Neo4j?

The workflow begins by preserving source identifiers and standardizing comparison fields. Names may receive whitespace and case normalization; phone numbers need a consistent format with country context. Keep the raw values so reviewers can inspect what the source supplied.

Next, generate candidate pairs. Blocking limits comparisons to plausible matches, such as records sharing a normalized email or a surname and postal region. The ONS working paper on data linkage, February 2021 describes blocking as reducing the search space before more expensive comparisons.

Score those candidates using field agreement, text similarity, source reliability, and relationship evidence. Keep the features alongside the score. A reviewer needs to distinguish a name match backed by two identifiers from a name match backed by a shared office address.

Finally, accept, reject, or defer each decision and assign accepted records to canonical entities. Treat the assignment as maintained data: a corrected source record or a reassigned phone number can change the evidence. This separation gives each stage an explicit output that can be evaluated independently.

Why use graph databases for entity resolution?

Graphs make shared evidence explicit. A normalized email can be an Identifier node linked to every record that contains it. Traversing through that node reveals both plausible duplicates and unusually common contact details that deserve less weight.

That structure is useful in several operational settings. Customer support can retrieve the source records behind a consolidated profile. Fraud investigators can follow shared devices or payment instruments while keeping associated people distinct. Supplier onboarding can retain separate legal entities while examining their registered addresses and ownership relationships.

Graph queries also expose the reasoning behind a proposed cluster. If one candidate pair shares an email and another shares a phone, the connecting path shows exactly how the cluster formed. Inspecting that path can reveal a weak bridge before it joins unrelated people.

These advantages depend on the model. SQL systems can also store provenance and perform record linkage. A graph becomes useful when the relationships themselves, including the paths that support or undermine a decision, are frequent objects of investigation.

Key challenges in traditional entity resolution

Candidate volume. Comparing every record with every other record creates n(n−1)/2 unordered pairs. Blocking reduces that workload, but an overly strict block excludes true matches before scoring begins. Multiple blocking passes can recover candidates missed by a single key.

Ambiguous evidence. Names repeat, households share phones, and organizations reuse contact addresses. Missing values need their own treatment: turning every absent email into a common placeholder creates artificial matches. Agreement on a rare identifier should generally carry different weight from agreement on a common surname.

Changing identities and observations. People move, organizations rename themselves, and source systems correct mistakes. Record when an identifier was observed and, where available, when it was valid. Otherwise, an old association can continue influencing current decisions after its meaning has changed.

Unequal error costs. A false merge can expose another customer's history or contaminate a risk assessment. A missed match can fragment a legitimate customer's records. Evaluate both rather than optimizing the number of duplicates removed. The ONS data linkage policy distinguishes precision, the share of links made that are correct, from recall, the share of true matches recovered.

These challenges persist in graph-based systems. The graph makes evidence and consequences easier to inspect; data quality, candidate coverage, and decision policy still determine resolution quality.

Neo4j entity resolution architecture

A practical architecture separates ingestion, matching, graph storage, and review. Source systems supply records through batch loads or change streams. A preparation layer normalizes fields and preserves source provenance. Neo4j holds the evidence graph and the decisions that applications need to query.

Graph elementMeaningDesign constraint
Record nodeOne source recordUse a stable, source-qualified key
Identifier nodeOne normalized, typed identifierDistinguish email, phone, and other identifier namespaces
HAS_IDENTIFIER relationshipA record contains that identifierPreserve observation or validity information when available
POSSIBLE_MATCH relationshipA candidate pair and its evidenceStore rule version and feature values separately from approval
Entity node and RESOLVES_TO relationshipAn accepted canonical assignmentPreserve decision provenance and support reassignment
Neo4j entity resolution architecture

Figure: Candidate evidence and accepted identity assignments remain distinct: the decision step creates canonical links while preserving the source records and the evidence behind them.

Graph Data Science (GDS) adds an optional analytical layer. Its algorithm workflow creates an in-memory graph for algorithms. Choose which labels, relationships, and properties belong in that projection. A graph for neighborhood similarity has different semantics from a graph containing only accepted identity links.

GDS also separates execution modes: stream returns results, mutate changes the projected graph, and write persists algorithm output to Neo4j. None of those modes supplies your acceptance policy. Keep scoring output behind a decision step before publishing canonical assignments to downstream consumers.

Entity matching techniques in Neo4j

Deterministic matching applies explicit agreement rules. A trusted customer identifier within its issuing system may justify a direct match; agreement on name alone usually does not. Cypher patterns can express combinations of identifiers and contradictions, making the rule inspectable.

Fuzzy text matching handles spelling variation. APOC Core provides apoc.text.levenshteinSimilarity, which returns a value between zero and one based on edit distance. Use it on normalized strings after candidate generation. Its output measures string similarity, not the probability that two records describe the same person.

Probabilistic and learned matching combine evidence across fields. A statistical linkage model can weight agreement according to how likely it is among matches and nonmatches; a supervised classifier can learn from labeled pairs. Neo4j can supply relationship features to an external classifier and store its decisions as graph properties or relationships. The government review of linkage quality distinguishes deterministic, probabilistic, and machine-learning approaches and their shared dependence on identifying data.

Graph algorithms answer additional questions, but their outputs need careful interpretation:

TechniqueWhat it compares or groupsIdentity-resolution failure mode
GDS Node SimilarityShared graph neighbors, using metrics such as JaccardShared household or organizational context looks similar
GDS k-nearest neighborsConfigured node properties, including feature vectorsSimilar profiles can belong to different entities
Weakly connected componentsNodes connected by paths, ignoring directionOne incorrect bridge joins otherwise separate groups

Node Similarity can compare records through their identifier neighborhoods. K-nearest neighbors compares configured properties instead. Neither is a substitute for evaluating identity-specific evidence.

Weakly connected components is useful for grouping accepted pairwise links, with a subsequent check for contradictions across the group. Treating components formed from every shared phone or possible match as canonical identities would turn association into identity. Community membership and canonical identity need separate meanings in both the schema and the application.

Using Cypher for entity resolution

The following example uses synthetic CRM and support records and was tested on Neo4j 5.26.32 Community with APOC Core 5.26.32. Run the blocks in order in an empty database with APOC Core installed. GDS is unnecessary for these queries.

First, create uniqueness constraints for source records, typed identifiers, and canonical entities:

CREATE CONSTRAINT er_record_id IF NOT EXISTS
FOR (r:Record) REQUIRE r.recordId IS UNIQUE;

CREATE CONSTRAINT er_identifier IF NOT EXISTS
FOR (i:Identifier) REQUIRE (i.kind, i.value) IS UNIQUE;

CREATE CONSTRAINT er_entity_id IF NOT EXISTS
FOR (e:Entity) REQUIRE e.entityId IS UNIQUE;

Load the records and connect their normalized contact identifiers. Here the email and phone values are already normalized; the query retains each original name and creates a lowercase comparison field.

UNWIND [
  {id:'CRM-101', source:'crm', name:'Maya Chen',
   email:'maya.chen@example.test', phone:'+12025550101'},
  {id:'SUP-204', source:'support', name:'Maia Chen',
   email:'maya.chen@example.test', phone:'+12025550101'},
  {id:'CRM-305', source:'crm', name:'Leo Chen',
   email:'leo.chen@example.test', phone:'+12025550101'},
  {id:'SUP-406', source:'support', name:'Maya Chen',
   email:'maya.chen@example.test', phone:'+12025550199'},
  {id:'CRM-507', source:'crm', name:'Ravi Shah',
   email:'ravi.shah@example.test', phone:'+12025550177'}
] AS row
MERGE (r:Record {recordId:row.id})
SET r.source = row.source,
    r.name = row.name,
    r.nameNorm = toLower(trim(row.name))
WITH r, row
UNWIND [
  {kind:'email', value:row.email},
  {kind:'phone', value:row.phone}
] AS contact
WITH r, contact
WHERE contact.value IS NOT NULL AND contact.value <> ''
MERGE (i:Identifier {kind:contact.kind, value:contact.value})
MERGE (r)-[:HAS_IDENTIFIER]->(i);

MERGE matches an existing pattern or creates it. It does not decide that similar records represent the same person. The constraints protect the keys used here; source records remain separate nodes.

Generate candidates through shared identifiers, then apply a name-similarity filter:

MATCH (a:Record)-[:HAS_IDENTIFIER]->(i:Identifier)
      <-[:HAS_IDENTIFIER]-(b:Record)
WHERE a.recordId < b.recordId
WITH a, b, count(DISTINCT i) AS sharedIdentifiers
WITH a, b, sharedIdentifiers,
     apoc.text.levenshteinSimilarity(
       a.nameNorm, b.nameNorm
     ) AS nameSimilarity
WHERE nameSimilarity >= 0.8
MERGE (a)-[m:POSSIBLE_MATCH {ruleVersion:'contact-v1'}]->(b)
ON CREATE SET m.decision = 'review'
SET m.sharedIdentifiers = sharedIdentifiers,
    m.nameSimilarity = nameSimilarity
RETURN a.recordId AS leftRecord, b.recordId AS rightRecord,
       sharedIdentifiers, round(nameSimilarity, 3) AS nameSimilarity
ORDER BY leftRecord, rightRecord;

The ID ordering emits each unordered pair once. The 0.8 cutoff is illustrative, not a validated production threshold. Every surviving pair starts in review; a name score alone grants no canonical assignment.

On the supplied records, the query returns:

Left recordRight recordShared identifiersName similarity
CRM-101SUP-20420.889
CRM-101SUP-40611.000
SUP-204SUP-40610.889

The exact name match involving SUP-406 has less corroborating contact evidence than the Maya/Maia pair. Leo shares the household phone but fails the name filter. Ravi shares neither contact identifier and never enters scoring. This illustrates both the usefulness and the limits of the block: a real match with both contacts changed would also be missed.

Even two agreeing identifiers need scrutiny. If one source copied both fields from another, that agreement is not independent corroboration. Use source provenance and contradiction checks when interpreting that evidence; do not turn the identifier count into a probability.

For this example, suppose a reviewer confirms that CRM-101 and SUP-204 represent one person. Persist that specific decision while retaining both records:

MATCH (a:Record {recordId:'CRM-101'})
      -[m:POSSIBLE_MATCH {ruleVersion:'contact-v1'}]->
      (b:Record {recordId:'SUP-204'})
SET m.decision = 'accepted', m.decisionId = 'REVIEW-001'
MERGE (e:Entity {entityId:'PERSON-001'})
MERGE (a)-[ra:RESOLVES_TO]->(e)
SET ra.decisionId = 'REVIEW-001'
MERGE (b)-[rb:RESOLVES_TO]->(e)
SET rb.decisionId = 'REVIEW-001'
RETURN e.entityId AS entityId,
       a.recordId AS firstRecord, b.recordId AS secondRecord;

This assignment assumes neither record already belongs to another entity. A production decision service must check existing assignments and record the reviewer, time, and supporting evidence. It also needs a correction path when later evidence invalidates a decision.

The query returns PERSON-001 with CRM-101 and SUP-204. The other Maya record remains unassigned, and Leo's shared phone remains visible without making him part of that entity. Keeping these records intact lets a reviewer revisit either conclusion without reconstructing deleted source nodes.

How to build an entity resolution pipeline in Neo4j

Define the identity contract. Specify whether you are resolving people, households, accounts, or legal entities. Those are different targets. Decide which source identifiers are authoritative within which namespaces, and define how preferred attributes are selected when accepted records disagree.

Ingest with repeatable keys. Choose an import method appropriate to the source and update cadence. Preserve raw observations, normalize comparison fields, and make retries safe through stable keys and constraints. The example loader handles an unchanged fixture; it does not retract an old identifier when a source changes. Production ingestion must remove or expire superseded relationships and process deletions.

Measure candidate coverage before tuning scores. Try several blocking passes and deduplicate their candidate pairs. Include known matches that disagree on the first blocking key. A perfect classifier cannot recover a pair it never receives. For incremental runs, begin with changed records and the affected identifier neighborhoods, then assess whether their existing clusters also need reconsideration.

Validate decisions on held-out examples. Include spelling variations, shared households, missing fields, and conflicting identifiers. Measure pair precision and recall as well as false merges and splits at the entity level. When evaluating generalization to unseen entities, keep evaluation entities separate from training entities to reduce leakage. Inspect results by source pair and data-quality segment, since an acceptable average can hide a poorly served population.

Publish versioned, reversible assignments. Use explicit accept, reject, and review outcomes. Persist rule or model versions, decision provenance, and the evidence snapshot. The example's ON CREATE preserves an existing review decision on an unchanged rerun; changed evidence requires explicit invalidation or a new evaluation version. Enforce one current assignment per record through the decision service, and preserve superseded assignments for investigation.

For larger analytical batches, project the relevant graph into GDS, inspect results before writing them, and refresh projections when their source evidence changes. Monitor candidate volume, reviewer backlog, cluster-size changes, and overturned decisions. These measures reveal whether the pipeline remains useful after its initial labeled dataset stops representing incoming records.

Limitations of Neo4j for entity resolution

Neo4j gives teams a graph database and analytical tools, but the resolution policy remains application work. Similarity functions, graph algorithms, and Cypher patterns need domain-specific validation. A well-formed graph can still encode incorrect identity decisions.

Large identifier hubs remain expensive. A contact value shared by many records creates many candidate pairs. Filter known placeholders, distinguish generic organizational contacts, and control high-degree identifiers before expensive scoring. An index helps find the contact node; it does not eliminate the fan-out behind it.

GDS introduces an analytical memory budget. Projected graphs and algorithm execution consume memory beyond the stored graph. Use Neo4j's memory estimation facilities, project only the evidence needed, and schedule analytical work with operational traffic in mind. Returning fewer similarity results is not equivalent to avoiding all underlying comparisons.

A separate graph store needs synchronization. When source records and accepted mappings live in a warehouse or lakehouse, a pipeline into a persistent Neo4j database must keep the graph representation in sync. That includes propagating corrections, deletions, and identifier changes, plus any decision history the graph exposes. This is a reasonable trade when Neo4j owns the operational graph, but it adds work when the primary requirement is querying relationships already recorded in tables.

For that latter case, the architecture question is where to run the graph queries. The same customer-resolution model can be expressed over tables of records, identifiers, candidate pairs, and accepted assignments.

PuppyGraph is the first and only zero-ETL graph query engine that queries existing relational data as a graph without loading it into a separate graph database. It is a distributed graph query engine over SQL databases, data warehouses, and data lakes and lakehouses, including direct reads of open table formats such as Iceberg and Delta Lake. Connect the sources, define a graph schema mapping tables to nodes and edges, and query with openCypher and Gremlin while those same tables remain available to SQL.

Comparison of the best data lakehouse solutions

Figure: PuppyGraph queries the existing tables in place, so graph and SQL queries share one copy of the data and no pipeline keeps a graph copy in sync.

In this design, the resolution pipeline continues to own normalization, matching, and accepted assignments in the source tables. PuppyGraph supplies the graph query layer for inspecting their relationships, so removing graph-specific ETL leaves the identity decisions and their evidence under the pipeline's control.

Comparison of the best data lakehouse solutions

Figure: Companies that use PuppyGraph.

Comparison of the best data lakehouse solutions

Figure: Engineering teams at PuppyGraph customers on what they use it for.

Conclusion

Neo4j entity resolution works best when source observations, candidate evidence, and accepted identities remain distinct. Cypher makes the matching rules inspectable; APOC and GDS add comparison and grouping techniques. Their value depends on candidate coverage, measured decision quality, and the ability to correct assignments as evidence changes.

Choose the storage architecture around the workload: an operational graph maintained in Neo4j, or graph queries over resolution evidence already held in existing tables.

Try the forever-free PuppyGraph Developer Edition and book a demo with the team to see how openCypher and Gremlin queries connect records and identity evidence across warehouse and lakehouse tables, with no graph-specific ETL, while your resolution pipeline retains control of accepted matches.

Hao Wu

Hao Wu

Software Engineer

Hao Wu is a Software Engineer with a strong foundation in computer science and algorithms. He earned his Bachelor’s degree in Computer Science from Fudan University and a Master’s degree from George Washington University, where he focused on graph databases.
Free downloadBook demo

Read next

All articles

Start free. Pay when it goes to production.

Contact us for any questions about capabilities, sizing and support.

* No payment required to start either one.

$ docker run -p 8081:8081 \
  -p 8182:8182 -p 7687:7687 \
  puppygraph/puppygraph:latestPulling puppygraph/puppygraph:latestStarting PuppyGraphWeb console listening on 0.0.0.0:8081Gremlin endpoint listening on 0.0.0.0:8182PuppyGraph is running# no pipelines · no second database · no data movement