Chapter 22

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-KK 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.

query  retrieve  top-K docs  generate  grounded answer\text{query} \;\xrightarrow{\text{retrieve}}\; \text{top-}K\text{ docs} \;\xrightarrow{\text{generate}}\; \text{grounded answer}

(22.rag-pipeline)

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 20252025 is consistent enough to be worth memorizing. Internal docs corpora run 100K100\text{K} to 10M10\text{M} documents for enterprise deployments. Top-KK per query lands at 33 to 2020 documents after reranking. Total retrieved context is typically 5K5\text{K} to 50K50\text{K} tokens. Latency target is under 22 seconds end-to-end (retrieval plus generation). Quality: a well-tuned RAG system reaches 8080 to 95%95\% 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 ×\times IDF over the query terms. The intuition is unchanged from 19721972: 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 10,00010{,}000-word page is not necessarily more relevant than a 100100-word page just because it has more chances to match. TF saturation caps the contribution of repeated terms, the 100100th occurrence of “quantum” in a document matters less than the first, because the diminishing returns are real.

score(D,Q)=qQIDF(q)f(q,D)(k1+1)f(q,D)+k1(1b+bDavgdl)\text{score}(D, Q) = \sum_{q \in Q} \text{IDF}(q) \cdot \frac{f(q, D) \cdot (k_1 + 1)}{f(q, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})}

(22.bm25)

where f(q,D)f(q, D) is the term frequency of qq in document DD, D|D| is document length, avgdl\text{avgdl} is the average document length in the corpus, and k1,bk_1, b are hyperparameters (typically k1=1.2k_1 = 1.2, b=0.75b = 0.75). 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 20252025 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 Rd\mathbb{R}^d, typically with dd between 384384 and 15361536. 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-KK 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.

sim(q,d)=eqedeqed\text{sim}(q, d) = \frac{\mathbf{e}_q \cdot \mathbf{e}_d}{\|\mathbf{e}_q\| \cdot \|\mathbf{e}_d\|}

(22.cosine-similarity)

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 20192019) is the historical reference point, fine-tuned BERT, 768768-dim, the technique that made dense retrieval practical at scale. OpenAI text-embedding-3-small (20242024) is the modern general-purpose default for managed deployments, cheap, 15361536-dim, strong across domains. all-MiniLM-L6-v2 is tiny (2222M params, 384384-dim), runs locally on a CPU, and is the right choice for prototypes. E5-Mistral (Wang 20232023) 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-KK results contains more relevant content than either alone. Combining them often beats either alone by 551515 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 1/(k+rank)1 / (k + \text{rank}) across all retrievers and ranks descending. The constant kk is typically 6060; the choice barely matters once it is in a reasonable range.

RRF(d)=i{sparse, dense}1k+ranki(d)\text{RRF}(d) = \sum_{i \in \{\text{sparse, dense}\}} \frac{1}{k + \text{rank}_i(d)}

(22.rrf)

The reason RRF dominates the alternative, a weighted sum of normalized scores, αnorm(scoresparse(d))+(1α)norm(scoredense(d))\alpha \cdot \text{norm}(\text{score}_\text{sparse}(d)) + (1 - \alpha) \cdot \text{norm}(\text{score}_\text{dense}(d)), 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.

Retrieval comparator

Interactive
Retrieval comparator
BM25 vs Dense vs Hybrid · on a hand-curated 10-doc corpus
Pick a query:
Query:"BM25 sparse retrieval algorithm"
Correct docs are highlighted with ✓ in the rankings below.
BM25
sparse · exact match
  1. 1.BM25 sparse retrievalscore: 9.21
  2. 2.Reciprocal Rank Fusionscore: 1.55
  3. 3. Dense vector embeddingsscore: 0.00
  4. 4. How automobiles workscore: 0.00
  5. 5. Electric vehiclesscore: 0.00
Dense
embedding similarity
  1. 1.BM25 sparse retrievalsim: 0.820
  2. 2.Reciprocal Rank Fusionsim: 0.620
  3. 3. Dense vector embeddingssim: 0.550
  4. 4. Neural network trainingsim: 0.180
  5. 5. Inflation and monetary policysim: 0.060
Hybrid
RRF combination
  1. 1.BM25 sparse retrievalrrf: 0.0328
  2. 2.Reciprocal Rank Fusionrrf: 0.0323
  3. 3. Dense vector embeddingsrrf: 0.0317
  4. 4. How automobiles workrrf: 0.0308
  5. 5. Inflation and monetary policyrrf: 0.0305
