Update and delete documents
Use Graph.update to replace a document that changed. Use Graph.delete_document to remove a document from search. Both work on a document key, which is the identity of a document across edits.
Before you start
-
Run Neo4j 5.23 or newer, and set
NEO4J_URI,NEO4J_USERNAMEandNEO4J_PASSWORD. The Quickstart shows how. -
Install the extras that the example uses:
uv pip install "agentic-graphrag[neo4j,extract]"
Choose a document key
Every document has a document_key. Graph.update and Graph.delete_document find a document by it.
- For a file, the key is the absolute path of the file.
- For a record, the key is the file path plus the record id or the row number.
- For
Graph.add(text=...), the key comes from a hash of the text. An edit to the text makes a new key, so you cannot update it later.
If you add text that will change, pass a Document with a key you choose.
Add, update and delete a document
This script adds a document with the key apollo-notes. It then updates the document with the same text, updates it with new text, and deletes it.
import asyncio
import hashlib
from agrag.common.data_models.document import Document, DocumentFamily, SourceFormat
from agrag.common.data_models import DEFAULT_SCHEMA
from agrag.embedding import build_embedder
from agrag.graphdb import build_graph_store
from agrag.ingestion import Graph
from agrag.ingestion.extract import GlinerExtractor
TEXT = "NASA ran the Apollo program. Neil Armstrong commanded Apollo 11."
async def main() -> None:
graph_store = build_graph_store("neo4j")
graph = await Graph.open(
schema=DEFAULT_SCHEMA,
graph_store=graph_store,
embedder=build_embedder("ibm-granite/granite-embedding-small-english-r2"),
extractor=GlinerExtractor(),
)
try:
document = Document(
text=TEXT,
title="Apollo notes",
uri="notes://apollo",
document_key="apollo-notes",
source_format=SourceFormat.TXT,
family=DocumentFamily.PROSE,
content_hash=hashlib.sha256(TEXT.encode()).hexdigest(),
loader_name="manual",
char_count=len(TEXT),
)
await graph.add(documents=[document])
same = await graph.update("apollo-notes", text=TEXT)
print("same text ->", same.no_op)
changed = await graph.update(
"apollo-notes",
text="NASA ran the Apollo program. Buzz Aldrin walked on the Moon.",
)
print("new text ->", changed.no_op, changed.parts_closed)
deleted = await graph.delete_document("apollo-notes")
print("delete ->", deleted.no_op, deleted.parts_closed)
finally:
await graph_store.close()
asyncio.run(main())
Expected output:
same text -> True
new text -> False 1
delete -> False 2
Update a document
Call Graph.update with the key and the new content. Give exactly one of text or source. A source must be a single file.
result = await graph.update("apollo-notes", text="New text")
Read the result:
no_opisTruewhen the content, chunker settings, schema structure, extraction settings and dates match the stored version, the document has an open chunk, andforceis false. Agentic GraphRAG then skips chunking, extraction and writes.parts_closedcounts theHAS_PARTlinks of the old version that Agentic GraphRAG closed. The links go to chunks, sections, tables and figures.add_resultholds the summary of the new ingestion, asGraph.addreturns it.
If the key does not exist yet, Graph.update adds the content as a new document.
Set force=True to extract entities again when the text and schema structure did not change. The update keeps the existing chunks and embeddings. If only published_at or content_date changed, Graph.update writes the dates without extracting again.
To update dates, pass a replacement source with date columns configured in ReadOptions, or a loader that returns a dated Document. A replacement without dates, including text=..., clears stored dates. A date-only update reports decision="metadata_only", parts_closed=0, and add_result=None.
Re-extract after a change of schema or settings
Agentic GraphRAG stores a schema fingerprint and an extraction fingerprint on each document and each chunk. A fingerprint is a value computed from the settings that it covers. The schema fingerprint covers the structure of the schema: entity types, parents, properties, relations, patterns, cardinality and symmetry. The schema name, the schema version and the descriptions do not change it. The extraction fingerprint covers the descriptions, the extraction settings, the extraction mode, the LLM model name and the heading path option of BAMLExtractor. The escalation budget is not part of it. See how Agentic GraphRAG decides a document is unchanged.
To apply a change of schema or settings to a stored document, open the graph with the new schema or settings. Then call Graph.update with the same key and the same text. The result depends on what changed:
- If the schema structure changed, or the chunking rules changed, Agentic GraphRAG writes a new version of the document. It chunks the text again and embeds the new chunks. The old version closes when the call commits.
- If only the descriptions, the extraction settings, the extraction mode or the LLM model name changed, Agentic GraphRAG keeps the chunks and extracts the entities again. It does not embed the chunks again.
A change of the schema name or the schema version alone does not start a new version, and it does not extract the document again. The same holds for a change of the escalation budget.
If the document was deleted, no chunk is open. Graph.update then writes a new version with the same text.
Graph.add does not compare anything. If you add a key that already exists, the graph keeps the old version open next to the new one. Use Graph.update to replace a document.
Delete a document
Call Graph.delete_document with the key.
result = await graph.delete_document("apollo-notes")
The call removes the document from search. It does not delete the document node, the chunks or the history. A key that does not exist gives no_op=True.
See what stays in the graph
After the script above, run this query. It lists the chunks of each document and whether the link to them is open.
import asyncio
from agrag.graphdb import build_graph_store
async def main() -> None:
graph_store = build_graph_store("neo4j")
await graph_store.connect()
try:
edges = await graph_store.execute_read(
"MATCH (d:Document {document_key: 'apollo-notes'})-[p:HAS_PART]->(c:Chunk) "
"RETURN d.document_key AS key, p.invalid_at IS NULL AS open, "
"count(c) AS chunks ORDER BY open"
)
for edge in edges:
print(edge["key"], "open" if edge["open"] else "closed", edge["chunks"])
finally:
await graph_store.close()
asyncio.run(main())
Expected output after the script above:
apollo-notes closed 2
Both chunks are still in the graph, but no link is open, so search does not return them.
What happens to entities:
- An entity that only the old version mentioned is removed after the update or the delete.
- An entity that the new version also mentions stays.
- The document node stays, with the hash of its last content.
A re-extraction that keeps the chunks works the same way for entities and relations. The document keeps its version, entities that only the old run produced are removed, and entities that the new run also finds stay. A relation loses this document's evidence from the old run, and it is removed when no evidence is left.
Candidates
Graph.update replaces the document's pending candidates with candidates from the new run. Graph.delete_document removes its pending candidates. Approved and rejected candidates stay. A failed update leaves the candidates unchanged.
If a call stops halfway
Graph.update and Graph.delete_document are safe to interrupt. Open the graph again and Agentic GraphRAG finishes or undoes the unfinished call. See Ingestion pipeline.
Related
- Knowledge graph explains open and closed links.
- Ingest documents shows how to add documents.
- Ingestion API lists every argument.