Entity resolution: how to make your knowledge graph accurate at scale
Apple Inc., AAPL and Apple are one company, however many documents mention them and however big your graph grows. Here is how entity resolution in GraphRAG-SDK brings them into one node, and how to turn it on in one line.
5 nodes. Apple Inc., AAPL and Apple become one company, IBM becomes one, and the fruit and both Parises each keep their own node.
Why one node per thing matters
Entity resolution is the process of recognizing when different names refer to the same real-world thing and merging them into one node. A knowledge graph is built one document at a time. A report says Apple Inc., a market feed says AAPL, an article says Apple. Entity resolution recognizes all three as one company, so every fact about Apple sits on one node and every question about Apple finds them all. And Paris, France and Paris, Texas stay two nodes, each with its own facts.
- Complete answers. Facts from every document about an entity meet in one place.
- Connected paths. Relationships from different documents join up, so multi-hop questions find their way.
- Every name still works. A merged entity keeps its other names: a search for “AAPL” finds Apple Inc.
How it works, in three steps
Entity resolution runs when you call finalize() after ingesting. It looks only at what you just added, checks it against everything already in the graph, and lets an LLM make the call.
1Check what is new against the whole graph
Each new entity’s name is turned into a vector, and FalkorDB‘s vector index finds the most similar names anywhere in the graph, not only in the same document. Identical spellings are matched too, and so are acronyms, such as IBM and International Business Machines. A new Apple Inc. finds Apple, AAPL and apple, the fruit, as its most similar names, and the four go to the judge together.
2An LLM judge decides, with the facts in front of it
Spelling alone never merges two entities. For every candidate, the judge reads its name, label, description and the properties your ontology declares. It answers that Apple Inc., AAPL and Apple are one company and the fruit is something else. Properties settle the hardest cases: two people called George Bush, both presidents, are told apart by birth_year: 1924 and birth_year: 1946.
3Merge without losing anything
Descriptions, other names, sources, properties and every relationship move to the surviving node. When two values conflict, one is kept and the other is saved as history. Every merge is planned before any is applied, and each one is a single database query, so you can stop a run at any point and the next finalize() finishes it.
The results
We added 5,000 new entities to real graphs of 10,000, 15,000 and 50,000 entities and let entity resolution run.
Real runs: FalkorDB in Docker, gpt-4.1 as the judge, text-embedding-3-large for names. On the 50,000-entity graph, about 99% of all 2,459 merges were correct, including 124 of 125 among the hand-labeled entities. Time to add and resolve: 193 s at 10,000 and 15,000 entities, 248 s at 50,000.
Get started
- FalkorDB 4.20 or later. GraphRAG-SDK checks the version and refuses older ones, which could drop vector-index entries.
- Already have a graph? Run
finalize(dedup_all=True)once to check every entity, not only new ones.
import asyncio from graphrag_sdk import ( GraphRAG, ConnectionConfig, LiteLLM, LiteLLMEmbedder, ) async def main(): async with GraphRAG( connection=ConnectionConfig( host="localhost", graph_name="my_graph", ), llm=LiteLLM(model="openai/gpt-4.1"), embedder=LiteLLMEmbedder( model="openai/text-embedding-3-large", dimensions=256, ), ) as rag: await rag.ingest( text="AAPL rose 3% after Apple Inc. ...", document_id="doc-1", ) # Required after ingest: resolves every new # entity against the whole graph, using the # llm above as the judge result = await rag.finalize() print(result.entities_deduplicated) asyncio.run(main())
Call finalize() once per batch of documents. To use a different model as the judge, import LLMJudge from graphrag_sdk and pass it to finalize(): rag.finalize(judge=LLMJudge(LiteLLM(model=...))).
FAQ
What is entity resolution?
Entity resolution finds the mentions in your documents that refer to the same real-world thing, such as Apple Inc., AAPL and Apple, and merges them into one node. It also keeps apart different things that share a name, such as Paris, France and Paris, Texas.
How is it different from deduplication?
Deduplication usually removes exact or near-identical copies. Entity resolution also decides when different names mean the same thing, and when the same name means different things. GraphRAG-SDK does both: identical names, similar names and acronyms all go to an LLM judge, which decides.
Does it re-check the whole graph every time?
No. Only entities added since the last run are checked, but each one is compared with the most similar names across the entire graph. To check every entity, for example on a graph built before this feature, run finalize(dedup_all=True) once.
Which model is the judge?
By default, the llm you passed to GraphRAG. To use another model, import LLMJudge from graphrag_sdk and pass it to finalize(judge=...). You can also write your own judge by implementing DedupJudge.
Is anything lost when entities merge?
No. Other names, descriptions, sources, properties and every relationship move to the surviving node. When two values conflict, one is kept and the other is saved as history. A search for a merged name, such as AAPL, still finds the entity.
Author
-
Software Engineer at FalkorDB, working across AI, GraphRAG, and developer platforms. He builds graph-powered AI solutions, contributes to GraphRAG, Snowflake integrations, and MCP tooling, and develops full-stack products while driving automation, testing, and open-source initiatives with Python and TypeScript.