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
The surviving entities’ descriptions are rewritten: what this document’s chunks said about them is taken off, unless another chunk still says it (no LLM call). A summarised entity cannot have text taken out of its summary: when the removed text is no longer said by any remaining edge, it is rebuilt from its remaining
MENTIONED_IN edges if every one of them was written by this version (said_version; a table row’s edge does not count), and otherwise its summary is kept and the removed text beside it is still dropped. An unchanged chunk keeps its entities’ summaries for sure only with cache_unchanged_chunks=True; without it the chunk is extracted again, and a summary is kept only if the model describes the entity in the same words. An entity still holding more than five descriptions is summarised by the next finalize(). update() does the same for the chunks a new version replaces. Text a document ingested by an earlier SDK version wrote cannot be told apart, and is kept.
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. Relationship tables (a RelationshipMapping in the ontology) in added or modified run after all of it, modified first, because their endpoints resolve against nodes the entity tables in the same batch write. A git diff that lists consumed_batch.csv before unit.csv still writes every edge, and each entry keeps its position in the result.
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(). Callfinalize()once after the batch, not once per file. Its dedup checks only the entities whose check is pending — the ones the batch added, which have no name vector yet, and any an earlier run left pending — so one call covers the whole batch and a second call with nothing pending does no dedup work (an entity whose check did not complete stays pending and is retried).update()and deletes may run whilefinalize()deduplicates; a merge whose entities they change is skipped and decided again on the next run. - 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()— incremental: the pending entities (added since the lastfinalize(), or left pending by it) are checked against the whole graph (exact-name and acronym match, then one judge pass over their nearest names). Dedup also writes every entity’s name embedding. Then every entity holding more than five descriptions gets one LLM summary of them (descriptions_summarised); one whose summary fails keeps them and is listed indescription_summary_failures, to be tried again by the next run.embed_relationships()— embeds fact text onRELATESedges.ensure_indices()— rebuilds/verifies all indexes.
apply_changes() does not call it automatically: one finalize() after the batch judges all of the batch’s new entities together, in fewer LLM calls than one per file.
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.