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

A vector store

A vector store is three operations: add vectors with metadata, find the nearest ones to a query, and filter before you rank. Write one in thirty lines and the hosted ones stop being mysterious.

Part 4 of 10

Intermediate15 min readBack to Courses

Add, search, filter

Every vector database, hosted or embedded, offers the same three operations. add stores a vector with its text and metadata. search takes a query vector and returns the k nearest entries. filter restricts the candidates by metadata before or during the search.

The differences are operational: how the index is built, how it scales, how it persists, how it handles updates. The interface is small because the problem is small — everything else is engineering around speed and durability.

That is why swapping one store for another is usually a week of work rather than a rewrite. The parts that change are the client call, the index configuration and the way filters are expressed. The parts that stay are your chunks, your embeddings and the evaluation set that tells you whether the swap helped.

Exact search is a loop

Brute force is honest: compute the similarity to every stored vector and take the best k. It is exact, and it is fine for thousands of chunks — a few milliseconds in memory. Most small applications never need anything else.

Approximate indexes trade a little recall for a lot of speed at millions of vectors. The trade is invisible until it is not: an ANN index can miss the correct passage, and the answer is then wrong for a reason no prompt can fix.

The arithmetic explains the threshold. Five thousand chunks of 1,536 dimensions is about 7.7 million multiplications per query — trivial. Ten million chunks is fifteen billion, which is seconds per query. An index such as HNSW visits only part of a neighbour graph, and its search parameter decides how much to explore: raise it and recall rises with latency.

The same three operations, hosted

from langchain_chroma import Chroma

store = Chroma(
    collection_name="docs",
    embedding_function=embeddings,
    persist_directory="./chroma",
)

store.add_documents(chunks)

results = store.similarity_search(
    "How do refunds work?",
    k=4,
    filter={"product": "billing"},
)

A store in thirty lines

Add a fourth document, then search for “refund” and confirm the result carries its metadata. Change k to 1 and to 10 and watch what happens at the edges.

Output
Click "Run" to execute your code

Filter, then rank

A metadata filter is not a post-processing step. If you search for ten results and then discard the eight that belong to another tenant, you have eight fewer results than you asked for — and possibly none of the right one, because it was ranked eleventh.

Production stores apply the filter during the search so that the k results are the k best among the allowed candidates. When you write your own, filter the list before sorting, not after.

A worked case makes it concrete. Ten candidates, six of them for another product, and the one passage you need sits ninth. Ask for five and filter afterwards: it never appears. Filter first and it is in the allowed set, so it can be returned. Highly selective filters also interact with approximate indexes: some stores fall back to a full scan, which is correct and slower.

A metadata filter is not an access control. If a tenant must not see another tenant’s data, use a separate collection or namespace, not a filter that a bug can omit.

One collection per tenant

Separate collections are cheaper to reason about than filters and impossible to leak by forgetting a clause. They also make deletion honest: removing a customer means dropping a collection, not hunting rows.

Within one tenant, filters are still useful for narrowing by product, language or date. Those are relevance decisions, not security ones, and they belong in the query.

The operational cost is real: more collections mean more index builds and more configuration to keep in step. Start with one per tenant when the tenants are few and the data is sensitive, and with one collection and a tenant key when they are many. Either way, the boundary is a decision to write down rather than a clause to remember.

The store is an index, not the truth

A vector store can be rebuilt from the source documents, the chunker and the embedding model. That is its defining property: it holds a derived view, not the record. The moment it becomes the only copy of something, backups and migrations stop being an index problem and become a data-loss problem.

Treat it that way and the operational questions get easier. Re-embedding is a rebuild into a new collection, followed by a switch. A bad chunking change is a rebuild, not a repair. Deleting a document means deleting its chunks, and the source system remains the place where truth is decided.

Filter before ranking

Search returns the best two overall. Change it to return the best two that belong to “billing”, and notice that filtering after the slice would have returned only one.

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

Which three operations define a vector store?