Rank of first correct doc (top-5)
BM25:
★ 1
Dense:
★ 1
Hybrid:
★ 1
Insight
BM25 dominates: the query terms ("BM25", "sparse", "retrieval") appear literally in the relevant documents. Dense scores are also high (modern embedding models often catch keyword overlap too), but BM25 is the clearer win here.
Click through the four queries. BM25 wins on keyword-heavy queries; Dense wins on semantic and paraphrased queries; Hybrid (RRF) is robust across all types, never losing badly to either alone. This is why hybrid retrieval is the production default in mature RAG systems, since it handles the full diversity of real user queries.

A 10-document corpus mixing technical and conceptual content. Four query types demonstrate the operational differences: keyword-heavy (BM25 wins), semantic (Dense wins), paraphrased (Dense wins decisively), mixed (Hybrid ties the best method). Three columns side by side; correct docs are highlighted. Watch how Hybrid stays robust across all query types, never losing badly to either method. This is why hybrid retrieval is the production default in mature RAG systems.

Chunking strategies

Why chunking matters

Documents are usually too long to embed as single vectors, a 5050-page PDF compressed into one 15361536-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 303050%50\%. 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, 512512 tokens per chunk with 5050-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 20242024) 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 303050%50\% range on representative benchmarks.

Chunk size has a predictable trade-off curve. Small chunks (100100300300 tokens) give precise retrieval but lose surrounding context. Medium chunks (5005001,0001{,}000 tokens) are the production sweet spot for most corpora. Large chunks (1,500+1{,}500+ tokens) are rarely chunked at all, the embeddings get noisier as the unit grows, because a single vector cannot summarize too many topics simultaneously.

Chunking visualizer

Interactive
Chunking visualizer
Same document · four chunking strategies · color-coded boundaries
Strategy:
80-character chunks with 15-character overlap.
Document (colored by chunk)
Retrieval-augmented generation (RAG) is the most-deployed LLM application pattern in production. The core idea is simple: instead of relying solely on the model's parametric memory, retrieve relevant documents from an external corpus and pass them to the model as context. This solves three fundamental limits of pure parametric memory: training cutoffs, private data, and specific records. The retrieval step has two main flavors. Sparse retrieval uses algorithms like BM25 that match exact keywords; it remains a strong baseline that is hard to beat on domains with specialized vocabulary. Dense retrieval uses embedding models that map text into high-dimensional vectors; semantically similar texts produce nearby vectors, which lets the system match paraphrases and synonyms. Modern production systems usually combine both, often via Reciprocal Rank Fusion. Chunking is the often-overlooked decision that dominates retrieval quality. Documents are typically too long to embed as single vectors, so they must be split into pieces. The choice of chunking strategy (fixed-size with overlap, sentence-based, paragraph-based, semantic, parent-document) affects what gets retrieved more than the choice of embedding model. Wrong chunk sizes can degrade recall by thirty to fifty percent. After retrieval, a reranking stage often improves precision. Cross-encoders score each candidate document jointly with the query, producing more accurate relevance estimates than the dual-encoder embedding models used in first-stage retrieval. Reranking is slower per pair but only runs on the top candidates, so the total cost remains tractable. Production RAG systems handle more than just retrieval. They cache aggressively (re-embedding millions of documents is expensive), monitor index freshness, isolate multi-tenant data, enforce document-level access controls, and instrument every layer for evaluation. RAGAS and similar frameworks measure faithfulness (does the generated answer match the retrieved content) alongside traditional information-retrieval metrics like recall and NDCG. The architectural landscape continues to evolve. Vanilla RAG with a single retrieval call suffices for FAQ and documentation use cases. Agentic RAG, where the model decides when to retrieve via tool calls, handles multi-step research. Graph RAG attempts multi-hop reasoning over knowledge graphs. RETRO bakes retrieval into the model architecture during training. Each variant trades simplicity for capability; choose based on the actual use case.
Statistics
Chunks
39
Avg size (tokens)
20
Size range
1520
σ 0.8
Coverage
123%
+23% overlap
Insight
Simple and predictable; can split mid-sentence. The default in most RAG tutorials. Overlap fraction (here ~19%) trades chunk count for boundary context. In production, fixed-size chunks usually target 500-1000 tokens with 50-100 tokens of overlap.
Toggle between strategies to see the trade-offs. Chunk count drives retrieval precision (more chunks → more fine-grained matches). Chunk size varianceaffects embedding quality (uniform sizes embed more consistently). Overlappreserves context at boundaries but inflates index size. The chunking decision affects retrieval more than the embedding model choice and is the most common source of "RAG isn't working" debugging.

