Vector databases from the ground up
Updates and deletes
Most vector indexes are built once and updated badly. An update is a delete and an insert, a delete is often a tombstone, and the gap between what is stored and what is live is where stale answers come from.
Why in-place update is hard
A graph index stores edges between neighbours. Change one vector and every edge that pointed to it may now be wrong. Insert one and the graph needs new edges to stay navigable. Rebuilding the affected part is possible; rebuilding the whole graph is what many engines actually do. The cost is not the update itself, it is the maintenance window the update schedules.
A partition index has the same problem with centroids (the centre points of its lists). A vector that moves should perhaps move lists, and the centroid it was assigned to was computed from the old data. The index is a summary of the corpus, and changing the corpus invalidates the summary. The summary is still useful, but it is no longer the exact thing you measured when you built it.
This is also why the update path is the part of a vector database that benchmarks rarely cover. Insert-only workloads are easy and fast. The moment a corpus changes, the index has to match its summary to the new truth, and every engine does that differently. A product that edits documents daily is running a different workload from the one the benchmark measured.
Tombstones and compaction
The usual compromise is a tombstone (a deleted marker): mark the id deleted, filter it out of results, and reclaim the space later in a compaction pass (a background clean-up). Deletes become cheap and immediate, and the index slowly fills with vectors that are stored but never returned. The search stays correct as long as the filter is applied everywhere, which is a large “as long as”.
That is a reasonable trade as long as you watch it. Compaction is a background job with a cost, and an index that is ninety percent tombstones is slow. If the filter is ever wrong, it is also a leak of deleted content. The metric to watch is simple: live vectors divided by stored vectors, and the trend over a week.
A delete with an explicit selector
from qdrant_client import models
client.delete(
collection_name="docs",
points_selector=models.PointIdsList(
points=["doc-1041", "doc-1042"],
),
wait=True,
)Delete now, reclaim later
Delete two ids and compare the live results before and after compaction. Then delete every other id and watch the gap between stored vectors and live ones grow.
The visibility problem
A delete that is acknowledged but not yet applied is a delete that will come back. In a replicated system the replica you read from may not have seen the tombstone; in a single node the search may be using a cached graph. “Deleted” is a consistency question, not a true-or-false value. The product promise should be written as a time, not as a state.
The same applies to updates. If the payload is updated but the vector is not, search returns the old meaning with the new text. Re-embedding is part of the write, not a follow-up job, or the two stores drift. A queue that re-embeds later is fine only if search excludes the row until the new vector lands.
Deleting by metadata is a scan in disguise. If a user asks to be forgotten, delete by a stable id you recorded at write time; a filter-based delete that misses one collection is a compliance incident, not a bug.
Changing the embedding model
Vectors from two models are not comparable, even when the dimensions match. Upgrading the model means re-embedding the corpus and switching the query side at the same time. That is a migration with two indexes rather than an in-place edit. The old index keeps serving until the new one has been measured on the same queries, and then it is deleted on purpose.
Version the embeddings. Write the model name and version next to every vector, query only the current version, and keep the old index until the new one has been measured. A version field costs one column and saves an outage. It also makes the rollback obvious: point the query back at the old version, which is still there because you did not delete it in the same deploy.
The migration plan fits on one page. Build the new index beside the old one, and fill in old documents in batches. Write new documents to both indexes, compare a sample of queries, switch reads, and keep the old index for a rollback window. The hard part is not the plan. It is resisting the temptation to skip the comparison because the new model scored better on a benchmark that was never your traffic.
Two embedding versions, one index
Search across all versions, then restrict to v1 and v2. Confirm that the mixed search compares vectors that were never meant to share a space.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Why is an in-place vector update hard for a graph index?
