Retrieval & RAG
Retrieval-augmented generation, how production LLMs ground their answers in real documents. From the parametric-vs-non-parametric knowledge distinction, through sparse retrieval (BM25), dense retrieval (embeddings + vector stores), hybrid retrieval (Reciprocal Rank Fusion), chunking strategies (fixed-size, sentence, parent-doc, contextual), cross-encoder reranking, RAG architectures (vanilla → agentic → graph-based), production patterns (caching, multi-tenant, freshness), and evaluation (recall@K, faithfulness, attribution, RAGAS). The chapter that lets the model look up what it doesn't know.
A trained LLM’s knowledge lives in its weights, call this parametric memory. It is fast, always available, and learned from pretraining. It also has three fundamental limits: the training cutoff hides recent events, your company’s private docs were never in pretraining, and specific records (a customer’s order history, a database row) are too numerous to memorize. Retrieval-augmented generation (RAG) addresses all three. Index a corpus; given a query, fetch the most relevant documents; concatenate them into the model’s context; generate a grounded answer. The model knows what is in its weights and what it can find.
This chapter walks the production RAG stack. Sparse retrieval (BM25, the classic) is still the strong baseline, especially on technical vocabulary. Dense retrieval (embeddings plus a vector store) handles semantic queries, “automobile” and “car” land in the same neighborhood of the embedding space. Hybrid retrieval combines both via Reciprocal Rank Fusion and consistently beats either alone in mature systems. Chunking, how to split documents before embedding, dominates retrieval quality more than the choice of embedding model, and most “RAG isn’t working” debugging traces back to it. Reranking with a cross-encoder turns recall-optimized first-stage retrieval into precision-optimized final results. RAG architectures range from vanilla (single retrieval) to agentic (retrieval as a tool, the Chapter 21 bridge) to graph-based (multi-hop). Production patterns, caching, multi-tenant isolation, freshness, evaluation, turn the conceptual pipeline into a reliable system.
RAG is, by a wide margin, the most-deployed LLM application pattern of this era. Every enterprise AI assistant, every documentation chatbot, every customer support agent, every legal-research tool, they all use RAG for one or more use cases. By the end of this chapter you should know how to design, evaluate, and operate a production RAG system, and which failure modes to anticipate. Then Chapter 23 closes Part VII with multimodal, extending the capability stack beyond text.
Why retrieval matters
The three limits of parametric memory
A trained transformer stores knowledge in its weights, facts, patterns, language. This parametric memory is fast and always available, but it has three fundamental limits that bite immediately in production. Training cutoff: anything that happened after pretraining is unknown to the model. Frontier labs train continuously, but every released checkpoint has a fixed cutoff date, and queries about anything after that date have nothing in the weights to retrieve. Private data: your company’s docs, the user’s records, internal databases, the chat history from last Tuesday, none of this was in pretraining, and parametric memory has no way to learn it short of fine-tuning, which is the wrong tool for facts that change daily. Specific records: a customer’s order history, a single document, a database row, the current value of a metric, these are facts the model could not memorize all of even if they had been in pretraining, because the catalog of facts of this shape is unboundedly large.
The two-stage pipeline
Retrieval-augmented generation addresses all three limits with the same two-stage pattern. Retrieve: given the user’s query, find the top- most-relevant documents from a corpus. Generate: include those documents in the model’s context and generate a grounded answer. The model’s parametric memory still does the language and reasoning work; the retrieved documents supply the specific facts.
The fundamental trade-off is the same one that shows up everywhere in systems engineering: parametric memory is fast but fixed; non-parametric memory (retrieval) is dynamic but adds latency, infrastructure, and complexity. Production systems use both: parametric for general knowledge and language fluency; retrieval for fresh, private, and specific facts.
The empirical scale of production RAG in early is consistent enough to be worth memorizing. Internal docs corpora run to documents for enterprise deployments. Top- per query lands at to documents after reranking. Total retrieved context is typically to tokens. Latency target is under seconds end-to-end (retrieval plus generation). Quality: a well-tuned RAG system reaches to answer accuracy on domain-specific QA, far above what the same model achieves without retrieval on the same questions.
Sparse retrieval: BM25
TF-IDF intuition
The classical information-retrieval baseline predates deep learning by decades and remains the strongest starting point for any new corpus. The conceptual core is TF-IDF. Term frequency (TF) measures how often a query word appears in a document, a document that mentions “quantum” ten times is more likely about quantum mechanics than one that mentions it once. Inverse document frequency (IDF) measures how rare the word is across the whole corpus, “quantum” carries more signal than “the.” Score is the sum of TF IDF over the query terms. The intuition is unchanged from : the best documents are the ones that emphasize the query’s rare words.
The BM25 refinements
BM25 (Robertson 2009) is TF-IDF with two refinements that turn out to matter a lot in practice. Document length normalization penalizes long documents so they do not win on length alone, a -word page is not necessarily more relevant than a -word page just because it has more chances to match. TF saturation caps the contribution of repeated terms, the th occurrence of “quantum” in a document matters less than the first, because the diminishing returns are real.
where is the term frequency of in document , is document length, is the average document length in the corpus, and are hyperparameters (typically , ). The formula looks ornate; the behavior is exactly the two refinements above wrapped around a TF-IDF kernel.
Why BM25 still wins
The story of BM25 in is that the old technique aged remarkably well. Exact-match prowess is a real advantage: when the user types an error code, a product SKU, a function name, or any other identifier where the precise string matters, embeddings can miss the match while BM25 lands it instantly. No training required: BM25 works on any corpus the moment it is indexed, which makes it the right baseline for prototyping and the right safety net for production. Fast and well-understood: every search engine, Elasticsearch, Lucene, Whoosh, Tantivy, implements it; the operational properties are known. Strong baseline: dense retrieval often only modestly beats BM25 on general-purpose corpora, and on domains with technical vocabulary BM25 can be the winning method outright.
Where BM25 fails is exactly where you would expect from its mechanics. Synonyms: “automobile” and “car” share no exact terms, so a query about one will not retrieve documents about the other. Paraphrases: different surface forms for the same question miss each other. Concept queries: “What causes inflation?” matches documents that mention “inflation” by name and misses documents on monetary policy that do not.
Dense retrieval: embeddings and vector stores
The setup
The dense-retrieval pattern is mechanically simple and conceptually different from BM25. An embedding model maps text to a vector in , typically with between and . Pre-compute: embed every document (or chunk) and store the vectors in a database. At query time: embed the user’s query, find the top- nearest vectors by cosine similarity, return the corresponding documents. The whole machinery rests on the empirical fact that modern embedding models pack semantically-similar text into nearby regions of the vector space.
Why dense retrieval beats BM25 on semantic queries follows from the same mechanic. Synonyms cluster: “automobile” and “car” map to nearby vectors because they appear in similar contexts during embedding-model training. Paraphrase-robust: “What causes inflation?” lands near documents on monetary policy even when the words differ. Concept queries work: the embedding captures meaning, not just surface form, so the model finds what the user meant rather than just what they typed.
Embedding models
The landscape is unhelpfully broad, so a few exemplars matter more than an exhaustive list. DPR (Karpukhin 2020, arxiv.org/abs/2004.04906) established the dual-encoder pattern for retrieval specifically: question and passage each get their own BERT tower, trained so the right passage’s vector lands near the question’s, and it was the paper that first showed dense retrieval beating BM25 on open-domain QA rather than merely matching it. Sentence-BERT (Reimers ) is the historical reference point, fine-tuned BERT, -dim, the technique that made dense retrieval practical at scale. OpenAI text-embedding-3-small () is the modern general-purpose default for managed deployments, cheap, -dim, strong across domains. all-MiniLM-L6-v2 is tiny (M params, -dim), runs locally on a CPU, and is the right choice for prototypes. E5-Mistral (Wang ) is an early LLM-based embedding model, very strong on benchmarks at the cost of inference latency. The right pick is the cheapest model that hits your recall threshold on a held-out evaluation set; “best on the leaderboard” is rarely the production answer once cost and latency are budgeted.
Vector stores and ANN indices
Vectors need to live somewhere with fast nearest-neighbor search. FAISS (Meta) is open-source, runs locally, supports IVF / HNSW / PQ indices, and is the right starting point for prototypes and self-hosted production at small to medium scale. Pinecone is the managed-service default, production-grade, autoscaling, with usage-based pricing. Weaviate, Qdrant, and Milvus are open-source managed/self-hosted alternatives. pgvector is a Postgres extension that lets you keep vectors next to the rest of your application’s relational data, the right choice when SQL filters and vector search need to compose tightly.
The index type is where the practical performance trade-offs live. Flat (brute force) is exact and slow at scale, fine up to a few hundred thousand vectors, painful above. IVF (Inverted File) clusters vectors and searches within clusters, fast and lossy in a tunable way. HNSW (Hierarchical Navigable Small World) is a graph-based index that is both fast and accurate at scale; it is the modern default for most production deployments. PQ (Product Quantization) compresses vectors aggressively, faster and smaller at the cost of recall, useful at billion-scale where storage dominates.
Hybrid retrieval
Why combine
The empirical fact that makes hybrid retrieval the production default is straightforward: BM25 and dense retrieval find different documents on most queries, and the union of their top- results contains more relevant content than either alone. Combining them often beats either alone by – points on recall@K, with the largest gains on workloads that mix keyword-heavy queries (“error code E0023”) and semantic queries (“how do I authenticate?”). Real production traffic is almost always mixed, users do not stick to one query type, so the hybrid pattern keeps the right method available for the right query.
Reciprocal Rank Fusion
The cleanest way to combine the two rankings is Reciprocal Rank Fusion (RRF), which scores each document by the sum of across all retrievers and ranks descending. The constant is typically ; the choice barely matters once it is in a reasonable range.
The reason RRF dominates the alternative, a weighted sum of normalized scores, , is mundane and decisive. BM25 scores and cosine similarities have completely different ranges and distributions; normalizing them across queries is genuinely difficult and the normalization is a constant source of bugs and tuning debt. RRF uses ranks only, sidesteps the normalization entirely, and behaves predictably across corpora. The hybrid score is well-defined and the implementation fits in eight lines of code.
Hybrid retrieval wins most when the workload is diverse. Mixed query types in the same workload, half keyword, half semantic, get the biggest lift. Mixed corpora (technical docs alongside narrative content) benefit similarly. In mature production systems hybrid is no longer a tunable choice; it is the default, with BM25 and dense both running on every query and RRF combining their results.
Chunking strategies
Why chunking matters
Documents are usually too long to embed as single vectors, a -page PDF compressed into one -dim vector loses all the structure that makes retrieval work. Chunking splits documents into smaller pieces, embeds each piece separately, and retrieves at the chunk level. The choice of how to chunk is the single biggest lever on retrieval quality after the embedding model itself, and arguably bigger than the embedding-model choice in practice. The wrong chunk size can degrade recall by – . Most of the time someone reports that “RAG isn’t working” the actual fix is in chunking, the chunks are too long and the relevant facts get diluted, or too short and they lose context, or split mid-sentence and the embedding loses coherence.
Strategies
The space of practical chunking strategies has settled into six recognizable patterns. Fixed-size chunks, for example, tokens per chunk with -token overlap, are simple and predictable, the default in most tutorials. They can split mid-sentence, which hurts embedding quality, but the operational simplicity is real. Sentence-based chunking chunks on sentence boundaries, preserves semantic units, and produces variable chunk sizes (sometimes tiny, sometimes huge). Paragraph-based chunking uses paragraph breaks as the boundary, which produces larger semantic units and works well for prose-heavy corpora.
Semantic chunking uses embeddings themselves to detect topic boundaries, adjacent sentences whose embeddings diverge mark a chunk boundary. The output is higher quality at the cost of more compute and more moving parts; it is uncommon in production and overkill for most workloads. Parent-document retrieval is the trick that gets the best of both worlds: embed small chunks (for precise retrieval) but return large parent documents (for sufficient context to the LLM). The parent is whatever surrounding window, paragraph, section, full doc, gives the model enough to work with. Contextual retrieval (Anthropic ) is a recent technique with a strong empirical track record: prepend each chunk with a short context summary before embedding, so each chunk carries metadata about its position in the document. Reported retrieval gains are in the – range on representative benchmarks.
Chunk size has a predictable trade-off curve. Small chunks ( – tokens) give precise retrieval but lose surrounding context. Medium chunks ( – tokens) are the production sweet spot for most corpora. Large chunks ( tokens) are rarely chunked at all, the embeddings get noisier as the unit grows, because a single vector cannot summarize too many topics simultaneously.
Reranking
The two-stage pattern
Production retrieval is almost always two stages. Stage 1, first-stage retrieval uses a cheap method (BM25, dense, or hybrid) to get a candidate pool of to documents. Stage 2, reranking scores each pair with a more accurate model and reorders the pool. Return top-, typically to , of the reranked pool to the LLM. The two stages have different objectives: first-stage retrieval optimizes for recall (find any relevant doc and put it in the pool); reranking optimizes for precision (put the most relevant doc first).
The reason for the split is purely a latency-quality trade-off. Cross-encoders are the most accurate relevance models, but they score one pair at a time and cannot be precomputed because the query is new every time. Running a cross-encoder against an entire corpus of M documents per query is not viable. Running it against the top- from a cheap first stage is.
Cross-encoders
A cross-encoder takes the query and the document concatenated as input, [query] [SEP] [document], and emits a single relevance score. The architecture lets the model attend across the query and document jointly, which produces a strictly more accurate relevance signal than the dual-encoder dense-retrieval pattern (which embeds query and document separately and can only compare via cosine similarity). The cost is exactly the structural cost: every pair requires a full forward pass; nothing can be cached across queries.
The structural difference is concrete enough to build without a trained model: a bi-encoder’s score is a function of two independently-computed vectors; a cross-encoder’s score is a function of the pair, and can use information that exists only once both texts are read together, negation, ordering, exact phrasing.
The rerankers worth knowing are a small set. ms-marco-MiniLM-L-6-v2 is small, fast, free, and runs locally, the right starting point. cross-encoder/ms-marco-electra-base is stronger and slower. Cohere Rerank is a managed API service and is consistently very strong on benchmarks. Voyage AI Rerank is another competitive managed option. The managed services are usually worth the latency budget once you are past the prototype stage.
Reranking matters most when the ordering of the top- has economic weight. Domains where exact relevance ordering matters, legal, medical, code search, see large lifts because the wrong-but-related document is much worse than the right one. Small top- passed to the LLM (top-) makes rank quality critical because the LLM cannot recover from a bad top- as gracefully as it can filter through a long top-. High recall, precision is the bottleneck is the pattern: the relevant doc is in the pool but not at the top, and reranking moves it. Typical recall@10 lift from reranking on diverse corpora is to points.
ColBERT: late interaction, a middle ground
Bi-encoders and cross-encoders sit at opposite ends of a cost-quality trade-off: bi-encoders precompute one vector per document and compare with a single dot product (cheap, but the query and document never interact); cross-encoders read the query and document jointly (accurate, but require a full forward pass per candidate, so nothing can be precomputed). ColBERT (Khattab 2020, arxiv.org/abs/2004.12832) is late-interaction retrieval, a middle ground that keeps most of the precomputation while recovering some of the joint-interaction quality.
Instead of one vector per document, ColBERT embeds every token of the document (and the query) separately, producing a small matrix rather than a single vector, all still precomputed and stored at indexing time, exactly like a bi-encoder. Relevance is scored with MaxSim: for each query token, find its most-similar document token, then sum those per-token maxima across the query. Because the comparison happens per-token rather than on one pooled vector, ColBERT catches fine-grained matches, a document that specifically discusses “convolution” rather than “deep learning” in general, that a single-vector embedding smooths away.
The cost is storage: a single -dim document vector becomes a matrix of one vector per token, – more index size for a typical passage. That trade-off makes ColBERT a specialist’s tool: worth it when fine-grained retrieval precision matters enough to pay for the larger index, skippable when a bi-encoder plus cross-encoder reranker already clears the quality bar more cheaply.
MMR: diversifying the final top-K
Reranking alone can still hand the LLM a top-K that is highly relevant and highly redundant, five near-duplicate chunks making the same point crowd out the one chunk that would have added new information. Maximal Marginal Relevance (MMR) fixes this by selecting the top-K greedily, trading off relevance against similarity to what has already been selected.
where is the set of documents already selected, is the document’s relevance to the query (from BM25, dense retrieval, or the reranker), and is the document’s similarity to the most-similar document already chosen. close to recovers plain top-K by relevance, ignoring redundancy entirely. close to selects almost purely for diversity, at real cost to relevance. – is the typical production range, mostly relevance-driven, with enough of a redundancy penalty to keep near-duplicates from monopolizing the top-K.
The algorithm is greedy: pick the single most relevant document first, then repeatedly pick whichever remaining document maximizes (22.mmr) , which by construction pulls each new pick away from documents already chosen.
MMR sits downstream of reranking, not instead of it: rerank first for relevance, then diversify the top-K before handing it to the LLM. Skipping it costs nothing on corpora with little redundancy. On corpora with lots of near-duplicate content, restated FAQ answers, or template policy variations, MMR is the difference between a top-K that says one thing five times and a top-K that actually covers the question.
RAG architectures
Vanilla RAG
The simplest pattern is vanilla RAG: . Single retrieval call, no iteration, no decision-making by the model about whether to retrieve. Sufficient for FAQ, docs search, and most enterprise use cases, which is a much larger fraction of production traffic than the agentic-RAG marketing implies. Vanilla RAG with good hybrid retrieval, careful chunking, and a cross-encoder reranker is the right answer for most workloads.
Agentic RAG
When the query needs multiple retrievals, research tasks, comparisons across documents, queries where the initial results suggest a refined search, agentic RAG is the pattern. The model decides when to retrieve via tool calls (the Chapter 21 bridge: retrieval becomes one of the tools in the catalog), may retrieve multiple times with different queries, and can reformulate queries based on intermediate results. The agent loop from Chapter 21 carries directly: think → retrieve → observe → think again. Retrieval is just one of the tools the agent can call, alongside calculators, web searches, and database queries. The cost is latency: each retrieval adds a round trip, and complex queries can fan out to many calls.
Specialized variants
A few variants are worth recognizing by name even if you do not deploy them today. Self-RAG (Asai ) trains the model to emit special tokens deciding when retrieval is needed and to self-critique its outputs against retrieved context, more sophisticated than vanilla, less common in production. HyDE (Hypothetical Document Embeddings) generates a hypothetical answer first, embeds that for retrieval, and improves recall on short or vague queries, a useful trick when the query itself is too sparse to retrieve well. Graph RAG builds a knowledge graph from documents and retrieves subgraphs rather than chunks; better for multi-hop reasoning (“Who is the CEO of the company that made X?”) but expensive to construct and operate, with limited production deployment as of . RETRO (Borgeaud ) bakes retrieval into the model architecture, trained from scratch with cross-attention over retrieved chunks, distinct from inference-time RAG and a separate engineering category.
RAG vs. long context
Frontier context windows grew large enough, into the hundreds of thousands of tokens and beyond as of the o1/Gemini-1.5-and-later generation, that a fair question is whether retrieval is even necessary. If the model can just read the whole corpus every time, why build a retrieval pipeline at all?
The honest answer is that long context and RAG solve different problems, and the choice is economic more than qualitative. Long context wins on quality for small, static corpora: a single contract, a codebase, a handful of papers. Feed the whole thing in and the model reasons over the complete, uncompressed document, no chunking artifacts, no missed passages, no retrieval failures to debug. For a corpus that fits comfortably in a context window and does not change between queries, long context is simply less to build and less to break.
RAG wins on cost and control everywhere else. A M-document corpus does not fit in any context window, retrieval is not optional at that scale. Dynamic content, a support-ticket queue, a live product catalog, changes constantly; re-embedding one changed chunk is cheap, re-processing a multi-hundred-thousand-token context on every query is not. And feeding K tokens of context on every request multiplies inference cost and latency compared to retrieving the K tokens that actually matter, at scale that difference is the product’s unit economics.
Xu et al. 2024 (“Retrieval meets Long Context”, arxiv.org/abs/2310.03025) made the sharpest empirical case for combining rather than choosing: retrieval augmentation with a moderate context window matched or beat brute-force long-context generation on several benchmarks at a fraction of the compute, and pairing retrieval with a larger context window (retrieve more candidates, rerank, then fill a bigger window with the reranked results) beat either extreme alone. The two techniques are less rivals than points on the same dial; production systems increasingly use both, retrieval to cut a huge corpus down to a relevant slice, a moderately long context to hold that slice without brutal chunking.
The decision guide below assumes retrieval is worth doing at all; the prior question, retrieve or just paste everything into context, is almost always answered by corpus size and update frequency, not by “long context wins now.”
The decision guide is short. FAQ or docs search: vanilla RAG. Multi-step research: agentic RAG. Multi-hop questions across many entities: graph RAG (still emerging). High-quality production at scale: vanilla RAG plus hybrid retrieval plus reranking plus careful chunking, the patterns from sections – (hybrid, chunking, reranking) plus vanilla RAG assembled into one stack.
Exercises
Four exercises that lock in the production RAG stack. Each is a self-contained problem with a starting template; hints are collapsed by default; try the problem first.
The exercises trace the chapter’s arc: implement BM25 from scratch (Ex 1) → build a vector store (Ex 2) → combine them via RRF (Ex 3) → evaluate retrieval quality (Ex 4).
Exercise 1 (easy): BM25 from scratch
Implement BM25 scoring against a small corpus. Compute IDF and TF for each query term; apply length normalization and TF saturation.
Hint
BM25 score for query against document :
where:
- = how many times appears in
- = length of in tokens
- = average document length in the corpus
- , are standard
- with = number of docs containing
Implementation steps:
- Tokenize all docs (lowercase + whitespace split)
- Build a term-frequency dictionary per doc
- Build a document-frequency dictionary across the corpus
- For each query token, compute IDF and add the per-doc contribution
Solution
The key pieces: precompute per-doc lengths and avgdl, a document-frequency count per term (each doc contributes at most once per term), then sum the per-term BM25 contribution for every query token.
import math
from collections import Counter
def tokenize(text):
return text.lower().split()
def bm25_scores(query, docs, k1=1.2, b=0.75):
tokenized_docs = [tokenize(d) for d in docs]
doc_lens = [len(d) for d in tokenized_docs]
avgdl = sum(doc_lens) / len(doc_lens)
N = len(docs)
df = Counter()
for d in tokenized_docs:
for term in set(d):
df[term] += 1
query_terms = tokenize(query)
scores = []
for d, dl in zip(tokenized_docs, doc_lens):
tf = Counter(d)
score = 0.0
for term in query_terms:
if df[term] == 0:
continue
idf = math.log((N - df[term] + 0.5) / (df[term] + 0.5) + 1)
f = tf[term]
score += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * dl / avgdl))
scores.append(score)
return scoresRunning it against the three test queries:
"machine learning"→ doc 2 (“deep learning is a subset…”) scores 1.92, doc 1 (“machine learning models…”) scores 1.69, everything else 0.00."cats"→ doc 3 (“cats are great pets”) scores 1.34 (literal match); doc 0 (“the cat sat on the mat”) scores 0.00, since BM25 does no stemming and “cat” ≠ “cats”."deep learning data"→ doc 2 scores 2.38, doc 1 scores 2.18.
Skipping terms with df[term] == 0 avoids a division by zero in the IDF log and correctly contributes nothing for out-of-vocabulary query terms.
Exercise 2 (medium): Vector store with cosine retrieval
Build a tiny in-memory vector store. Embed documents using a mock embedding function; at query time, embed the query and return the top-K most similar documents by cosine similarity.
Hint
The pattern:
- Pre-compute: embed every doc; store (id, vector) pairs.
- At query time: embed the query; compute cosine similarity to each stored vector; sort descending.
Cosine similarity:
For the exercise: use a mock embedding function, random-but-seeded so the same text always produces the same vector. (In production: use a real embedding model.)
Solution
Embed the query with the same mock embed(), compute cosine similarity against every stored vector, sort descending, and slice to top_k.
def search(self, query, top_k=3):
q_vec = self.embed(query)
results = []
for doc_id, text, vector in self.docs:
sim = np.dot(q_vec, vector) / (np.linalg.norm(q_vec) * np.linalg.norm(vector))
results.append((sim, doc_id, text))
results.sort(key=lambda x: -x[0])
return results[:top_k]With this store, "machine learning" ranks doc 2 first (sim=0.80, “deep learning is a subset of machine learning”) then doc 1 (sim=0.33), both keyword-boosted on the “machine” and “learning” axes. "cat affection" ranks doc 3 first (sim=0.28, “cats are great pets that love affection”), boosted on the “cat” axis. The keyword bias in embed() is doing the heavy lifting, exactly like a real embedding model’s learned associations would.
Exercise 3 (medium): Hybrid retrieval with RRF
Combine BM25 and dense rankings via Reciprocal Rank Fusion. Verify that the combined ranking is robust across query types, never losing badly to either method.
Hint
Reciprocal Rank Fusion:
where the sum is over retrievers, is the doc’s rank in retriever (starting at 1), and is a constant (typically 60).
Why ? It limits the influence of any single top-ranked doc. Without it, a doc ranked 1 would dominate ( vs is a 2× gap; vs is much smaller).
Steps:
- Get rankings from each retriever as
[(doc_id, rank), ...]lists. - Iterate over all rankings; accumulate RRF score per doc.
- Sort descending.
Solution
Accumulate 1 / (k + rank) per doc across every ranking that mentions it, then sort by total score descending.
def reciprocal_rank_fusion(rankings, k=60):
scores = {}
for ranking in rankings:
for doc_id, rank in ranking:
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])On the two mock rankings this produces (doc_id, score): [(0, 0.0325), (1, 0.0323), (3, 0.0318), (4, 0.0159), (5, 0.0156), (2, 0.0154), (7, 0.0154)]. Doc 0 (rank 1 in BM25, rank 2 in dense) edges out doc 1 (rank 3 in BM25, rank 1 in dense), since . Doc 3, present in both lists at middling ranks, still beats docs 4/5/7 which appear in only one ranking each: RRF rewards appearing in multiple retrievers over any single high rank.
Exercise 4 (hard): Retrieval evaluation with recall@K
Build a small evaluation harness. Given a labeled test set (query → list of relevant doc IDs), compute recall@K for a retrieval method. Compare BM25 vs dense vs hybrid; verify the chapter’s central claim, that hybrid is robust across query types.
Hint
Recall@K = fraction of relevant docs that appear in the top-K of the retriever’s ranking.
For each query in the test set:
- Run the retriever; get top-K results.
- Count how many of the K results are in the labeled relevant set.
- Divide by the total number of relevant docs.
Average across queries for the overall recall@K.
In production, this is the basic IR evaluation. RAGAS extends it with faithfulness (does the answer match retrieved content?) and attribution (can each claim be traced?).
Solution
recall_at_k intersects the top-K retrieved IDs with the relevant set; evaluate averages that across the test set.
def recall_at_k(retrieved_ids, relevant_ids, k):
top_k = retrieved_ids[:k]
hits = len(set(top_k) & set(relevant_ids))
return hits / len(relevant_ids)
def evaluate(retriever_fn, test_set, k=5):
recalls = []
for query, relevant_ids in test_set:
retrieved = retriever_fn(query)
recalls.append(recall_at_k(retrieved, relevant_ids, k))
return sum(recalls) / len(recalls)Running this over the three mock retrievers gives BM25 = 66.67%, Dense = 100.00%, Hybrid = 100.00%. The per-query breakdown shows why: BM25 and dense both hit 100% recall on “machine learning” and “cat affection”, but on “prices rising” BM25 drops to 0% (its mock ranking [0, 2, 3, 5, 6] has no lexical overlap with the inflation docs 1 and 4), while dense hits 100% (paraphrase generalization) and hybrid, built from BM25+dense via RRF, inherits dense’s catch and also lands at 100%. That is the chapter’s “hybrid is robust” claim made measurable.
Production patterns and evaluation
Production concerns
The gap between a RAG prototype and a production system is filled by four operational concerns that every mature deployment converges on. Caching appears at three layers: embedding caches for stable corpora (re-embedding is expensive), query-embedding caches for repeated queries, and full retrieval-result caches for popular queries, the cache hit rate on the popular-query tail is usually higher than first-time engineers expect. Freshness is the question of how quickly new documents become retrievable; real-time indexing is the right answer for live data (news, chat, support tickets), nightly batch is fine for stable corpora (product docs, policies). The trade-off is freshness against indexing cost, and the right answer is corpus-specific.
Multi-tenant isolation is non-negotiable for any system serving more than one customer. The standard pattern is per-tenant indices and per-tenant embedding caches, with tenant ID filters applied at retrieval time. Cross-tenant leakage in retrieval is exactly the kind of bug that ends contracts. Security generalizes the same pattern to document-level ACLs: filter retrieval results by the requesting user’s permissions, maintain an audit trail of which documents each user has retrieved, and refuse to retrieve content the user cannot otherwise see. Retrieval is a security boundary; treat it like one.
Evaluation
Production RAG that has not been measured is not done; it is just being optimistic. The discipline that turns “seems to work” into “measurably works” is a small set of metrics, used together.
| Metric | What it measures |
|---|---|
| Recall@K | Did we retrieve the relevant docs in the top ? |
| MRR | Where did the first relevant doc appear in the ranking? |
| NDCG@K | Are highly-relevant docs ranked higher than weakly-relevant ones? |
| Faithfulness | Does the generated answer match the retrieved content? |
| Attribution | Can each claim in the answer be traced to a source document? |
| Context precision | Are the retrieved chunks relevant (low noise)? |
| Context recall | Did retrieval pull in all the relevant chunks? |
RAGAS (Es ) is the standard framework for RAG evaluation in . It uses an LLM judge to score faithfulness and context precision automatically, which makes it practical to run in CI rather than only as an offline analysis. The framework’s metrics map cleanly onto the table above, and the production discipline is to gate releases on recall@K and faithfulness regressions the same way you would gate on latency or error-rate regressions elsewhere.
The common production failure modes are predictable once you know where to look. Retrieval misses: the relevant doc is not in top-, are fixed with hybrid retrieval, better chunking, and reranking. Faithfulness errors: the model hallucinates despite correct retrieved context, are fixed with prompt engineering (smaller models are noticeably worse at this; if faithfulness is the bottleneck, the model choice is a knob). Attribution gaps: the answer lacks citations, are fixed with structured output requiring per-claim source IDs. Stale data: the retrieved doc is outdated, is fixed with freshness monitoring and recency filters at retrieval time.
RAG is the most-deployed LLM pattern of – , and it is about much more than embeddings. Production RAG combines BM25 and dense retrieval (hybrid via RRF), thoughtful chunking (often the biggest single quality lever), cross-encoder reranking when precision matters, the right architecture for the use case, and a disciplined evaluation loop. Engineers who design these systems need both the conceptual model and the production discipline this chapter has assembled.
Chapter 23 closes Part VII: multimodal. Extending the capability stack beyond text, vision-language models, audio, video, and the protocols that let LLMs operate across modalities. After Part VII: Part VIII opens with safety, interpretability, and evaluation as full disciplines. Part IX assembles the capability stack into complete agent architectures. The model that knows what it can find is in place.