The same document re-chunked under four strategies. Fixed-size with overlap (the tutorial default; predictable but can split mid-sentence). Sentence-based (preserves semantic units; variable size). Paragraph-based (larger units; works for prose). Parent-document (embed small chunks; retrieve large parents, modern default). Watch how chunk count, size variance, and coverage change. The chunking choice often affects retrieval more than the embedding model.

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 5050 to 200200 documents. Stage 2, reranking scores each (query,candidate)(query, candidate) pair with a more accurate model and reorders the pool. Return top-KK, typically 33 to 1010, 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 (query,doc)(query, doc) pair at a time and cannot be precomputed because the query is new every time. Running a cross-encoder against an entire corpus of 11M documents per query is not viable. Running it against the top-100100 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 (query,doc)(query, doc) 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-KK 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-KK passed to the LLM (top-33) makes rank quality critical because the LLM cannot recover from a bad top-33 as gracefully as it can filter through a long top-2020. 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 55 to 1515 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 15361536-dim document vector becomes a matrix of one vector per token, 101050×50\times 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.

MMR(d)=λsim(q,d)(1λ)maxdSsim(d,d)\text{MMR}(d) = \lambda \cdot \text{sim}(q, d) - (1 - \lambda) \cdot \max_{d' \in S} \text{sim}(d, d')

(22.mmr)

where SS is the set of documents already selected, sim(q,d)\text{sim}(q, d) is the document’s relevance to the query (from BM25, dense retrieval, or the reranker), and maxdSsim(d,d)\max_{d' \in S} \text{sim}(d, d') is the document’s similarity to the most-similar document already chosen. λ\lambda close to 11 recovers plain top-K by relevance, ignoring redundancy entirely. λ\lambda close to 00 selects almost purely for diversity, at real cost to relevance. λ0.5\lambda \approx 0.50.70.7 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: queryretrieve(K)concat(query,docs)LLManswer\text{query} \to \text{retrieve}(K) \to \text{concat}(\text{query}, \text{docs}) \to \text{LLM} \to \text{answer}. 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 20232023) 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 20252025. RETRO (Borgeaud 20222022) 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 1010M-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 200200K tokens of context on every request multiplies inference cost and latency compared to retrieving the 55K 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 4466 (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 QQ against document DD:

score(D,Q)=qQIDF(q)f(q,D)(k1+1)f(q,D)+k1(1b+bDavgdl)\text{score}(D, Q) = \sum_{q \in Q} \text{IDF}(q) \cdot \frac{f(q, D) \cdot (k_1 + 1)}{f(q, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})}

where:

  • f(q,D)f(q, D) = how many times qq appears in DD
  • D|D| = length of DD in tokens
  • avgdl\text{avgdl} = average document length in the corpus
  • k1=1.2k_1 = 1.2, b=0.75b = 0.75 are standard
  • IDF(q)=log(Ndf+0.5df+0.5+1)\text{IDF}(q) = \log(\frac{N - df + 0.5}{df + 0.5} + 1) with dfdf = number of docs containing qq

Implementation steps:

  1. Tokenize all docs (lowercase + whitespace split)
  2. Build a term-frequency dictionary per doc
  3. Build a document-frequency dictionary across the corpus
  4. 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 scores

Running 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:

  1. Pre-compute: embed every doc; store (id, vector) pairs.
  2. At query time: embed the query; compute cosine similarity to each stored vector; sort descending.

Cosine similarity:

cos(a,b)=abab\cos(a, b) = \frac{a \cdot b}{\|a\| \, \|b\|}

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:

RRF(d)=i1k+ranki(d)\text{RRF}(d) = \sum_{i} \frac{1}{k + \text{rank}_i(d)}

where the sum is over retrievers, ranki(d)\text{rank}_i(d) is the doc’s rank in retriever ii (starting at 1), and kk is a constant (typically 60).

Why k=60k = 60? It limits the influence of any single top-ranked doc. Without it, a doc ranked 1 would dominate (1/11/1 vs 1/21/2 is a 2× gap; 1/611/61 vs 1/621/62 is much smaller).

Steps:

  1. Get rankings from each retriever as [(doc_id, rank), ...] lists.
  2. Iterate over all rankings; accumulate RRF score per doc.
  3. 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 1/61+1/62>1/63+1/611/61 + 1/62 > 1/63 + 1/61. 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.

recall@K=relevanttop-K retrievedrelevant\text{recall@K} = \frac{|\text{relevant} \cap \text{top-K retrieved}|}{|\text{relevant}|}

For each query in the test set:

  1. Run the retriever; get top-K results.
  2. Count how many of the K results are in the labeled relevant set.
  3. 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.

MetricWhat it measures
Recall@KDid we retrieve the relevant docs in the top KK?
MRRWhere did the first relevant doc appear in the ranking?
NDCG@KAre highly-relevant docs ranked higher than weakly-relevant ones?
FaithfulnessDoes the generated answer match the retrieved content?
AttributionCan each claim in the answer be traced to a source document?
Context precisionAre the retrieved chunks relevant (low noise)?
Context recallDid retrieval pull in all the relevant chunks?

RAGAS (Es 20232023) is the standard framework for RAG evaluation in 20252025. 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-KK, 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 2024202420252025, 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.