Repository navigation
Entity resolution at ingestion — where do you dedupe entities before they hit the graph store? #22618
Replies: 2 comments 2 replies
|
I'd lean toward doing the primary resolution at ingestion time. Once Apple, Apple Inc. and AAPL become separate nodes, it's not just retrieval that gets noisy. Edges, degree counts, communities, multi-hop traversal... all of that inherits the mess too. Deduping only at query time kind of papers over the duplicates in the final answer without actually fixing the graph underneath. Honestly I'd probably want both: ingestion-time resolution as the main mechanism, plus a periodic reconciliation pass, since new documents can surface evidence that two previously separate entities are actually the same thing. |
|
We’ve run into this exact fragmentation problem in production GraphRAG setups with Neo4j and PropertyGraphIndex. Relying on query-time deduping quickly turns multi-hop Cypher traversals into a performance nightmare because every synonym path duplicates graph traversal branches and fragments Louvain/Leiden community clustering. Here is the architectural pattern that has worked best for us in high-throughput Graph pipelines: 1. Synchronous Ingestion: Deterministic CanonicalizationFor the primary ingestion pipeline, you want zero LLM latency and deterministic guarantees:
MERGE (n:Entity {id: $canonical_id})
ON CREATE SET
n.name = $primary_name,
n.aliases = [$mention],
n.createdAt = datetime()
ON MATCH SET
n.aliases = apoc.coll.toSet(n.aliases + [$mention]),
n.mentionCount = coalesce(n.mentionCount, 0) + 1This guarantees that trivial variants ("Apple", "Apple Inc.") collapse immediately into a single node with all edge connections aggregated cleanly. 2. Ambiguous Pairs: Soft
|
Uh oh!
There was an error while loading. Please reload this page.
We've been building a GraphRAG pipeline with LlamaIndex's PropertyGraphIndex → Neo4j, and kept hitting the same wall: the extractor is great at finding entities, but it has no memory of which ones are the same node. Every run, "Apple Inc.", "Apple", and "AAPL" land as three separate nodes, and the graph slowly fragments.
How are others handling entity resolution at the ingestion boundary today — inline Cypher MERGE, a post-processing pass, or leaving it to retrieval time?
What we landed on (sharing for feedback):
A 3-layer short-circuit before anything reaches the graph store:
The goal is to avoid sending every entity pair to an LLM. On a 500-doc / 3,000-mention / 63-identity benchmark (real run, real Groq pricing), this resolved 99.4% recall / 100% precision with 7 LLM calls and 1,566 tokens total, vs ~661k tokens ($0.0334) for full-LLM ER.
Honest limitation: ~18 of the 3,000 mentions were missed merges without an alias map (~4 with one), because Layer 1 can't bridge abbreviations it hasn't seen yet. That's the next thing I'm working on.
I open-sourced the approach as AutoGraft (MIT, one-line wrapper for the Neo4j store) — linking it as a reference, not a pitch. What I'd really like to hear: how do you all handle this? Is anyone doing ER after the graph is built (query-time or periodic merge job) rather than at ingestion? The trade-offs feel non-obvious.
All reactions