Consolidate duplicates
Run resolution over every entity in the graph. Review the matches it finds. Then apply them.
When to use this guide
Graph.add already compares new mentions with the entities that the graph stores. Run Graph.consolidate in two cases. You added data before you configured an LLM, so some pairs stayed unmatched. Or you want to review the whole graph again.
Before you start
- A running Neo4j and the
NEO4J_*variables. The Quickstart shows how. - The extras for local extraction:
uv pip install "agentic-graphrag[neo4j,extract]"
- Optional: an LLM for the last tier. Install the
llmextra, and setLLM_BASE_URL,LLM_API_KEY, andLLM_MODEL_ID. Without an LLM, that tier returns no match for every pair and raises no error.
1. Add documents that name one person in three ways
import asyncio
from agrag.common.data_models import GENERIC
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import GlinerExtractor, Graph
DOCUMENTS = [
"Charles Babbage designed the Analytical Engine in London.",
"Babbage Charles taught mathematics at Cambridge.",
"C. Babbage proposed the Difference Engine.",
]
async def main() -> None:
graph_store = build_graph_store("neo4j")
graph = await Graph.open(
schema=GENERIC,
graph_store=graph_store,
embedder=build_embedder("ibm-granite/granite-embedding-small-english-r2"),
extractor=GlinerExtractor(),
)
try:
for text in DOCUMENTS:
result = await graph.add(text=text)
print(result.resolution)
finally:
await graph_store.close()
asyncio.run(main())
Each add prints a resolution summary. ambiguous_count counts the pairs that an LLM called uncertain. Without an LLM, "C. Babbage" stays a separate entity.
2. Preview and apply the matches
Graph.consolidate() is a dry run. It writes nothing. Graph.consolidate(apply=True) writes the matches. This script previews, applies, and then reads the groups back.
import asyncio
from agrag.common.data_models import GENERIC
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import GlinerExtractor, Graph
async def main() -> None:
graph_store = build_graph_store("neo4j")
graph = await Graph.open(
schema=GENERIC,
graph_store=graph_store,
embedder=build_embedder("ibm-granite/granite-embedding-small-english-r2"),
extractor=GlinerExtractor(),
)
try:
report = await graph.consolidate()
ids = [
str(entity_id)
for match in report.would_match
for entity_id in (match.entity_a_id, match.entity_b_id)
]
rows = await graph_store.execute_read(
"MATCH (e:_AgragNode) WHERE e.id IN $ids RETURN e.id AS id, e.name AS name",
{"ids": ids},
)
names = {row["id"]: row["name"] for row in rows}
for match in report.would_match:
first = names[str(match.entity_a_id)]
second = names[str(match.entity_b_id)]
print(f"{first} = {second} ({match.comparator}, score {match.score})")
print("uncertain pairs:", report.ambiguous_count)
applied = await graph.consolidate(apply=True)
print("applied:", applied.applied, "failures:", len(applied.failures))
groups = await graph_store.execute_read(
"MATCH (r:ResolvedEntity)<-[:RESOLVED_AS]-(e) "
"RETURN r.name AS resolved, collect(e.name) AS members"
)
for row in groups:
print(row["resolved"], "<-", sorted(row["members"]))
finally:
await graph_store.close()
asyncio.run(main())
Without an LLM, the output shows the one match that the fuzzy tier found:
Charles Babbage = Babbage Charles (fuzzy_fast_path, score 1.0)
uncertain pairs: 0
applied: True failures: 0
Charles Babbage <- ['Babbage Charles', 'Charles Babbage']
With an LLM configured, the last tier also matches "C. Babbage", and the three names form one group:
Babbage Charles = Charles Babbage (fuzzy_fast_path, score 1.0)
Charles Babbage = C. Babbage (llm, score None)
Babbage Charles = C. Babbage (llm, score None)
uncertain pairs: 0
applied: True failures: 0
Babbage Charles <- ['Babbage Charles', 'C. Babbage', 'Charles Babbage']
The order of the names, and the name of the group, can differ in your run.
3. Read the report
would_matchlists every confirmed match. Each item has the two entity ids, the tier, a score when the tier has one, and the reason when an LLM decided. The tier isfuzzy_fast_path,embedding, orllm.ambiguous_countis the number of pairs that an LLM called uncertain. These pairs never merge.appliedisTruewhen the matches were written.failureslists any match that Agentic GraphRAG did not write, and any entity whose candidates it did not read. Consolidate does not compare that entity in this call. Run it again after you fix the cause.
The report lists matches that the graph already stores. Applying the same matches again is safe.
Related
- Concept: Entity resolution
- Concept: Matching and resolved entities
- Guide: Extract and resolve entities
- Reference: Ingestion API