Updating and Deleting Documents in a Vector Index
The delete path, tombstones, and why removed documents keep being retrieved.
Updating and deleting documents in a vector index means replacing or removing the vectors that retrieval scores — so answers stop citing text that no longer belongs in the corpus. An upsert overwrites a record by ID; a delete removes (or tombstones) it so search skips it. In RAG one source document usually maps to many chunk IDs, so the unit of correctness is the document’s source_id, not a single vector. Change detection that decides when to call these APIs lives on incremental indexing; this page is the store-level mutate path under indexing.
How does upsert update a vector?
Upsert writes a vector record by ID: if that ID is absent the store inserts it; if the ID already exists the store overwrites the entire record. Pinecone’s upsert guide (fetched July 2026) states the overwrite rule explicitly — partial field changes use a separate update API, not a half-upsert. The replacement vector must come from the same embedding model as the rest of the index; mixing models under one ID set skews similarity (Milvus AI Quick Reference on update/delete, 2026).
Local ANN libraries such as FAISS historically make in-place update awkward — LangChain issue #2699 (2023, still the practitioner pattern) settles on remove-then-add or a full rebuild when churn is high. Namespaces and tenants partition where the ID lives; isolation depth belongs on multi-tenancy, not here.
When should you update metadata instead of re-embedding?
Update metadata alone when the meaning of the text has not changed — publication flags, tenant tags, timestamps, routing labels. That write touches the metadata store and leaves the vector (and the ANN graph) alone. Re-embed and upsert when the body text changed, because only a new embedding moves the point in vector space.
- Metadata-only (lite). DigitalOcean’s pgvector guide updates source / updated_at without touching the embedding column; Weaviate’s object update patches properties and re-vectorizes only when a previously vectorized property changes; ShShell’s Type B update is the same idea for Pinecone/Chroma-style stores.
- Full re-embed (heavy). ShShell’s Type A upsert: regenerate the embedding, write the same ID, rebuild graph links. Weaviate documents that updating a vectorized property recalculates the embedding and reindexes the object.
- Wrong choice either way. Re-embedding because a tag flipped wastes embedding spend. Metadata-only after a rewrite leaves the old meaning searchable under a confident ID.
How do you delete vectors from a vector index?
Production deletes use one of three patterns — by ID when you know the keys, by metadata filter when you need to cascade a document or tenant, or by clearing a collection/namespace for teardown. Soft-delete flags work only if every query hard-filters them out.
| Pattern | How it works | Use when | Watch-outs |
|---|---|---|---|
| Delete by ID | Pass known vector/object IDs (Weaviate delete_by_id; Pinecone ids=[…]; Chroma ids=; AWS S3 Vectors keys; SQL DELETE … WHERE id) | You already resolved chunk IDs for a document | Pinecone documents a max of 1000 IDs per delete request (as of their July 2026 delete guide) — page large cascades |
| Delete by filter | Metadata predicate selects rows (source_id, org_id, chapter) then deletes matches (Pinecone filter; Weaviate delete_many; Chroma where) | Cascading a whole document or tenant without listing every chunk ID | Weaviate caps one delete_many at QUERY_MAXIMUM_RESULTS (default 10,000) — re-run until zero matches; use dry_run=True to count first |
| Clear collection / namespace | Drop the collection or wipe a namespace | Test resets or retiring a tenant partition | Namespace isolation model → multi-tenancy; not a substitute for per-document deletes in production sync |
What are tombstones in a vector index?
A tombstone marks a vector as deleted so the query engine skips it without immediately rewriting every ANN edge. ShShell’s updates/deletes lesson (2026) describes the usual HNSW path: flip a deleted bit → search skips that ID → a background cleaner removes the node and re-links the graph. Until compaction finishes, search can still visit tombstoned nodes — if you delete a large fraction of the index and never compact, latency rises even though those IDs never return as hits.
Not every store works that way. PostgreSQL + pgvector deletes the row and its embedding with ordinary DELETE (DigitalOcean, updated July 2026). Cloud indexes are often eventually consistent: Pinecone’s delete guide notes a slight delay before deletes are visible to queries and points at index freshness stats. Cloudflare Vectorize writes to a WAL and coalesces mutations into async index jobs — up to about 200,000 vectors or 1,000 individual updates per job (docs updated April 2026) — so thousands of one-at-a-time deletes serialize into many jobs instead of a few batched ones. Graph-construction knobs for the cleaner’s re-link live on HNSW.
How do you update a document that was split into many chunks?
A single source document usually becomes N chunk vectors with distinct IDs. Upserting only the new chunk IDs — without removing the previous chunks for that document — leaves old and new text searchable at once. That is the duplicate-version failure on stale index, not a successful update.
| Strategy | What you write | What retrieval sees | When it is correct |
|---|---|---|---|
| Upsert new chunk IDs only | Insert/overwrite the new chunks; leave prior chunk IDs untouched | Old and new chunks both rank (coin-flip answers) | Almost never for a full document rewrite |
| Delete-then-reinsert by source_id | Filter-delete every vector with that source_id, then chunk, embed, insert | Only the new revision is searchable | Default for edited documents (same pattern as incremental indexing) |
| Soft-supersede + query filter | Mark old chunks is_current=false / bump version; insert new chunks | Safe only if every query hard-filters non-current rows | When you need audit history and can enforce the filter everywhere |
Chunk-level upsert — re-embedding only the paragraphs that changed — is a later optimisation once the source_id cascade is reliable. Milvus’s document-update guidance makes the same point at store level: regenerate the embedding and replace the entry, and keep metadata versions if you need an audit trail.
What failure does a missing delete path cause?
A missing delete path causes a stale RAG index: orphan vectors for deleted sources, stale versions after edits, or duplicate versions when upserts pile on without a cascade. Retrieval has no HTTP 404 for ghosts — it returns confident citations to content operators already removed. This page is the mutate mechanics. The symptom-to-cause differential (orphan vs stale version vs duplicate) and the reconcile check live on the failure leaf.
How do you implement updates and deletes?
Every production update path needs three contracts on the store: stable source_id (and usually a content hash) on every chunk, a delete or filter-delete by that ID, and the same embedding model on every re-embed. Change detection, inventory reconcile, and when to call these APIs belong on incremental indexing. Runnable, pinned client code belongs on building the pipeline. Index structures and trade-offs sit on the indexing hub. This mechanism page stops at the store semantics those implementations must satisfy.
What is upsert in a vector index?
Upsert writes a vector record by ID: insert if the ID is new, overwrite the entire record if it already exists. Partial field changes usually need a separate update API. The new vector must come from the same embedding model as the rest of the index.
When should you update metadata instead of re-embedding?
Update metadata alone when only tags, status, tenant, or timestamps change and the text meaning is unchanged. Re-embed and upsert when the body text changed. Re-embedding for a tag flip wastes spend; metadata-only after a rewrite leaves stale meaning searchable.
Should you delete vectors by ID or by metadata filter?
Delete by ID when you already know the chunk keys — it is the most efficient path. Delete by metadata filter when you need to cascade a whole document or tenant via source_id without listing every chunk. Respect store limits: for example Pinecone documents a 1000-ID cap per delete request, and Weaviate’s delete_many defaults to a 10,000-match ceiling per call.
What is a tombstone in a vector database?
A tombstone marks a vector deleted so search skips it without immediately rewriting the ANN graph. A background job later removes the node and re-links neighbours. Until compaction runs, search may still visit tombstoned nodes — large delete fractions without cleanup raise latency even though those IDs never return as hits.
Why does a deleted document still get retrieved?
Because deletion often stopped at the source system while chunk vectors remained searchable — orphan vectors — or an update upserted new chunks without removing the old ones. Diagnose orphan vs stale vs duplicate versions on /failures/stale-index/; the store-level fix is cascade delete or tombstone by source_id.