update()— re-sync a single documentdelete_document()— remove a single document and its orphansapply_changes()— heterogeneous batch (added / modified / deleted)finalize()— run once at the end of a batch
apply_changes() routes each list to the right primitive.
Mental Model — What Lives in the Graph
To understand the effect of each primitive, it helps to know the node shapes the lexical graph keeps:
Incremental updates are scoped via these relationships — orphan cleanup never goes global, it only inspects entities the touched document actually referenced.
update()
Re-sync a previously-ingested document. The new content replaces the old chunks, entities are re-extracted, and entities that become orphaned (no remaining MENTIONED_IN edges) are cleaned up along with their incident RELATES edges.
Parameters
Returns — UpdateResult
Effect on the graph
Crash safety
The call uses a six-phase state machine with one load-bearing commit point (mark_pending_committed). Crashes before the commit roll back (pending data is discarded); crashes after the commit roll forward (cleanup resumes on the next call to update/delete_document against this id).
Example
delete_document()
Remove a single document and everything it uniquely owns.
Parameters
Returns — DeleteDocumentResult
Effect on the graph
Crash safety
A single atomic write (pending_delete=true + cleanup-state arrays on the doc) is the commit marker. Before that write the live document is untouched. After it, every remaining step is idempotent and resumes on the next call against this id.
Example
apply_changes()
Heterogeneous batch — the convenience wrapper for CI-driven incremental ingestion. Dispatches each list to the right primitive in a fixed order:
deleted→delete_document()modified→update(if_missing="ingest")(so a “modified” file the graph has never seen is upserted, not errored)added→ingest()
Parameters
The order
deletes → updates → adds is part of the public contract; do not assume reordering is safe.
Overlapping ids across input lists raise ValueError at the input boundary — this is almost always a broken git-diff parser.
Returns — ApplyChangesResult
BatchEntry with error (string) and error_type (exception class name) set; the batch never raises. Branch on entry.is_success:
Effect on the graph
apply_changes() does not itself touch the graph — it dispatches to the primitives documented above and aggregates their results. Two whole-batch consequences worth knowing:
- No automatic
finalize(). Cross-document deduplication is O(graph size), so the batch deliberately leaves it to the caller. Callfinalize()once after the batch, not once per file. - Peak entity cardinality is minimised because deletes run first — orphan candidates are gone before adds can re-introduce overlapping ids.
Canonical CI usage
Concurrency and the update_concurrency trap
Raising update_concurrency above 1 is unsafe in general because the orphan-cleanup correctness proof relies on MENTIONED_IN edges being persisted before any cutover begins. Two concurrent updates sharing an entity e1 are only guaranteed to preserve it because:
- Pre-cutover,
e1still has its oldMENTIONED_INedges. - Post-
pipeline.run()(but pre-cutover), the newMENTIONED_INedges are already written.
test_concurrent_updates_preserve_shared_entity is the tripwire for this invariant — break it before bumping the value.
finalize()
Run once after a batch of ingests/updates/deletes. Skipping it leaves cross-document duplicates in place and disables entity-/edge-level vector search.
Returns — FinalizeResult
What it does (in order)
- Removes NULL-name stub entities (legacy cleanup).
deduplicate_entities()— global exact-name dedup across all documents.backfill_entity_embeddings()— embeds entity names that have no embedding yet (incremental-safe).embed_relationships()— embeds fact text onRELATESedges.ensure_indices()— rebuilds/verifies all indexes.
apply_changes() does not call it automatically: they scan the whole graph and would re-run on every file in a batch.
Choosing the Right Primitive
End-to-End Example
A full runnable example lives ingraphrag_sdk/examples/07_incremental_updates.py. It exercises:
- Initial ingest with a stable
document_id - No-op update (content-hash short-circuit)
- Real update with orphan cleanup
- Adding a second document
- A heterogeneous
apply_changes()batch - A single trailing
finalize() - Verifying with
completion()
See Also
- Ingestion — the underlying 9-step pipeline that
ingest()andupdate()both drive. - Storage — node/edge shapes, including why MERGE makes re-ingestion idempotent.
- Configuration —
finalize()reference and post-ingestion knobs. - Ontology Discovery — if you’re updating against a corpus and don’t have a schema yet, or want to propose schema additions from new documents via
suggest_schema_extensions. - Ontology Evolution — if your schema needs to change between updates (
add_entity,add_attribute,rename_*, etc.) — these are the mutation primitives that the discovery proposal hands off to.