Chapter 99 — Advanced RAG & Retrieval at Scale
📖 Definition
Retrieval-Augmented Generation (RAG) retrieves relevant information and gives it to the LLM as context. The basics (chunk → embed → search → prompt) are covered in the AI / LLM Engineering: RAG chapter. This chapter covers what senior interviews probe: why RAG fails, how to measure it, and how to scale it.
Most RAG quality problems are retrieval problems. If the right passage isn't in the context, no prompt will fix it.
🔍 The Full Production Pipeline
Offline (ingestion)
- Load sources (PDF, HTML, Confluence, tickets, DB rows) with metadata (source, owner, date, permissions).
- Parse and clean (tables, headers, OCR for scans).
- Chunk with structure awareness.
- Enrich (titles, summaries, keywords, contextual headers).
- Embed + build vector and keyword indexes.
- Keep it fresh: incremental updates, deletions, re-embedding when the model changes.
Online (query)
- Understand the query (rewrite, decompose, route).
- Retrieve from several retrievers (vector + BM25 + filters).
- Fuse and rerank.
- Assemble context within a token budget, ordered and deduplicated.
- Generate with citations; verify; refuse if evidence is missing.
- Log everything for evaluation.
📊 Chunking Strategies
| Strategy | How | Good for |
|---|---|---|
| Fixed size + overlap | e.g. 500 tokens, 50 overlap | Baseline |
| Recursive / structure-aware | Split by headings → paragraphs → sentences | Docs, wikis, markdown |
| Semantic | Split where embedding similarity between sentences drops | Long narrative text |
| Document-specific | Keep tables, code functions, Q&A pairs intact | Technical docs, FAQs |
| Parent–child (small-to-big) | Search small chunks, return the larger parent section | Precise match + enough context |
| Contextual chunk headers | Prepend "Doc title › Section › …" or an LLM-written context sentence before embedding | Chunks that are ambiguous alone ("It costs $5") |
Too small → missing context. Too large → diluted embeddings and wasted tokens. Tune with your eval set.
🔍 Hybrid Search
- Dense (vector) search understands meaning ("car" ≈ "automobile") but misses exact terms: product codes, error IDs, names.
- Sparse (BM25 / keyword) nails exact terms but misses paraphrases.
- Hybrid runs both and merges results — almost always better in enterprise data.
💻 Code Example — Reciprocal Rank Fusion (RRF)
from collections import defaultdict
def reciprocal_rank_fusion(result_lists: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
"""Each list is doc ids ordered best-first. RRF needs only ranks, not comparable scores."""
scores: dict[str, float] = defaultdict(float)
for results in result_lists:
for rank, doc_id in enumerate(results, start=1):
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
fused = reciprocal_rank_fusion([vector_hits, bm25_hits])[:50]
reranked = cross_encoder_rerank(query, fused, top_n=8) # precision stage🔍 Query-Side Techniques
| Technique | What it does | When |
|---|---|---|
| Query rewriting | Turn chat follow-ups into standalone queries ("what about its price?" → "price of X Pro plan") | Multi-turn chat |
| Multi-query | Generate several phrasings, retrieve for each, fuse | Vague queries |
| Decomposition | Split "compare A and B on cost and security" into sub-questions | Multi-hop questions |
| HyDE | LLM writes a hypothetical answer; embed that to search | Short queries vs long documents |
| Routing | Choose the index/tool (docs vs SQL vs web) by query type | Multiple sources |
| Metadata filters | Extract "2024", "India", "product=X" into filters | Structured constraints |
🔍 Context Assembly
- Rerank top 50–100 down to 5–10 with a cross-encoder (or LLM reranker).
- Deduplicate near-identical chunks; group chunks from the same document.
- Order: models attend best to the beginning and end ("lost in the middle") — put the strongest evidence first.
- Budget tokens: long context is not free (cost, latency, distraction).
- Include source IDs so the answer can cite them.
🔍 Advanced Patterns
| Pattern | Idea |
|---|---|
| Agentic RAG | The LLM decides when/what to search, iterates, and stops when it has enough |
| Corrective / self-RAG | Grade retrieved chunks; if weak, rewrite the query or use another source |
| GraphRAG | Extract entities and relations into a knowledge graph; answer global "what are the main themes" or multi-hop questions over communities/summaries |
| Structured RAG / text-to-SQL | For numbers and aggregates, query the database instead of embedding rows |
| Long-context vs RAG | If the corpus fits (e.g. 200k tokens) and prompt caching is available, stuffing may beat RAG for small static corpora; RAG wins for large, changing, permissioned data |
🔍 Vector Indexes (ANN) at Scale
Exact nearest-neighbour search over millions of vectors is too slow, so we use approximate nearest neighbour (ANN).
| Index | How | Trade-offs |
|---|---|---|
| Flat (brute force) | Compare with every vector | Exact; fine below ~100k vectors or with GPU |
| HNSW | Multi-layer graph; greedy navigation | High recall, fast, memory-hungry, slower inserts; tune M, ef_search |
| IVF | Cluster vectors (k-means); search only nprobe nearest clusters |
Less memory, needs training, recall depends on nprobe |
| PQ (product quantisation) | Compress vectors into short codes | 10–100× less memory, lower accuracy; often IVF-PQ |
| DiskANN / ScaNN | Disk-based graphs / Google's optimised ANN | Billion-scale |
Key knobs: recall@k vs latency vs memory. Always measure recall against brute force on a sample.
🔍 Permissions & Multi-Tenancy
- Store ACLs (tenant, groups, document permissions) as metadata and filter at query time — never rely on the LLM to hide data.
- Prefer pre-filtering (filter then search) for strict security; post-filtering can return too few results.
- Separate indexes/namespaces per tenant when isolation requirements are strict.
- Propagate deletions quickly (right-to-be-forgotten).
📊 Evaluating RAG
Evaluate retrieval and generation separately.
| Layer | Metric | Question it answers |
|---|---|---|
| Retrieval | Recall@k / hit rate | Is a relevant chunk in the top k? |
| Retrieval | MRR, NDCG@k | Is it ranked near the top? |
| Retrieval | Context precision | How much of the context is relevant? |
| Generation | Faithfulness / groundedness | Is every claim supported by the context? |
| Generation | Answer relevance / correctness | Does it answer the question correctly? |
| Generation | Citation accuracy | Do citations point to supporting text? |
| System | Refusal correctness | Does it say "I don't know" when evidence is missing? |
| System | Latency, cost per query | Is it viable? |
Build a golden set: real user questions + the documents that answer them + reference answers. Slice results by query type (lookup, multi-hop, numeric, follow-up).
💻 Code Example — Retrieval Metrics
import math
def recall_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
return len(set(retrieved[:k]) & relevant) / max(len(relevant), 1)
def mrr(retrieved: list[str], relevant: set[str]) -> float:
for rank, doc in enumerate(retrieved, start=1):
if doc in relevant:
return 1.0 / rank
return 0.0
def ndcg_at_k(retrieved: list[str], gains: dict[str, float], k: int) -> float:
dcg = sum(gains.get(d, 0) / math.log2(i + 2) for i, d in enumerate(retrieved[:k]))
ideal = sorted(gains.values(), reverse=True)[:k]
idcg = sum(g / math.log2(i + 2) for i, g in enumerate(ideal))
return dcg / idcg if idcg else 0.0⚠️ RAG Failure Modes & Fixes
| Symptom | Root cause | Fix |
|---|---|---|
| "I don't know" but the doc exists | Chunking split the answer; embedding misses exact terms | Structure-aware chunks, hybrid search, contextual headers |
| Confident wrong answer | Retrieved similar-but-wrong doc; model ignored context | Reranker, faithfulness check, instruct to cite and refuse |
| Outdated answer | Stale index, no recency signal | Incremental ingestion, date metadata, recency boost |
| Answers mix two products | Ambiguous chunks | Metadata filters, product-aware chunk headers |
| Slow | Too many chunks, LLM reranking everything | Smaller k, cross-encoder, caching |
| Data leak across users | No permission filtering | ACL metadata + pre-filtering |
🧠 What a Senior Answer Adds
- Separates retrieval metrics from generation metrics and fixes retrieval first.
- Designs for freshness, permissions, cost and latency, not just accuracy.
- Knows when not to use vector RAG (numeric questions → SQL; small static corpus → long context + caching).
🎯 Likely Interview Questions
- Your RAG system gives wrong answers. How do you debug it? — Check if the right chunk was retrieved (recall@k); if not fix chunking/hybrid/query rewriting; if yes fix reranking, prompt, faithfulness checks.
- Why hybrid search? — Dense handles meaning, sparse handles exact terms/IDs; fusion beats either.
- What is RRF? — Rank fusion summing 1/(k + rank) across lists; no score normalisation needed.
- Why rerank? — Cross-encoders are more accurate; apply them to a small candidate set for precision.
- How do you choose chunk size? — Based on content structure and eval results; parent–child to balance precision and context.
- What is HyDE? — Embed a hypothetical answer instead of the short question to improve matching.
- HNSW vs IVF-PQ? — Graph, high recall, memory-heavy vs clustered + compressed, memory-efficient, lower recall.
- How do you handle permissions in RAG? — ACLs as metadata, filter at retrieval time, never trust the LLM for access control.
- How do you evaluate RAG? — Retrieval (recall@k, MRR, NDCG) and generation (faithfulness, correctness, citations) on a golden set.
- RAG vs long context? — Long context for small static corpora with caching; RAG for large, changing, permissioned data and cost control.
- What is GraphRAG good for? — Multi-hop and global "summarise themes" questions over entity relationships.
- "Lost in the middle" — what is it? — Models use information at the start/end of long contexts better; order the best evidence first.
← Embeddings, Multimodal & Diffusion | Index | Next: Evaluation & Responsible AI →