Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

BM25
A word-matching score that ranks a document higher when it contains rare query words, with a cap on repetition.
chunk
A short passage cut from a longer document, small enough to retrieve and cite on its own.
citation
A pointer from an answer back to the exact passage it came from, so a reader can check it.
context window
The fixed amount of text a model can read in one request, counting the question and everything retrieved.
corpus
The whole collection of documents a system searches.
cosine similarity
A score from -1 to 1 for how closely two arrows point in the same direction, ignoring their lengths.
dot product
Multiply matching numbers in two lists and add the results; a larger total means closer agreement.
embedding
A list of numbers that places a piece of text in a space where similar meanings sit close together.
grounded answer
A reply built only from the passages that were retrieved, so its claims can be traced to them.
index
A structure built ahead of time so the system can find relevant passages without reading everything.
lexical search
Finding passages by the words they contain, matching the query's terms rather than its meaning.
magnitude
The length of a vector, found by squaring its numbers, adding them and taking the square root.
reranking
Scoring a short list of results again with a slower, more careful method and putting the best first.
retrieval
The step that selects passages from the corpus to put in front of the model.
top-k
The shortlist a search returns: its best few results, cut off at a number chosen in advance.
vector
An ordered list of numbers, where each position is one measured property.

RAG from the ground up

Keeping the index fresh

An index is a snapshot, and a snapshot goes stale. Incremental sync, stable ids and an atomic swap are what keep answers current without rebuilding everything every night.

Part 9 of 10

Advanced17 min readBack to Courses

An index is a snapshot

The index is a copy of the corpus at the moment it was built. Nothing about a vector store watches the filesystem, so a document edited after the last build is invisible until something re-reads it. The failure is quiet: the answer is fluent, cited and about last month’s policy, and nothing in the response says it is out of date.

Freshness is therefore a budget rather than a wish. Decide how stale an answer may be — minutes for a price list, a day for a handbook — and make the sync schedule meet that number. “We re-index when someone remembers” is not a freshness policy, it is an outage waiting for a question, and the alert should fire on index age rather than on a failed job.

The corpus is not the only thing that changes. A new embedding model, a new chunker or a new metadata field also makes the index old, and those changes are invisible to a document hash. Track the pipeline version beside the content version; without it, a deploy changes how new chunks are built while the old ones stay in the previous format.

Sync by hash, not by clock

A modification time is a weak signal. Copying a file preserves it, restoring a backup changes it without changing the content, and a deployment can touch every file at once. A hash of the text answers the only question that matters: is this document different from the one in the index? It also survives a rename, which a path-based comparison does not.

Compare the hashes and you get four sets: added, changed, deleted and unchanged. The first three need work and the fourth must be skipped, or every sync re-embeds the whole corpus and pays for it again. Deletes are the half everyone forgets, and a deleted document that still answers questions is a compliance problem, not a stale cache. Keep the manifest beside the index, so the next run diffs against what is actually stored.

An incremental upsert

from chromadb import PersistentClient

client = PersistentClient(path="./index")
collection = client.get_or_create_collection("docs")

stored = collection.get(include=["metadatas"])
stale = [
    chunk_id
    for chunk_id, meta in zip(stored["ids"], stored["metadatas"])
    if meta["doc_hash"] != hashes.get(meta["doc_id"])
]
collection.delete(ids=stale)

collection.upsert(ids=chunk_ids, documents=texts, metadatas=metadatas, embeddings=vectors)

Diff the corpus against the index

Compute the four sets, then change one hash, delete a document and add a new one. Only the changed documents should be marked for work.

Output
Click "Run" to execute your code

Stable ids make an update idempotent

A chunk id should be derived from the document id, the document hash and the position of the chunk inside it. Then re-running a sync replaces the old chunks instead of adding a second copy, and a failed run can simply be run again. Ids from a random generator turn every retry into duplication, and duplication looks like a ranking problem.

The embedding model belongs in the id scheme too, or at least in the collection name. Vectors from two models live in different spaces: a query embedded by the new model and a passage embedded by the old one are not comparable, and the similarity between them is a number without a meaning. Version the collection, not the individual vector.

Never add new-model vectors to a collection built with the old model. Re-embed the corpus into a new collection and switch to it; mixing the two produces results that look plausible and rank almost randomly.

Build beside, then flip

A rebuild that writes into the live collection is visible while it is half finished: some documents are new, some are missing, and every query in between sees a corpus that never existed. Build the new version in its own collection and point an alias at it only when the build has completed and its count matches the source.

The flip is also the rollback. If the new index is worse, point the alias back at the old collection and the service is where it was a minute ago. Monitor what the alias points at, when it was last synced and how many documents it holds; a stale index should be an alert, not a discovery made by a customer.

Schedule the rebuild before the switch is needed. Re-embedding a large corpus takes hours, and a model migration started on the day the old model is retired is an outage with a project plan. A second collection costs storage; a failed migration costs trust.

Build a second collection, then switch

Run a build that fails halfway and confirm the alias still serves the old collection. Then build again, flip the alias, and check that the new document is visible only after the flip.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does a content hash catch that a modification time does not?