A practical look at the retrieval layer of RAG — what a vector database actually does, why keyword search fails, and what it costs to get it wrong.
A few weeks ago, a health-tech client sent me a screenshot that made me stop typing. His RAG system had answered a doctor's question about a medication interaction with a dosing suggestion that was wrong — not subtly wrong, dangerously wrong. The team's first instinct was to blame the model. I asked one question: "How is your system retrieving the context?" Nobody knew. We opened the code and found it: keyword search over their knowledge base, feeding the top hits into the prompt. There was no vector database anywhere in the pipeline.
That answer explained everything. The model had done its job. It read three irrelevant chunks and produced a confident, grounded-sounding, completely wrong answer. The retrieval was broken — there was no way for it to find meaning, only matching strings.
That incident is why I am writing this. Vector databases are not a trendy accessory you bolt onto an LLM project. They are the difference between a RAG system that quotes the right source and one that confidently quotes the wrong one. This article covers what a vector database actually does under the hood, the taxonomy of options, a production architecture, real code, and the honest failure modes — including when you should not use one at all.## The Problem Keyword Search Can't Solve
To understand why vector databases exist, you have to understand the exact failure that killed my client's system. Keyword search — BM25, SQL LIKE, Elasticsearch — matches exact strings. It is brilliant at that, and terrible at meaning.
Consider two sentences:
- "The doctor recommended a lower dose for patients with impaired kidney function."
- "Renal patients should receive a reduced amount."
Keyword search sees almost no shared tokens between them — "lower" and "reduced" match, nothing else does — so it scores them as near-total strangers and drops the chunk that actually contains the answer. The retrieval layer hands the LLM the wrong documents, and the LLM makes the best of garbage.
The deeper point: in RAG the retrieval step is the source of truth the model is bound to. When the retrieved context is correct, the model will not invent facts it cannot support — but when the context is wrong, it will not know it is wrong either. Retrieval quality sets a hard ceiling on the whole system, and no amount of prompt engineering, fine-tuning, or fancier models raises it. They only make the errors sound more confident.
That is the entire argument for vector databases in one paragraph: search by meaning instead of by exact string.## How Embeddings Actually Work
A vector database stores vectors, and vectors come from embedding models. Here is the mechanism in one paragraph: an embedding model converts a piece of text into a list of numbers — usually 768 to 3,072 floats — arranged so that semantically similar texts land close together in that high-dimensional space. "The dog chased the ball" and "the canine ran after the toy" produce vectors that are nearly parallel, even though they share almost no words. Distance in that space is a proxy for difference in meaning; the famous toy example — king − man + woman ≈ queen — is the same idea in arithmetic form.
The critical practical detail is that the embedding model you use is a permanent architectural decision. You index documents with it, then you query with it. Change the model later and every vector in the store is in the wrong language for the new model — you must re-embed the entire corpus. Decide early, measure the retrieval quality on your own data, and treat the choice as locked-in until you have a hard reason to pay for a re-embed.
What a Vector Database Actually Does
Here is the part that confuses people: the vector database is not doing anything clever with meaning. The embedding model produced the meaning; the database just makes searching it fast and correct. Its job is to solve one specific problem — finding the k nearest neighbors of a query vector among millions of vectors, in milliseconds, at scale.
The Approximate Nearest Neighbor Trade-Off
Finding the exact nearest neighbor in a million-vector space is too slow for real-time retrieval. So vector databases use Approximate Nearest Neighbor (ANN) algorithms that trade a small amount of accuracy for orders of magnitude in speed. The two you will actually meet:
- HNSW (Hierarchical Navigable Small World) — a graph-based index. Fast at query time, memory-hungry (the whole graph sits in RAM), excellent for most RAG workloads. This is the default in Qdrant and the go-to choice for most production systems I have built.
- IVF (Inverted File) — clusters vectors into partitions and only searches the nearest ones. Uses less memory, slightly slower, better when the index is too large for RAM.
The practical guidance I give clients: start with HNSW — easier to tune, faster to query, and your corpus needs to be very large before memory cost becomes the thing that matters.
Distance Functions Matter
The measure of "near" is configurable, and getting it wrong silently degrades retrieval. The three you will see:
- Cosine similarity — measures the angle between vectors, ignores magnitude. The standard default for RAG; most text embeddings behave best with cosine.
- Dot product — faster, sensitive to vector magnitude. Fine if your embedding model is normalized; otherwise it rewards long vectors for the wrong reasons.
- Euclidean distance — measures raw distance. Works, but more sensitive to scale and less commonly the right default for text.
Rule of thumb: if your embedding model normalizes its output vectors to unit length, dot product and cosine give identical rankings, and dot product is cheaper. If unsure, use cosine.
Metadata Filtering Is the Hidden Requirement
Here is the requirement most tutorials skip and every production system needs: you almost never want to search the whole corpus — you want "all support tickets from the last 90 days" or "policies that apply to the EU region." A vector database that cannot filter by metadata during the ANN search forces you into a classic failure — pulling every matching vector into memory, then doing an exact search over them, which blows your latency budget on a large corpus.
The right approach is index-level filtering: store region, date, document type, tenant ID, and permissions as payload fields, and let the database apply them inside the search. This is also where multi-tenancy lives — one shared store serving ten companies with no tenant filter is an information-leak bug waiting to happen. Filter by tenant, always.
The Vector Database Taxonomy
Every week someone asks me which vector database to use. Here is the landscape as I actually reason about it.
| Option | What it is | Best for | Watch out for |
|---|---|---|---|
| Qdrant | Dedicated vector DB, Rust, HNSW-first, metadata filtering first-class | Production RAG, multi-tenant, needs fast filtered search | One more service to operate |
| pgvector | Postgres extension | Teams already on Postgres, small-to-medium corpora, simplest ops | Filtered search gets slow past a few million vectors |
| FAISS | Library, not a server (from Meta) | Offline indexing, research, embedding pipelines at scale | You build the persistence, filtering, and serving yourself |
| Managed (Pinecone, Weaviate Cloud, etc.) | Hosted vector DBs | Teams that want zero ops | Cost grows with volume and latency guarantees vary |
The honest advice: if you are already running Postgres and your corpus is under a few million chunks, pgvector is the right first move — one fewer moving part, and your transactions and vectors live in one place. The moment filtered search on a large index starts hurting, or you need predictable multi-tenant latency, move to a dedicated engine like Qdrant. FAISS is for building pipelines, not shipping apps; managed services are for teams whose scarce resource is time, not money.
A Production-Shaped Example
Let me make this concrete. Here is the smallest real architecture I would ship: embed with a good model, store in Qdrant, search with metadata filtering, feed the top chunks to an LLM. I will use the Qdrant client with an OpenAI-compatible embedding endpoint.
First, create the collection with the right distance function and a payload schema that carries metadata:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
client = QdrantClient(url="http://localhost:6333")
COLLECTION = "knowledge_base"
client.recreate_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)
Then index a batch of documents, storing the text alongside its vector so you can return the actual source, not just an ID:
import requests
def embed(text: str) -> list[float]:
r = requests.post(
"http://localhost:8000/v1/embeddings",
json={"model": "your-embedding-model", "input": text},
)
return r.json()["data"][0]["embedding"]
docs = [
{"id": 1, "tenant": "acme", "region": "EU", "text": "Kidney-impaired patients should receive a reduced dose."},
{"id": 2, "tenant": "acme", "region": "EU", "text": "Standard adult dosing for this compound is 40 mg once daily."},
]
points = [
PointStruct(
id=d["id"],
vector=embed(d["text"]),
payload={"tenant": d["tenant"], "region": d["region"], "text": d["text"]},
)
for d in docs
]
client.upsert(collection_name=COLLECTION, points=points)
Query with the tenant filter applied at the index level, not as an afterthought:
query = "What dose should be used for a patient with reduced kidney function?"
hits = client.search(
collection_name=COLLECTION,
query_vector=embed(query),
query_filter={"must": [{"key": "tenant", "match": {"value": "acme"}}]},
limit=5,
)
context = "\n\n".join(h.point.payload["text"] for h in hits)
print(context)
That query_filter line keeps tenant A's answers from leaking into tenant B's. It belongs inside the search — the reason you want a database that supports index-level filtering rather than a raw library.
The full RAG call then becomes: embed the user question, retrieve the top-k chunks with the tenant filter, join them into a context block, and send that plus the question to the LLM with an instruction to answer only from the provided context.
Production Reality: Where This Quietly Goes Wrong
I have hit every one of these in real deployments, in the order they hurt:
Chunk size and overlap are treated as defaults. Chunking determines what retrieval can even find. Chunks too large dilute the meaning per vector; too small lose the surrounding context. There is no universal answer — it depends on your documents — so measure it, don't inherit a magic number from a blog post. Start at 512–768 tokens with 10–15% overlap and benchmark.
top-k set once, never revisited. A
limit=3that worked on your demo corpus will starve a production corpus. Monitor how often the correct answer appears in the retrieved set and tune k until retrieval quality stops improving.No hybrid search, so exact terms get lost. Embeddings are bad at exact strings: part numbers, error codes, model names. A query for "ERR-1042" will return semantically similar-looking noise instead of the document that literally contains "ERR-1042". The fix is hybrid retrieval — run BM25 keyword search and vector search in parallel and merge results with a reciprocal rank fusion.
Re-ranking is skipped for cost reasons. Retrieving 50 candidates and re-ranking the top 20 with a cross-encoder beats just taking the top 5 from vector search. It costs latency and money, and on real user queries it is usually worth it — the single biggest retrieval quality upgrade I can make for a client.
Embedding drift after a model change. Upgrade the embedding model and forget the re-embed, and every query is measured against vectors in a different space. Retrieval silently degrades. Version your embeddings and re-embed before you ship.
The empty-result failure is unhandled. When retrieval returns nothing relevant, many systems still feed an empty or irrelevant context to the model, which then answers from its training data as if grounded. Enforce a minimum relevance threshold and let the system say "I don't have the information" instead of hallucinating.
When NOT to Use a Vector Database
The honest part. A vector database is not always the answer, and I have told clients so to their face:
- Your corpus is under a few thousand chunks and does not grow. Run a brute-force cosine scan in a few lines of NumPy over an in-memory list. You do not need a server for that.
- Your queries are dominated by exact terms — codes, IDs, SKUs, model numbers. A well-indexed relational table or Elasticsearch beats embeddings there. Add vector search only if you also have meaning-based queries.
-
You cannot maintain one more service. Every service you add to a small team is a pager rotation, a backup job, and a failure surface. If
pgvectorcovers your volume, do not spin up a second database just to feel modern. ## The Practitioner's Checklist
Before you trust a RAG system, walk this list:
- [ ] Retrieval is meaning-based — you have embeddings and a vector index, not keyword search alone
- [ ] The embedding model choice is deliberate and versioned; a change triggers a full re-embed
- [ ] Distance function matches your embedding model (cosine for most text models)
- [ ] Metadata filtering — tenant, region, date, doc type — happens inside the search, not after it
- [ ] Multi-tenant isolation is enforced at the index level, never by trusting the app layer
- [ ] Chunk size and overlap were tuned against your own eval set, not inherited
- [ ] top-k is monitored and tuned, not frozen at the demo value
- [ ] Exact-term queries are covered — hybrid search or a keyword fallback exists
- [ ] Retrieval is re-ranked if quality or budget allows; you measured the trade-off
- [ ] Empty retrieval is handled explicitly — the system says "I don't know" instead of hallucinating
- [ ] Retrieval quality has an evaluation set and a measured metric
The Grounded Systems Are Built on Retrieval
Back to the health-tech client. We replaced the keyword layer with a vector store, added metadata filters so each hospital's data stayed isolated, tuned chunking against their real documents, and re-ranked the top candidates. The wrong-dosage answers stopped because retrieval started returning the right source. We changed the model nowhere and almost nothing about the prompt — the fix was entirely in the retrieval layer.
That is the whole lesson, and I will repeat it every time someone blames the LLM: in RAG, the model is only as good as the context you hand it, and the context is only as good as retrieval. Build the retrieval layer well — it is the difference between sounding grounded and being grounded.
Start small: get your documents into a vector index, add the tenant filter, and benchmark before you buy a second server. The day a wrong chunk reaches a clinician, the vector database was never a convenience — it was the floor.
*Gulshan Yad
Top comments (0)