Semantic Search & RAG
Retrieval-augmented generation (RAG) is the most common production LLM pattern: find the right evidence, put it in the prompt, and make the model answer from it with citations. Underneath the acronym it is an information-retrieval system (parsing, chunking, embeddings, inverted indexes, approximate nearest-neighbour graphs, rerankers) bolted onto a generator. Interviewers use it to see whether you can reason about the whole system: where quality is won or lost, what each index costs in memory and latency, how permissions and freshness work, and how you would measure and debug it. This page teaches each stage at the level of mechanism and trade-off.
TL;DR: the things to be able to say out loud
- RAG exists for freshness, grounding, permissions and citations. Long context competes on small corpora; fine-tuning teaches behaviour and format, not reliably new facts.
- The pipeline is ingest → parse → chunk → embed → index → retrieve → rerank → assemble → generate → cite. Most quality problems are upstream (parsing and chunking), not in the LLM.
- Chunking is a precision/context trade-off. Good defaults: structure-aware splits of a few hundred tokens with some overlap, plus small-to-big (retrieve small chunks, return parents) or contextual retrieval (prepend an LLM-written summary of where the chunk sits).
- Bi-encoder embeddings are trained contrastively (InfoNCE with in-batch negatives). Normalise them and cosine equals dot product. Matryoshka and binary/int8 quantization cut memory by large factors at small recall cost.
- BM25 still matters: exact identifiers, rare terms, codes and names. Hybrid (BM25 + dense) fused with RRF, \(\sum_r 1/(k+\text{rank}_r)\) with \(k=60\), is a strong, robust default.
- ANN indexes: HNSW (layered proximity graph, best recall/latency, RAM-hungry), IVF (k-means buckets, probe a few), PQ (compress vectors to bytes via sub-codebooks), DiskANN (graph on SSD, compressed vectors in RAM). Tune with M/ef or nlist/nprobe.
- Filtering is the hard part of vector search: post-filtering loses results when filters are selective; pre-filtering breaks graph traversal. Know the mitigations (filter-aware traversal, partitioning, iterative scans).
- Two-stage retrieval: cheap retriever for recall (top 50–200), then a cross-encoder or LLM reranker for precision (top 5–10). Rerankers are usually the highest-ROI quality fix.
- Query-side tricks (rewriting, multi-query, HyDE, decomposition, metadata-filter extraction) and agentic retrieval loops help multi-hop questions at a latency and cost price.
- Enforce ACLs inside the retrieval query (filter or partition), never by asking the LLM to ignore documents.
- Evaluate retrieval (recall@k, MRR, nDCG) and generation (faithfulness, answer relevance) separately, on a golden set. That separation is how you attribute a bad answer to retrieval or to generation.
Why RAG (and when not)
An LLM's parametric knowledge is frozen at training time, is lossy (it compresses trillions of tokens into weights), cannot be scoped per user, and cannot point to where a fact came from. RAG fixes these by moving the knowledge into an external, queryable store and conditioning generation on retrieved passages. The original formulation combined a dense retriever with a seq2seq generator and treated the retrieved document as a latent variable Lewis+ 2020. Today "RAG" usually means the simpler engineering pattern: retrieve, stuff the prompt, generate.
Freshness
Re-index a document in seconds; retraining takes weeks. Prices, policies, tickets and code change daily.
Grounding
Answering from supplied evidence reduces (does not eliminate) hallucination, and lets you detect when evidence is missing so the system can say "I don't know".
Permissions
Retrieval can be filtered per user or tenant. Weights cannot: anything in the weights is visible to everyone who can prompt the model.
Citations and audit
Every claim can link to a source span, which users and compliance teams need, and which makes evaluation possible.
RAG vs long context vs fine-tuning
Context windows of hundreds of thousands to millions of tokens make "just put everything in the prompt" viable for small corpora. Anthropic's own guidance is that a knowledge base under about 200k tokens (roughly 500 pages) can simply be included in the prompt, especially with prompt caching Anthropic 2024. Beyond that size, or when cost per query, latency, permissions or freshness matter, retrieval wins. Studies comparing the two find long context can match or beat RAG on quality when resources allow, while RAG is far cheaper, which motivates hybrids that route between them Li+ 2024. Retrieval and long context also combine well: retrieval helps even long-context models Xu+ 2023. Models also use long contexts unevenly; information in the middle of a long prompt is used less reliably than at the start or end Liu+ 2023.
Fine-tuning is the wrong tool for injecting facts. A controlled comparison found RAG consistently outperformed unsupervised fine-tuning for knowledge injection, for both existing and new knowledge Ovadia+ 2023. Fine-tune for behaviour: output format, tone, domain jargon, tool-use style, or to make a model better at using retrieved context. Fine-tuning the embedder or reranker on your domain is a different, often very worthwhile, intervention (see below).
| RAG | Long context (stuff everything) | Fine-tuning | |
|---|---|---|---|
| Corpus size | Unbounded | Up to the context window (and cost) | N/A (bakes into weights) |
| Freshness | Seconds–minutes (re-index) | Immediate (next prompt) | Retrain cycle |
| Per-user permissions | Yes, via filtering | Yes, if you assemble per user | No |
| Citations | Natural | Possible (quote spans) | No |
| Cost per query | Low (few k tokens) | High (all tokens, mitigated by caching) | Low at inference |
| Failure mode | Retrieval misses | Lost-in-the-middle, distraction, cost | Hallucinated or stale facts |
| Best for | Large, changing, permissioned corpora | Small corpora, whole-document reasoning | Style, format, task behaviour |
"Why not just use a 1M-token context?" is a favourite. A strong answer covers cost and latency (every query pays for every token unless cached), permissions (you'd still have to assemble per-user contexts), freshness, attention dilution and lost-in-the-middle, and that corpora are usually far bigger than any window. Then concede the point: for a small, static corpus, long context plus caching is simpler and often better, and a hybrid (retrieve documents, then give whole documents rather than tiny chunks) is common.
Context windows, prompt-caching prices and long-context quality changed quickly through 2025–2026, so the break-even point between RAG and stuffing the context keeps moving. Re-check current model limits and prices before quoting thresholds.
- Lewis+ 2020, Retrieval-Augmented Generation: the paper that named the pattern.
- Gao+ 2023, RAG for LLMs: A Survey: taxonomy of naive, advanced and modular RAG.
- Li+ 2024, RAG or Long-Context LLMs?: quantitative comparison and a routing hybrid.
The end-to-end pipeline
A RAG system has an offline (indexing) path and an online (query) path. Treat them as two services with a shared contract: the chunk schema (text, embedding, sparse terms, metadata, ACL, source pointer, version).
| Stage | What it does | Main knobs | Typical failure |
|---|---|---|---|
| Ingest | Pull docs from sources (Drive, Confluence, S3, DBs, tickets), track versions and deletions | Connectors, change data capture, schedule | Stale or missing docs; deletes never propagated |
| Parse | Turn bytes into clean text plus structure (headings, tables, figures) | Parser choice, OCR, layout model, VLM | Garbled tables, lost reading order, headers/footers in every chunk |
| Chunk | Split into retrievable units with metadata | Size, overlap, boundaries, parent links, context prefix | Answer split across chunks; chunk lacks the context that names its subject |
| Embed | Dense (and/or sparse) vectors per chunk | Model, dimension, quantization, instruction prefixes | Domain mismatch; query/doc prefix mismatch |
| Index | ANN index + inverted index + metadata store | HNSW M/ef, IVF nlist/nprobe, PQ | Low recall from bad params; filters return too few results |
| Retrieve | Candidate generation, top-k | k, hybrid weights, filters | Relevant chunk not in top-k |
| Rerank | Precise relevance scoring of candidates | Model, candidate count, latency budget | Added latency; reranker domain mismatch |
| Assemble | Dedup, order, trim to token budget, add source tags | Budget, ordering, parent expansion | Context overflow, duplicates, lost-in-the-middle |
| Generate | Answer conditioned on context | Prompt, model, temperature, abstain instruction | Ignoring context, blending in parametric knowledge |
| Cite | Attach source spans to claims | Inline markers, post-hoc attribution check | Citations that don't support the claim |
Think of RAG quality as a product of stage-level probabilities: P(answer correct) ≈ P(doc parsed well) × P(right chunk exists and is self-contained) × P(chunk retrieved in top-k) × P(reranker keeps it) × P(LLM uses it faithfully). A 0.9 at each of five stages is about 0.59 overall. That is why teams that only tune prompts plateau.
- Barnett+ 2024, Seven Failure Points of RAG: field report mapping failures to pipeline stages.
- Anthropic, Introducing Contextual Retrieval: a clean worked example of hybrid + rerank + chunk context with measured gains.
Document parsing
"Garbage in" is the single biggest cause of bad RAG, and parsing is where it enters. Text from HTML or Markdown is easy. PDFs are hard because a PDF is a list of positioned glyphs, not a document tree: there is no guaranteed reading order, tables are just lines and text runs, multi-column layouts interleave, and scanned PDFs contain no text at all.
| Approach | How it works | Good at | Weak at |
|---|---|---|---|
| Text-layer extraction (pdfminer, PyMuPDF-style) | Read embedded glyphs and positions, heuristically group into lines and blocks | Fast, cheap, exact characters for born-digital PDFs | Tables, multi-column order, scanned pages, equations |
| OCR (Tesseract-style, cloud OCR) | Recognise characters from page images | Scanned docs, images of text | Layout; errors on low-quality scans; no structure by itself |
| Layout-model pipelines (e.g. Docling, MinerU) | Detect regions (title, paragraph, table, figure, header/footer) with a vision model, run table-structure recognition, then reassemble reading order into Markdown/JSON | Structure-preserving output; tables as real rows/cells; drops boilerplate | Slower; GPU helps; still errs on unusual layouts |
| End-to-end OCR-to-markup models (e.g. Nougat) | Image-to-sequence transformer emits Markdown/LaTeX directly | Scientific papers, equations | Hallucination/repetition on out-of-domain pages; slow |
| General vision-language models (VLM "parse this page to Markdown") | Send the page image to a multimodal LLM with a transcription prompt | Charts, complex tables, forms, handwriting; describes figures | Cost per page; can silently paraphrase or hallucinate; needs validation |
| Skip parsing: embed page images (ColPali-style) | Retrieve directly over page screenshots with a vision retriever | Visually rich docs (slides, infographics) | Storage (many vectors per page); generator must read images |
Docling, for example, combines a layout analysis model and a table-structure model to convert PDFs into structured output Auer+ 2024; Nougat is a visual transformer that converts academic PDF pages into markup Blecher+ 2023.
Practical rules
- Tables: keep each table intact as one chunk (or row-groups with the header repeated). Serialise to Markdown or HTML; models read those well. Optionally add an LLM-written one-line summary for retrieval while returning the full table to the generator.
- Figures and charts: caption them (VLM description) so they are retrievable by text, and keep the image reference for multimodal generation.
- Boilerplate: strip repeated headers, footers, page numbers and navigation; they pollute embeddings and BM25 statistics.
- Keep provenance: page number, bounding box, section path. You need them for citations and highlighting.
- Route by document type: cheap text extraction for born-digital text, layout pipeline for complex PDFs, OCR/VLM for scans. Detect "no text layer" automatically.
- Validate: sample parsed output by eye, track characters-per-page and table-detection rates, and diff against a small hand-checked set. VLM transcription in particular should be spot-checked for paraphrase.
"Our RAG over 10-K filings gives wrong numbers" is often a parsing question in disguise. Strong candidates ask to look at the parsed chunks first, suspect table flattening (numbers detached from their row and column headers), and propose layout-aware parsing with tables kept whole and headers repeated, then a targeted eval on numeric questions.
Document parsing tooling moved fast in 2025–2026 (open layout pipelines, specialised OCR VLMs, and general multimodal models getting cheaper per page). Which tool is "best" depends on your documents; benchmark on your own pages rather than trusting a leaderboard.
- Docling (GitHub): open-source layout-aware document conversion.
- MinerU (GitHub): another open PDF-to-Markdown pipeline with layout and formula handling.
- Nougat paper: end-to-end page-image-to-markup approach and its failure modes.
Chunking strategies
A chunk is the unit you embed, index and retrieve. Chunking sets a basic tension:
- Small chunks have focused embeddings (one topic per vector, so higher precision) but lose context: "It increased 12% year over year" doesn't say what "it" is.
- Large chunks carry context but their embedding averages several topics (diluted similarity), they waste context-window tokens, and fewer fit in the prompt.
Embedding models also have maximum input lengths (often 512 tokens for older BERT-style models, up to 8k or more for newer ones), and quality on long inputs is not uniform even when allowed.
| Strategy | Mechanism | Pros | Cons / when to use |
|---|---|---|---|
| Fixed-size | Every N tokens | Trivial, predictable sizes | Cuts mid-sentence or mid-table. Baseline only. |
| Recursive character/token | Try to split on the largest separator (section, paragraph, newline, sentence, word) that keeps chunks under N | Respects natural boundaries cheaply; good default | Still structure-blind beyond separators |
| Sliding window / overlap | Consecutive chunks share e.g. 10–20% of tokens | Reduces "answer straddles boundary" misses | Index grows by the overlap fraction; near-duplicate hits |
| Structure-aware (Markdown/HTML/code) | Split on headings, sections, functions; attach heading path as metadata | Semantically coherent units; heading path gives context | Needs good parsing; sections vary wildly in size (sub-split big ones, merge tiny ones) |
| Semantic chunking | Embed sentences, start a new chunk where similarity between neighbours drops | Topic-coherent chunks without structure | Extra embedding cost; gains over recursive splitting are inconsistent in practice |
| Parent-child / small-to-big | Index small child chunks (or sentences); at query time return the parent section or a window around the hit | Precise matching and rich context | Two-level storage; more tokens per hit; must dedup parents |
| Late chunking | Run a long-context embedder over the whole document, then pool token embeddings per chunk span | Each chunk vector "sees" the whole document (resolves pronouns, references) | Needs a long-context embedder and token-level outputs; doc length limited |
| Contextual retrieval | LLM writes a short context ("This chunk is from ACME's Q2 2023 10-Q, revenue section…") prepended before embedding and BM25 indexing | Large measured recall gains; works with any embedder | One LLM call per chunk at index time (cheap with prompt caching) |
| Propositions / LLM summaries | LLM rewrites the doc into atomic facts or summaries that are indexed, pointing back to source text | Very precise matching | Index-time cost; risk of lossy or wrong rewrites |
Late chunking
Normally you chunk first, then embed each chunk independently, so a chunk's vector knows nothing about its neighbours. Late chunking reverses the order: encode the full document (up to the model's context length) once, get contextualised token embeddings, then mean-pool the token embeddings inside each chunk's span. Because attention ran over the whole document, a chunk that says "the city" carries information about which city Günther+ 2024. It costs nothing extra at index time but requires an embedder that exposes token embeddings and handles long inputs.
Contextual retrieval
Anthropic's variant uses an LLM: for each chunk, prompt the model with the whole document plus the chunk and ask for a short context to situate it, typically 50–100 tokens, which is prepended to the chunk before both embedding and BM25 indexing. With prompt caching (the document is cached across all its chunks) they report a one-time cost of about $1.02 per million document tokens. On their benchmarks, measured as 1 − recall@20, contextual embeddings cut retrieval failures by 35% (5.7% → 3.7%), adding contextual BM25 brought it to 49%, and adding a reranker to 67% (to 1.9%) Anthropic 2024. Those are the vendor's own numbers on their datasets, but the direction matches common practice: chunk-level context fixes the "orphaned chunk" problem.
Choosing sizes
There is no universal best size; it depends on document type and query type. Common starting points are a few hundred tokens (for example 256–512) with 10–20% overlap, structure-aware where possible, then tune against a golden set. Fact-lookup queries favour smaller chunks; "explain/summarise" queries favour larger units or parent expansion. Always store metadata (title, section path, date, author, doc type, ACL) alongside the chunk, since it powers filters and gives the LLM context cheaply.
Tuning chunk size by intuition and never measuring. Chunking changes are cheap to A/B on a golden set with recall@k; do that before touching the generator. Second mistake: dropping the document title and section heading from chunks. Prepending "Title > Section > Subsection" is a free version of contextual retrieval.
Expect "how would you chunk X?" for contracts, code, chat logs or tables. Good answers are structure-first: clauses or sections for contracts, functions/classes with file path and signature for code, conversation turns grouped by thread for chats, whole tables with headers. Then mention small-to-big or contextual prefixes to fix context loss, and that you would measure.
- Anthropic, Contextual Retrieval: method, prompt and ablations.
- Günther+ 2024, Late Chunking: contextual chunk embeddings from long-context encoders.
- LangChain recursive text splitter docs: the reference implementation of the recursive default.
Embeddings
Bi-encoders
A bi-encoder maps query and document independently to vectors \(\mathbf{q}, \mathbf{d} \in \mathbb{R}^D\) with a transformer encoder followed by pooling (mean of token embeddings, the [CLS] token, or the last token for decoder-based embedders). Relevance is a cheap similarity, \(s(q,d)=\mathbf{q}^\top\mathbf{d}\) or cosine. Because documents are encoded offline, query time costs one encoder forward pass plus a nearest-neighbour search. Sentence-BERT popularised siamese fine-tuning of BERT for this Reimers+ 2019, and DPR showed dense retrieval beating BM25 on open-domain QA Karpukhin+ 2020.
The cost of independence: the query and document never attend to each other, so all of a document's meaning must be squeezed into one vector before the query is known. That is why cross-encoders (joint encoding) are more accurate and why rerankers exist.
Contrastive training and InfoNCE
Embedders are trained so that a query is closer to its relevant document than to others. With a batch of \(B\) (query, positive) pairs, every other positive in the batch acts as a negative ("in-batch negatives"). The InfoNCE loss van den Oord+ 2018 for query \(i\) is:
$$\mathcal{L}_i = -\log \frac{\exp(s(q_i, d_i^+)/\tau)}{\sum_{j=1}^{B} \exp(s(q_i, d_j)/\tau)}$$This is softmax cross-entropy where the "correct class" is the positive. The temperature \(\tau\) (often around 0.01–0.05 with cosine similarity) sharpens the distribution. Key training levers:
- Batch size: more in-batch negatives make the task harder and the gradient more informative, so embedders train with very large batches (thousands to tens of thousands of pairs; gradient caching helps).
- Hard negatives: passages that look relevant but are not (mined with BM25 or an earlier model). They teach fine distinctions but risk false negatives (unlabelled positives), so teams filter them with a cross-encoder.
- Data: modern recipes do weakly-supervised contrastive pre-training on huge mined pair corpora (title–body, question–answer), then fine-tune on curated labelled sets Wang+ 2022 (E5). LLM-generated synthetic queries are now standard.
- Instruction/prefix tokens: many models expect "query: …" / "passage: …" or a task instruction. Using the wrong prefix measurably degrades retrieval.
Cosine, dot product, normalisation
Cosine similarity is \(\cos(\mathbf{q},\mathbf{d}) = \frac{\mathbf{q}^\top \mathbf{d}}{\lVert\mathbf{q}\rVert\,\lVert\mathbf{d}\rVert}\). If you L2-normalise every vector, cosine equals dot product, and squared Euclidean distance is a monotone function of it: \(\lVert\mathbf{q}-\mathbf{d}\rVert^2 = 2 - 2\,\mathbf{q}^\top\mathbf{d}\). So all three metrics give the same ranking on normalised vectors, and you can use whichever the index implements fastest (inner product usually). Use the metric the model was trained with; some models are trained with unnormalised dot product, where vector norm carries signal (for example popularity or confidence), and normalising them changes rankings.
Dimensions, Matryoshka and quantization
Typical dimensions run from 384 (small MiniLM-class models) through 768 and 1024 (base and large encoders) to 1536–4096 for API and LLM-based embedders. Memory is \(N \times D \times\) bytes per value: 10M chunks × 1024 dims × 4 bytes (float32) ≈ 41 GB of raw vectors, before index overhead. Three ways to shrink it:
- Matryoshka Representation Learning (MRL): train so that every prefix of the vector (first 64, 128, 256, … dims) is itself a good embedding, by summing the contrastive loss over several nested prefix lengths Kusupati+ 2022. You can then truncate (and re-normalise) to trade quality for memory, or do a fast coarse search with short prefixes and rescore with full vectors. Several commercial and open embedders expose a "dimensions" option built on this idea HF blog 2024.
- Scalar (int8) quantization: map each float to 8 bits using per-dimension ranges, 4× smaller.
- Binary quantization: keep only the sign of each dimension, 32× smaller, and similarity becomes Hamming distance (XOR + popcount, extremely fast). Hugging Face's experiments report binary retaining roughly 92.5% of retrieval quality, rising to about 96% when the top candidates are rescored with the float query, and int8 retaining about 99% with rescoring, with speedups of ~25× (binary) and ~3.7× (int8) on average HF blog 2024. The usual pattern: binary or int8 index for candidate generation, then rescore the top few hundred with higher-precision vectors.
Higher dimension does not automatically mean better; quality is model-dependent, and many large-dimension models truncate well thanks to MRL.
Multilingual and domain adaptation
Multilingual embedders map different languages into a shared space so that an English query can retrieve a German document. BGE-M3, for example, supports over 100 languages and produces dense, sparse and multi-vector (ColBERT-style) outputs from one model Chen+ 2024. Cross-lingual quality varies a lot by language pair, so test low-resource languages explicitly.
Domain fine-tuning of the embedder is one of the best levers for specialised corpora (legal, medical, internal jargon, code). Recipe: generate synthetic queries per chunk with an LLM (or mine real query logs and click data), mine hard negatives with the current retriever, filter false negatives with a cross-encoder, fine-tune with InfoNCE (MultipleNegativesRanking-style loss), and evaluate on a held-out golden set. A few thousand to tens of thousands of pairs often give clear recall gains. Remember that changing the embedder means re-embedding the whole corpus; version your embeddings and keep the model ID with the index.
MTEB and its caveats
The Massive Text Embedding Benchmark scores models across task families (retrieval, reranking, clustering, classification, STS, and more) Muennighoff+ 2022; MMTEB extended it to hundreds of tasks and many languages Enevoldsen+ 2025. Caveats an interviewer likes to hear:
- The average score mixes tasks; for RAG look at the retrieval column (nDCG@10 on BEIR-style sets Thakur+ 2021).
- Contamination and overfitting: public test sets leak into training data, so leaderboard gaps of a point or two are often noise relative to your domain.
- Leaderboards rarely reflect your constraints: latency, model size, max input length, licence, cost per million tokens, languages, and whether you can self-host.
- The only number that matters is recall@k on your golden set. Shortlist 3–5 models from the leaderboard, then evaluate.
| Model family (examples) | Type | Notes |
|---|---|---|
| E5, BGE, GTE, Arctic-Embed, Nomic, MiniLM | Open encoder-style | Small to large; self-hostable; good baselines Merrick+ 2024 |
| BGE-M3 | Open, multilingual | Dense + sparse + multi-vector from one model |
| Qwen3-Embedding (0.6B–8B) and Qwen3-Reranker | Open, LLM-based | Topped the MTEB multilingual leaderboard at release (mid-2025) Zhang+ 2025 |
| EmbeddingGemma | Open, small (~300M) | On-device class, Matryoshka dims Vera+ 2025 |
| Gemini Embedding, OpenAI text-embedding-3, Voyage, Cohere Embed | Commercial APIs | Strong general quality; MRL-style dimension control on several; data leaves your boundary Lee+ 2025 |
Embedding leaderboards turn over every few months. As of late 2026, LLM-based embedders (multi-billion-parameter models fine-tuned for embedding) dominate the top of MTEB, while small models remain the practical default for latency-sensitive self-hosting. Check the MTEB leaderboard for current names, and treat third-party "best of 2026" lists with caution.
Common probes: "Why do embeddings fail on part numbers or names?" (tokenisation splits rare strings, and the model was trained for semantic similarity, not exact match; that's what BM25 is for). "How would you cut vector memory 10×?" (MRL truncation plus int8, or binary with rescoring, or PQ). "How do you change embedding models without downtime?" (dual-write a new index, backfill, shadow-evaluate, switch reads, then delete the old index; query and corpus must use the same model).
- Karpukhin+ 2020, DPR: the canonical dense retriever with in-batch negatives.
- Kusupati+ 2022, Matryoshka Representation Learning.
- Hugging Face, Embedding Quantization: binary/int8 with rescoring, with numbers.
- MTEB paper: what the benchmark measures, and therefore what it doesn't.
Sparse retrieval: TF-IDF, BM25, SPLADE
Sparse retrieval represents text as a vector over the vocabulary, almost all zeros, and serves it from an inverted index: for every term, a postings list of (doc id, term frequency). A query touches only the postings lists of its terms, so search over billions of documents is fast, and algorithms like WAND and block-max WAND skip documents that cannot make the top-k.
TF-IDF
Weight a term by how often it appears in the document (term frequency) times how rare it is in the corpus: \(\text{idf}(t) = \log \frac{N}{n_t}\), where \(N\) is the number of documents and \(n_t\) the number containing \(t\). Rare terms ("pgvector", "ERR_4012") are discriminative; common ones ("the", "system") are not. Raw TF grows without bound and long documents get an unfair advantage, which BM25 fixes.
BM25
BM25 (Okapi BM25) scores a document \(D\) for query \(Q = \{q_1,\dots,q_n\}\) as Wikipedia:
$$\text{score}(D,Q) = \sum_{i=1}^{n} \text{IDF}(q_i)\cdot \frac{f(q_i,D)\,(k_1+1)}{f(q_i,D) + k_1\left(1 - b + b\,\frac{|D|}{\text{avgdl}}\right)}$$with \(\text{IDF}(q_i) = \ln\!\left(\frac{N - n(q_i) + 0.5}{n(q_i)+0.5} + 1\right)\) in the Lucene variant. What the parameters do Elastic blog:
- \(k_1\) (term-frequency saturation, typically 1.2–2.0, Lucene default 1.2): the TF term approaches \(k_1+1\) asymptotically. The 1st occurrence of a term matters a lot, the 10th barely. \(k_1=0\) means binary presence; large \(k_1\) behaves more like raw TF.
- \(b\) (length normalisation, 0–1, default 0.75): how much to penalise documents longer than average. \(b=0\) ignores length; \(b=1\) fully normalises. Lower \(b\) for corpora where long docs really are more relevant.
BM25 is a strong zero-shot baseline. On the BEIR benchmark, many early dense retrievers trained on MS MARCO underperformed BM25 out of domain Thakur+ 2021, which is a big reason hybrid search became standard. BM25 shines on exact identifiers, error codes, names, acronyms and rare jargon; it fails on paraphrase and synonyms ("car" vs "automobile") and needs language-specific analysis (tokenisation, stemming, stop words).
Learned sparse retrieval: SPLADE
SPLADE uses a masked-language-model head to predict, for each input token, a weight over the whole vocabulary, then aggregates (log-saturated, max-pooled) into one sparse vector per text, with a sparsity regulariser (FLOPS loss) so most weights are zero Formal+ 2021 Formal+ 2021b. Two effects: term weighting (learned importance instead of TF-IDF) and expansion (a document about "automobiles" also gets weight on "car"). The output still lives in an inverted index, so you keep exact-match behaviour and interpretability while gaining some semantic matching. Costs: a transformer pass per document and query, and expanded documents have longer postings than BM25, so query latency is higher.
- Elastic, Practical BM25 part 2: intuition for k1 and b with plots.
- SPLADE v2: learned sparse retrieval with expansion.
- BEIR: why zero-shot evaluation exposed dense retrievers' domain brittleness.
Late interaction: ColBERT and ColPali
Between bi-encoders (one vector per text, no interaction) and cross-encoders (full joint attention, no precomputation) sits late interaction. ColBERT encodes the query and document independently into one vector per token, then scores with MaxSim Khattab+ 2020:
$$s(q,d) = \sum_{i \in q} \max_{j \in d} \; \mathbf{q}_i^\top \mathbf{d}_j$$Each query token finds its best-matching document token, and those best matches are summed. Document token vectors are precomputed, so it is much cheaper than a cross-encoder, yet token-level matching captures fine-grained relevance and generalises out of domain better than single-vector models. The price is storage: one vector per token instead of per chunk, tens to hundreds of times more vectors. ColBERTv2 attacks this with residual compression (centroid id + quantised residual per token) and denoised supervision, shrinking the index several-fold Santhanam+ 2021. In practice ColBERT-style models are used either as a first-stage retriever (with a specialised engine such as PLAID) or as a reranker over candidates from a cheaper retriever.
ColPali applies the same idea to documents as images: a vision-language model (PaliGemma) embeds a page screenshot into many patch-level vectors, and text queries are matched with MaxSim against those patches Faysse+ 2024. It skips OCR and layout parsing entirely and handles charts, tables and slides naturally; the paper also introduced the ViDoRe benchmark for visually rich retrieval. Trade-offs: on the order of ~1,000 vectors per page, a VLM forward pass per page at indexing, and the generator then needs to read page images (a multimodal LLM), not extracted text.
| Bi-encoder | Late interaction (ColBERT) | Cross-encoder | |
|---|---|---|---|
| Doc precomputation | 1 vector | 1 vector per token | None |
| Query-time cost | 1 encode + ANN | 1 encode + MaxSim over candidates | 1 forward pass per (q, d) pair |
| Storage | Small | Large (compressible) | None |
| Accuracy | Good | Better, robust out of domain | Best |
| Role | First-stage retrieval | First stage or reranker | Reranker only |
- Khattab+ 2020, ColBERT.
- ColPali explained (Hugging Face blog): visual document retrieval walkthrough.
- ColBERT repository: reference implementation including PLAID.
Approximate nearest-neighbour indexes
Why approximate: the cost of exact kNN
Exact (flat) search computes the distance from the query to every vector: \(O(ND)\) per query. With \(N = 10^7\) and \(D = 768\) that is about \(7.7\times 10^9\) multiply-adds and, more importantly, reading about 31 GB of float32 vectors from memory. At ~100 GB/s of memory bandwidth that's roughly 0.3 s per query on one machine, memory-bandwidth-bound. Flat search is fine (and gives perfect recall) up to roughly a few hundred thousand to a million vectors, on a GPU, or after a selective filter has shrunk the candidate set. Beyond that you trade a little recall for orders-of-magnitude speed with an ANN index. ANN quality is measured as recall@k against exact search: the fraction of the true top-k the index returns.
HNSW: Hierarchical Navigable Small World graphs
HNSW builds a proximity graph where each vector links to nearby vectors, arranged in layers like a skip list Malkov+ 2016:
- Each inserted node draws a maximum layer \(\ell = \lfloor -\ln(U)\cdot m_L \rfloor\) with \(U\sim\text{Uniform}(0,1)\) and \(m_L = 1/\ln M\). So layer 0 has every node, layer 1 roughly \(1/M\) of them, and so on: few nodes and long-range links at the top, all nodes and short links at the bottom.
- Search: start at the entry point on the top layer, greedily move to the neighbour closest to the query until no improvement, drop down a layer, repeat. On layer 0 run a best-first beam search keeping a candidate list of size
efSearch, and return the best k. Upper layers do coarse "zoom-in", layer 0 does the fine search. Hop count grows roughly logarithmically with \(N\). - Insertion: search for the new node's neighbours with a beam of
efConstruction, connect to up to \(M\) of them (\(2M\) on layer 0) using a diversity heuristic that prefers neighbours in different directions over a cluster of near-duplicates, and prune neighbours' lists that overflow.
| Parameter | Effect | Typical values |
|---|---|---|
M | Max links per node (2M on layer 0). Higher: better recall, especially at high dimension, more memory, slower build | 8–64; 16 is a common default (pgvector default 16) |
efConstruction | Beam width during insertion. Higher: better graph quality, slower build. No query-time cost | 64–512 (pgvector default 64) |
efSearch | Beam width at query time, must be ≥ k. The main recall/latency dial, tunable per query | 40–500 (pgvector default 40) |
Memory: full vectors plus the graph. Layer-0 links cost about \(2M \times 4\) bytes per node (32-bit ids), so 128 bytes at M=16, plus a small amount for upper layers. For 768-dim float32 vectors (3,072 bytes) the graph adds only a few percent, but the whole thing should live in RAM, because traversal is random access. That is HNSW's main cost: 100M × 768-dim float32 is about 300 GB before overhead. Mitigations: int8/binary/PQ-compressed vectors inside the graph with rescoring, or a disk-based index.
Deletions and updates: removing a node can disconnect the graph, so most implementations use tombstones (mark deleted, skip at query time, still traverse through) and repair or rebuild later. Heavy churn degrades recall and wastes memory until compaction or rebuild. Build is also expensive: each insert is a search, so building 100M vectors takes hours of many-core CPU. Plan index rebuilds as a first-class operation.
IVF: inverted file with a coarse quantizer
IVF clusters the vectors with k-means into nlist centroids (the coarse quantizer) and stores each vector in the list of its nearest centroid. At query time, find the nprobe nearest centroids and scan only those lists. With \(N\) vectors and \(nlist\) lists, a query scans about \(N \cdot nprobe/nlist\) vectors plus \(nlist\) centroid comparisons.
- nlist: Faiss guidance suggests on the order of \(4\sqrt{N}\) to \(16\sqrt{N}\) clusters Faiss wiki; pgvector suggests rows/1000 up to 1M rows and \(\sqrt{\text{rows}}\) above that, with probes starting at \(\sqrt{\text{lists}}\) pgvector README.
- nprobe: the recall/latency dial. Recall drops for queries near cluster boundaries, since their true neighbours sit in adjacent lists.
- Training: k-means needs a representative sample (Faiss suggests 30–256 training vectors per centroid). If the data distribution drifts (new topics), lists become unbalanced and recall degrades, so retrain periodically.
- IVF uses less memory than HNSW and builds faster, but has a worse recall/latency curve at the same memory unless combined with compression or an HNSW over the centroids.
Product quantization (PQ)
PQ compresses each vector into a few bytes. Split the \(D\)-dim vector into \(m\) subvectors of \(D/m\) dims; for each subspace, learn a codebook of 256 centroids with k-means; store each subvector as the 1-byte id of its nearest centroid. A 768-dim float32 vector (3,072 bytes) with \(m=96\) becomes 96 bytes, 32× smaller. The effective codebook is \(256^m\) points, a huge implicit vocabulary from small tables.
Asymmetric distance computation (ADC): the query is kept exact. Before scanning, precompute a table of distances from each query subvector to all 256 centroids in its subspace (\(m \times 256\) entries). The distance to any database code is then \(m\) table lookups and adds, \(\hat d(q,x)^2 = \sum_{j=1}^{m} T_j[c_j(x)]\), with no decompression. That fits in cache and vectorises well (Faiss's "fast scan" variants use 4-bit codes and SIMD registers) Douze+ 2024. PQ distances are approximate, so systems re-rank the top candidates with full-precision vectors if available.
IVF-PQ
Combine both: IVF picks which lists to scan; within a list each vector is PQ-encoded, usually on the residual (vector minus its centroid), which has a smaller range and so quantises more accurately. This is the classic billion-scale recipe: a billion vectors at 64 bytes each is about 64 GB, which fits on one large machine or GPU Johnson+ 2017.
ScaNN
Google's ScaNN observed that for maximum-inner-product search, not all quantization error is equal: error parallel to the datapoint changes inner products with high-scoring queries much more than orthogonal error. Its anisotropic vector quantization loss weights the parallel component more heavily, improving recall at the same code size Guo+ 2020. ScaNN pairs this with partitioning (tree/IVF-style) and exact rescoring, and it ranked near the top of ann-benchmarks at publication. It is available as an open-source library and inside Google Cloud products (AlloyDB, Vertex AI Vector Search).
DiskANN and Vamana: SSD-scale
When vectors don't fit in RAM, HNSW's random access to full vectors is ruinous on disk. DiskANN builds a flat (single-layer) graph with the Vamana algorithm, whose pruning rule with a parameter \(\alpha > 1\) deliberately keeps some long-range edges so the graph has a small diameter, meaning few hops and therefore few SSD reads Subramanya+ 2019. Layout: full-precision vectors and adjacency lists live together on SSD (one read fetches both), while PQ-compressed vectors live in RAM to guide the beam search. Each hop issues a batch of parallel SSD reads; final candidates are re-ranked with full vectors read from disk. The paper reports serving a billion points on a single machine with 64 GB RAM at 95%+ 1-recall@1 and a few milliseconds latency. FreshDiskANN adds streaming inserts and deletes Singh+ 2021. DiskANN-style indexes now appear in several databases (for example Milvus, Azure services, and pgvector-compatible extensions).
Filtered search: the hard part
Real queries carry filters: tenant = X, date > 2025, doc_type = contract, user can read. Combining filters with ANN is where many production systems struggle.
- Post-filtering: run ANN for top-k′, then drop rows that fail the filter. Simple, but if the filter passes only 1% of rows, a top-100 ANN returns about one valid result. pgvector's docs give this exact example: with a filter matching 10% of rows and the default
ef_searchof 40, only about 4 rows match on average; version 0.8.0 added iterative index scans that keep scanning until enough results pass pgvector README. - Pre-filtering: compute the allowed id set first (from a B-tree or bitmap), then search only among those. Exact for brute force over small sets, but inside a graph index it breaks navigation: if most nodes are disallowed, the allowed ones may be disconnected from the entry point.
- Filter-aware traversal: traverse through disallowed nodes but only return allowed ones, and/or add extra edges so filtered subgraphs stay connected. Qdrant's filterable HNSW and ACORN (predicate-agnostic, denser graph with two-hop expansion) are examples Qdrant Patel+ 2024.
- Query planning by selectivity: very selective filter (a few thousand rows) → brute-force over the filtered set; permissive filter → ANN with filter-aware traversal. Good engines estimate selectivity and switch automatically.
- Partitioning: for high-cardinality, always-present filters (tenant id), build separate indexes or namespaces per value. Best isolation and performance, more operational overhead for many small tenants.
Recall–latency–memory trade-offs
| Index | Memory per vector (768-d) | Recall potential | Query latency | Build / updates | Sweet spot |
|---|---|---|---|---|---|
| Flat (exact) | 3 KB (float32) | 100% | Linear in N; ms at 100k, ~0.1–1 s at 10M (CPU) | Trivial / trivial | Small corpora, heavily filtered subsets, ground truth for evals |
| HNSW (float) | 3 KB + ~100–300 B graph | Very high (95–99%+) | ~1 ms-class at millions | Slow build; deletes via tombstones | Default for up to ~10s of millions in RAM |
| HNSW + int8/binary + rescore | 0.1–0.8 KB + graph | High with rescoring | Faster than float | As HNSW | Same, at lower RAM cost |
| IVF-Flat | 3 KB | High with enough nprobe | ms to tens of ms | Needs k-means training; easy appends | Moderate scale, cheaper build than HNSW |
| IVF-PQ | ~16–128 B | Moderate; high with rerank | ms; very high throughput, GPU-friendly | Training; easy appends | Hundreds of millions to billions, memory-bound |
| DiskANN / Vamana | ~32–128 B in RAM, full on SSD | Very high | Few ms (SSD reads) | Heavy build; Fresh variants handle streaming | Billion-scale on one node, cost-sensitive |
| ScaNN | Quantized codes + optional full | Very high for MIPS | Very fast on CPU | Training | Large-scale inner-product search |
All latency and recall figures above are rough orders of magnitude; they depend heavily on dimension, hardware, k and parameter settings.
Benchmark on your own data with ann-benchmarks-style sweeps: plot recall@10 vs QPS for each parameter setting and pick the knee ann-benchmarks.
"Design vector search for 1B vectors" is a classic. Do the arithmetic out loud: 1B × 768 × 4 B ≈ 3 TB raw, so float HNSW in RAM means a large sharded cluster. Options: PQ to 64–96 bytes (≈ 64–96 GB, single node or GPU) with full-precision rerank from SSD; DiskANN with compressed vectors in RAM; or MRL truncation plus binary for candidates. Then discuss sharding (by tenant or hash, scatter-gather and merge top-k), replication for QPS, filtering strategy, and how you measure recall against a flat-search sample.
Reporting "the vector DB is slow" without checking parameters. A too-small efSearch or nprobe gives fast but low-recall results that look like an embedding problem; a too-large one looks like a latency problem. Always measure ANN recall against exact search on a sample of real queries, separately from end-to-end relevance.
- Malkov & Yashunin, HNSW: the original paper, including the neighbour-selection heuristic.
- Pinecone, Product Quantization and HNSW explainers: clear visual walkthroughs.
- Faiss: guidelines to choose an index: practical sizing rules by dataset size and memory.
- DiskANN repository: Vamana, filtered and streaming variants.
Vector databases and how to choose
A "vector database" is an ANN index plus everything a database needs: persistence, updates and deletes, metadata filtering, replication, sharding, backups, access control and usually keyword search. The options split into extensions of existing databases, search engines that added vectors, and purpose-built systems.
| System | What it is | Notable traits |
|---|---|---|
| pgvector | Postgres extension | HNSW and IVFFlat; vector up to 2,000 dims, halfvec up to 4,000, binary and sparse types; iterative index scans for filtered queries (0.8.0+). Transactions, joins and row-level security come free pgvector |
| Elasticsearch / OpenSearch | Lucene-based search engines with vector fields | Mature BM25, analyzers, aggregations; HNSW-based kNN with quantization options; native hybrid queries. OpenSearch can also use Faiss engines Elastic docs OpenSearch docs |
| Pinecone | Fully managed (serverless) vector DB | No ops; namespaces for tenant isolation; metadata filtering; sparse-dense hybrid Pinecone docs |
| Weaviate | Open-source (Go), cloud offering | HNSW, built-in hybrid BM25 + vector fusion, native multi-tenancy, model integrations Weaviate docs |
| Qdrant | Open-source (Rust), cloud offering | Filterable HNSW with payload indexes, quantization (scalar, binary, PQ), sparse vectors, multi-vector Qdrant docs |
| Milvus (Zilliz) | Open-source distributed vector DB | Disaggregated storage and compute, many index types (HNSW, IVF variants, DiskANN, GPU indexes), built for very large scale Milvus docs |
| LanceDB | Embedded / serverless, on the Lance columnar format | Runs in-process or on object storage; IVF-PQ and other indexes; good for multimodal and data-lake workflows LanceDB docs |
| turbopuffer | Managed search on object storage | State in object storage with NVMe/memory caching; vector + BM25 full-text + hybrid; namespaces, aimed at many-tenant workloads at low cost turbopuffer docs |
| Others | Vespa, Chroma, Redis, MongoDB Atlas, cloud-native (Vertex, Azure AI Search, OpenSearch Serverless), Faiss as a library | Pick by ecosystem fit |
Choosing criteria
- Scale and growth: under ~10M vectors almost anything works, and staying in your existing Postgres or Elasticsearch is often the right call. Hundreds of millions to billions pushes toward distributed or disk/object-storage-based systems.
- Filtering: how selective and how dynamic are your filters? Test filtered recall, not just unfiltered QPS.
- Hybrid support: native BM25 + vector with fusion in one query vs running two systems and fusing in the app.
- Multi-tenancy: thousands of tenants? Need per-tenant indexes or namespaces, cheap idle tenants, tenant-level deletion.
- Update pattern: streaming upserts and deletes vs nightly batch; consistency (read-your-writes) requirements.
- Ops and cost: managed vs self-hosted, RAM vs SSD vs object storage cost model, backups, observability, team familiarity.
- Data governance: residency, encryption, VPC/BYOC deployment, compliance certifications.
Interviewers are wary of "we picked a vector DB because it's popular". A strong answer starts with "Postgres + pgvector until it hurts" for moderate scale (one system, transactional consistency with your app data, ACL joins), and names the concrete triggers to move: vectors outgrow RAM, filtered recall issues, need for strong BM25/analyzers, very many tenants, or ingest throughput.
Vector DB features (quantization modes, hybrid search, filtering algorithms, pricing models) change quickly, and the 2025–2026 trend toward object-storage-backed and disk-based designs is ongoing. Verify current capabilities in each product's docs before quoting them.
- pgvector README: index options, filtering caveats, tuning.
- Qdrant, A Complete Guide to Filtering in Vector Search: pre/post/in-graph filtering explained.
- turbopuffer launch post: the case for object-storage-first search architecture.
Hybrid search and score fusion
Dense and sparse retrievers fail differently: dense handles paraphrase and semantics, sparse handles exact terms and rare tokens. Running both and merging their results is a cheap, reliable gain. The problem is that their scores are on incomparable scales (BM25 is unbounded and query-dependent; cosine lies in [−1, 1] and is usually compressed into a narrow band).
Reciprocal Rank Fusion
RRF ignores scores and uses only ranks Cormack+ 2009:
$$\text{RRF}(d) = \sum_{r \in R} \frac{1}{k + \text{rank}_r(d)}$$where \(R\) is the set of result lists, \(\text{rank}_r(d)\) is \(d\)'s 1-based rank in list \(r\) (documents absent from a list contribute nothing), and \(k=60\) is the constant from the original paper. The constant damps the dominance of the very top ranks: with \(k=60\), rank 1 scores 1/61 ≈ 0.0164 and rank 10 scores 1/70 ≈ 0.0143, so a document ranked well in both lists beats one ranked first in only one. Example: doc A is #1 in BM25 and #8 in dense: 1/61 + 1/68 ≈ 0.0311. Doc B is #3 and #2: 1/63 + 1/62 ≈ 0.0320, so B wins. RRF needs no tuning or score calibration, which is why it is the default in many engines.
Weighted score fusion
Alternatively, normalise each list's scores (min-max within the result set, or z-score) and combine linearly: \(s(d) = \alpha\,\tilde s_{\text{dense}}(d) + (1-\alpha)\,\tilde s_{\text{sparse}}(d)\). It keeps magnitude information (a huge BM25 win counts for more than a narrow one) and can beat RRF when \(\alpha\) is tuned on labelled data, but min-max normalisation is sensitive to outliers and to how many results you fetched, and the best \(\alpha\) varies by query type. A practical pattern: RRF by default, tuned weights once you have a golden set, possibly a per-query \(\alpha\) predicted by a router (keyword-looking queries lean sparse).
Fusing too few results. If each retriever returns only top-5, fusion has little to work with; retrieve 50–200 from each, fuse, then rerank the fused top 50–100.
- Cormack, Clarke & Büttcher 2009: the short original RRF paper.
- Weaviate, Hybrid Search Explained: ranked vs relative-score fusion in practice.
Reranking
Retrieval is optimised for recall at low cost; reranking is optimised for precision on a small candidate set. The canonical reranker is a cross-encoder: concatenate "[CLS] query [SEP] passage" and run a transformer that outputs a relevance score. Because every query token attends to every passage token, it models relevance much better than a dot product between independent vectors. BERT cross-encoders on MS MARCO were the first big demonstration Nogueira+ 2019.
The cost is one forward pass per (query, candidate) pair at query time, with nothing precomputable. Back-of-envelope: a ~100–500M-parameter cross-encoder scoring 100 passages of ~256 tokens on a GPU takes on the order of tens to a couple of hundred milliseconds batched; on CPU it can be far slower. So the candidate count (50–200) is a direct latency dial.
| Reranker type | How it scores | Quality | Latency / cost |
|---|---|---|---|
| Pointwise cross-encoder | Independent score per (q, d) | High | Low–moderate; batchable |
| Late interaction (ColBERT) as reranker | MaxSim over token vectors | Good | Low if doc vectors stored |
| LLM pointwise | Prompt "rate relevance 0–3" or use yes/no token logprob | High; follows instructions ("prefer recent docs") | High; parallelisable |
| LLM listwise | Give the LLM the query and a list of passages, ask for a permutation; sliding windows for long lists | Very high in studies | Highest; sequential windows; position bias |
| Pairwise | Compare two passages at a time | High | O(n²) comparisons unless sorted cleverly |
RankGPT showed that instructing GPT-4 to output a permutation of passages (listwise, with sliding windows) was competitive with or better than supervised rerankers, and that the ability could be distilled into small models Sun+ 2023. Commercial rerank APIs and open rerankers (BGE-reranker, Qwen3-Reranker, mxbai, Jina) are cross-encoder- or small-LLM-based.
Latency budgets. A typical interactive budget might allot ~10–50 ms to query embedding and ANN, ~50–200 ms to reranking, and the rest to LLM time-to-first-token. Techniques: rerank fewer candidates, use a smaller or distilled reranker, truncate passages, cache rerank results for repeated queries, and run rerank on GPU. The reranker score also gives you a threshold: if the best candidate scores below a cutoff, abstain or trigger a fallback (query rewrite, web search) rather than generating from junk.
"Recall@100 is 95% but answers are still wrong" usually means precision at the top is the problem: add or improve a reranker and send fewer, better chunks. Show that you know the two-stage logic: the retriever's job is to get the answer somewhere in the candidates; the reranker's job is to put it in the top few, because only those fit in the prompt and get attention.
- Sentence-Transformers, Retrieve & Re-Rank: practical bi-encoder + cross-encoder pipeline.
- Sun+ 2023, RankGPT: LLMs as listwise rerankers.
Query-side techniques
User queries are short, ambiguous, conversational ("what about for contractors?") and phrased unlike the documents that answer them. Query understanding transforms the query before retrieval, usually with a fast LLM call.
| Technique | Mechanism | Helps with | Cost / risk |
|---|---|---|---|
| Conversational rewriting | Rewrite the latest turn into a standalone query using chat history | Follow-ups, pronouns ("it", "that policy") | One small LLM call; essential for chat |
| Query expansion / multi-query | Generate several paraphrases; retrieve for each; fuse with RRF | Vocabulary mismatch, recall | N× retrieval cost; drift if paraphrases wander |
| HyDE | LLM writes a hypothetical answer document; embed that and search | Short queries vs long passages (asymmetric match) | LLM latency; hallucinated specifics can mislead |
| Query2doc | Append an LLM-generated pseudo-document to the query (works for sparse and dense) | Recall, especially BM25 | As HyDE |
| Decomposition | Split multi-part or multi-hop questions into sub-questions, retrieve per sub-question, possibly sequentially | Comparisons, multi-hop ("CEO of the company that acquired X") | More calls; needs orchestration |
| Step-back prompting | Ask a more general question first ("what are the principles of X?") and retrieve for both | Questions needing background or principles | Extra retrieval |
| Routing | Classify the query and send it to the right index, tool or strategy (SQL vs docs vs web; no-retrieval for chit-chat) | Heterogeneous sources, cost | Router errors are silent; monitor them |
| Metadata filter extraction (self-query) | LLM extracts structured filters ("Q3 2025 board decks from finance" → date range, doc_type, dept) plus a residual semantic query | Precision; queries with time or entity constraints | Wrong filters silently drop the answer; validate against schema |
HyDE generates a hypothetical document with an instruction-following LLM and uses an unsupervised contrastive encoder to embed it; the encoder's dense bottleneck filters out incorrect details, and it gave strong zero-shot retrieval without relevance labels Gao+ 2022. Query2doc expands queries with LLM pseudo-documents and improved BM25 notably Wang+ 2023. Step-back prompting has the model abstract to higher-level concepts before reasoning Zheng+ 2023.
Stacking every query trick at once. Each adds an LLM call (often 200–800 ms) and can hurt simple queries. Use routing: cheap path for simple lookups, expensive decomposition only for queries that need it, and measure each technique's effect per query category.
Advanced RAG patterns
GraphRAG: entity and community graphs
Plain vector RAG answers "local" questions well (a fact lives in a few chunks) and "global" questions badly: "What are the main themes across these 5,000 interviews?" has no single chunk that answers it. Microsoft's GraphRAG builds, at index time, an LLM-extracted knowledge graph of entities and relationships from all chunks, partitions it into hierarchical communities with the Leiden algorithm, and has an LLM pre-write a summary per community Edge+ 2024.
- Global search: map-reduce over community summaries. Each summary produces a partial answer with a helpfulness score; the best partial answers are reduced into a final one. Good for sensemaking and theme questions over a whole corpus; the paper reports gains in comprehensiveness and diversity over vector RAG on such questions.
- Local search: find entities related to the query, then fan out to their neighbours, relationships, community summaries and source chunks. Good for entity-centric questions ("What has Supplier X been involved in?").
Costs: indexing is LLM-heavy (entity extraction over every chunk plus summaries), so it is expensive and slow to refresh, and extraction quality bounds everything downstream. Lighter variants like LightRAG combine graph structure with vector retrieval and incremental updates Guo+ 2024. Use graph approaches when the questions are relational or global; for most fact-lookup workloads hybrid + rerank is cheaper and as good.
RAPTOR: hierarchical summaries
RAPTOR embeds chunks, clusters them (soft clustering with Gaussian mixtures over dimension-reduced embeddings), summarises each cluster with an LLM, then recursively clusters and summarises the summaries, building a tree from leaves (raw chunks) to root (corpus-level summary) Sarthi+ 2024. At query time, searching a "collapsed tree" (all levels in one index) lets a question match a detailed leaf or a high-level summary, whichever fits. Good for long documents (books, long reports) and questions needing synthesis across sections; costs are index-time LLM summarisation and refresh complexity.
Agentic RAG: retrieval as a tool
Instead of one fixed retrieve-then-generate pass, give the LLM a search tool (or several: vector search, keyword search, SQL, web) and let it decide when and what to search, read results, and search again, in a ReAct-style loop that interleaves reasoning and actions Yao+ 2022. This handles multi-hop questions naturally (look up X, use the result to formulate the next query), can reformulate after a bad result, and can stop when it has enough evidence. Newer work trains models with reinforcement learning to interleave reasoning and search calls Jin+ 2025, and "deep research" style products run long agentic retrieval loops.
Trade-offs: higher and more variable latency and cost (several LLM turns), harder evaluation (trajectories, not single calls), and failure modes such as looping, over-searching, or stopping early. Guardrails: step limits, budgets, deduplication of already-seen results, and logging every tool call for debugging.
Corrective and self-reflective RAG
- Self-RAG trains the generator to emit special reflection tokens that decide whether to retrieve, judge whether each passage is relevant, whether the generated segment is supported by it, and whether the response is useful, enabling adaptive retrieval and critique at inference time Asai+ 2023.
- Corrective RAG (CRAG) adds a lightweight retrieval evaluator that grades retrieved documents. Confident-correct results are refined (split into strips, irrelevant strips filtered); incorrect ones trigger web search; ambiguous cases combine both Yan+ 2024.
The production-friendly takeaway is the pattern, not necessarily the trained models: grade the retrieval before generating (reranker score threshold or an LLM relevance check), and branch: answer, re-query, escalate to another source, or abstain. Google researchers formalised a related idea as "sufficient context", whether the retrieved context is enough to answer at all, and found models often answer confidently when it isn't Joren+ 2024.
When asked about GraphRAG or agentic RAG, interviewers want judgement, not enthusiasm. Say what question types each one fixes (global/relational; multi-hop), what it costs (index-time LLM spend; latency and unpredictability), and that you'd adopt it only after a golden set showed the simpler pipeline failing on those question types.
Agentic retrieval (RL-trained search agents, deep-research loops, "context engineering" for agents) was the most active RAG area in 2025–2026, and best practice is not settled. GraphRAG variants also keep evolving. Treat specific architectures here as examples, and check recent literature.
- Microsoft GraphRAG docs: indexing pipeline and global/local/DRIFT search modes.
- Sarthi+ 2024, RAPTOR.
- Yan+ 2024, Corrective RAG and Asai+ 2023, Self-RAG.
Permissions, ACLs and multi-tenancy
The rule: a user must never see, in an answer or a citation, content they could not open in the source system. The LLM cannot be trusted to "ignore" documents in its context, and prompt injection can make it reveal them, so enforcement must happen in retrieval, before anything reaches the prompt.
- Capture ACLs at ingest: store allowed principals (users, groups, roles) per document on each chunk. Expand groups at query time from the identity provider (more flexible, as group memberships change often) or at index time (faster queries, but stale when groups change).
- Filter in the query:
allowed_principals ∩ user_principals ≠ ∅as a metadata filter on both dense and sparse retrieval. That makes ACLs a filtered-ANN problem, so test recall for users with narrow access. - Propagate changes fast: permission revocations should reach the index within minutes, which means ACL-only updates without re-embedding. Some systems add a late check: re-verify access against the source system for the final top-k before generation (defence in depth, small latency cost).
- Multi-tenancy isolation levels: (1) shared index with a tenant-id filter: cheapest, but a filter bug leaks data and noisy neighbours share resources; (2) namespace/partition per tenant: strong logical isolation, per-tenant deletion and indexing, good when tenants are many and mostly small; (3) separate index, cluster or account per tenant: strongest isolation and compliance (residency, encryption keys), highest cost. Many products mix them: dedicated for large enterprise tenants, namespaces for the long tail.
- Everything derived inherits ACLs: contextual summaries, RAPTOR or GraphRAG community summaries, caches, and semantic caches can leak across permission boundaries if they mix documents with different ACLs. Build derived artefacts per permission scope or tag them with the intersection of their sources' ACLs.
- Deletion: right-to-erasure means removing the chunk, its vectors (tombstones must be compacted), derived summaries, caches and eval logs.
Caching answers or retrieved contexts keyed only by query text. Two users with different permissions asking the same question must not share a cache entry; include the permission scope (or tenant and group hash) in the key.
"Build enterprise search over Google Drive and Slack" always includes permissions. Mention ACL sync from source APIs, group expansion, filtering inside retrieval, revocation latency, per-tenant isolation choices, and the derived-artefact leak. Candidates who suggest "tell the LLM not to reveal restricted docs" usually fail this part.
Freshness and incremental indexing
Indexes go stale in three ways: new or changed documents, deleted documents, and changed permissions. Design the ingest path as a change stream.
- Change detection: webhooks or change feeds from sources where available, otherwise periodic crawls comparing modified timestamps or content hashes. Postgres sources can use logical replication or CDC.
- Idempotent upserts: deterministic chunk ids (doc id + chunk index or content hash), so reprocessing a document replaces its chunks. When a document changes, delete all its old chunks before or atomically with inserting the new ones; otherwise stale chunks linger, especially when the chunk count changes.
- Skip unchanged work: hash each chunk's text; re-embed only chunks whose hash changed. Embedding is often the dominant ingest cost, contextual retrieval even more so.
- Index mechanics: HNSW handles inserts online but deletes as tombstones; IVF appends easily but drifts as data changes (retrain centroids); many engines use an LSM-like design with small fresh segments merged into big optimised ones, so recent data is searchable within seconds.
- Versioning: keep
doc_version,indexed_atandembedding_modelon every chunk. Model upgrades become blue-green re-indexes; time-sensitive queries can filter or boost by recency. - Monitor lag: source-change-to-searchable latency, per-connector error rates, and a canary document updated on a schedule that synthetic queries check.
Freshness is a data-engineering problem first. The ANN index is rarely the bottleneck; connector reliability, deletion propagation and re-embedding cost are.
Evaluating RAG
Evaluate the two halves separately, then end to end. If you only measure final answers, you cannot tell whether a regression came from retrieval or generation.
Retrieval metrics
Given a query with a set of relevant chunks (or documents) and a ranked list of retrieved ones:
- Recall@k = (relevant items in the top k) / (total relevant items). The most important RAG retrieval metric: if the evidence isn't in the top k you pass to the LLM, the answer can't be grounded. "Hit rate@k" is the binary version (at least one relevant in top k).
- Precision@k = (relevant in the top k) / k. Matters because irrelevant context costs tokens and can distract the generator.
- MRR (mean reciprocal rank) \(= \frac{1}{|Q|}\sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i}\), where \(\text{rank}_i\) is the position of the first relevant result. Rewards putting one good answer near the top; ignores the rest.
- nDCG@k handles graded relevance (0 = irrelevant, 1 = partial, 2 = perfect) and position discounting Wikipedia:
where IDCG is the DCG of the ideal ordering, so nDCG lies in [0, 1]. (A linear-gain variant uses \(\text{rel}_i\) instead of \(2^{\text{rel}_i}-1\).) Example with binary relevance, relevant items at ranks 1 and 3 of top 3, two relevant total: DCG = 1/1 + 1/2 = 1.5; IDCG = 1 + 1/log₂3 ≈ 1.631; nDCG ≈ 0.92. nDCG@10 is the standard BEIR/MTEB retrieval metric.
Generation metrics
| Metric | Question it answers | How it is typically computed |
|---|---|---|
| Faithfulness / groundedness | Is every claim in the answer supported by the retrieved context? | LLM splits the answer into atomic claims, checks each against the context (NLI-style); score = supported / total |
| Answer relevance | Does the answer address the question? | LLM judge, or generate questions from the answer and compare their embeddings to the original |
| Context precision | Are the relevant chunks ranked high among those retrieved? | Per-chunk relevance judgement, rank-weighted |
| Context recall | Does the context contain everything needed for the reference answer? | Split the reference answer into statements; check each is attributable to the context |
| Answer correctness | Does it match the reference answer? | LLM judge vs reference, or exact/F1 match for short answers |
| Citation accuracy | Do cited sources actually support the cited claims? | Check each (claim, citation) pair |
| Abstention quality | Does it say "I don't know" when context is insufficient? | Include unanswerable questions in the golden set |
RAGAS popularised reference-free versions of several of these (faithfulness, answer relevance, context relevance) computed with LLM judges Es+ 2023; ARES trains lightweight judges on synthetic data and uses a small human-labelled set to put confidence intervals on the scores Saad-Falcon+ 2023. LLM judges have known biases (verbosity, position, self-preference) and must be calibrated against human labels on a sample before you trust them.
Building a golden set
- Collect real questions: from logs, support tickets, or interviews with users. Synthetic LLM-generated questions (from sampled chunks) fill coverage gaps, but they tend to be easier and to reuse the chunk's wording, which flatters lexical and dense retrieval alike.
- Label: for each question, the relevant chunk or document ids (for retrieval metrics) and a reference answer (for generation metrics). Have domain experts review; label at document level if chunk ids churn with re-chunking.
- Stratify: fact lookup, multi-hop, comparison, numeric/table, time-sensitive, unanswerable, permission-restricted, and adversarial (prompt injection in documents). Report metrics per stratum; averages hide regressions.
- Size: a hundred or two well-chosen questions catches large regressions; a few hundred to a thousand gives tighter comparisons. Keep a frozen test split you don't tune on.
- Run in CI: every change to parsing, chunking, embedder, index parameters, prompt or model runs the suite; track metrics over time. Add every production failure as a new test case.
"How would you evaluate this RAG system?" The strong answer has layers: offline golden set with retrieval metrics (recall@k, nDCG) and generation metrics (faithfulness, correctness) using calibrated LLM judges; online signals (thumbs, citation clicks, follow-up rephrasing, escalation to humans); and the ability to slice by query type. Mention that you measure ANN recall versus exact search separately from relevance.
- Es+ 2023, RAGAS and RAGAS docs: metric definitions and implementations.
- Saad-Falcon+ 2023, ARES: judge calibration with confidence intervals.
- Joren+ 2024, Sufficient Context: separating "context was insufficient" from "model ignored context".
Failure modes and debugging
The first debugging question for any bad answer is attribution: was the right evidence in the context window?
Log every request with the rewritten query, filters, candidate ids and scores from each retriever, the reranked list, the final prompt, and the answer. Then attribution is a lookup, not guesswork. A useful offline test: give the generator the gold chunk directly ("oracle context"). If it still fails, the problem is generation; if it succeeds, the problem is retrieval.
| Symptom | Likely cause | Fix |
|---|---|---|
| Misses on product codes, names, error strings | Dense-only retrieval | Add BM25 hybrid; check tokenisation/analyzers |
| Retrieves the right document, wrong section | Chunks too big or too small; lacking context | Structure-aware chunking; contextual prefixes; rerank |
| Answers from an outdated version | Stale chunks not deleted; no recency signal | Delete-on-update; version metadata; recency boost or filter |
| Confident answer when nothing relevant exists | No sufficiency check; prompt pushes the model to answer | Reranker score threshold; explicit abstain instruction; unanswerable cases in evals |
| Wrong numbers from tables | Table flattened during parsing | Layout-aware parsing; keep tables whole with headers |
| Good on tests, poor in production | Synthetic golden set unlike real queries | Mine real queries from logs; stratify |
| Filtered queries return few or no results | Post-filtering with selective filters | Filter-aware ANN, iterative scans, brute force for small subsets, partitioning |
| Follow-up questions fail | No conversational rewriting | Standalone-query rewrite using history |
| Contradictory chunks lead to a blended answer | Multiple versions or sources disagree | Dedup by version; surface the conflict; source-priority metadata |
| Model follows instructions found inside documents | Indirect prompt injection | Treat retrieved text as data (delimiters, instructions), limit tool permissions, sanitise sources, monitor |
| Latency spikes | Rerank over too many candidates; multi-query fan-out; cold caches | Cap candidates; route simple queries to the cheap path; cache embeddings and results |
Swapping in a bigger generator to fix what is really a retrieval miss. Check recall@k first; a better LLM cannot cite a chunk it never saw. The opposite mistake is also common: endlessly tuning retrieval when oracle-context tests show the generator is ignoring good evidence.
Assemble context deliberately: put the strongest evidence first (or first and last), dedupe near-identical chunks, label each chunk with source and date, and keep the total small enough that the model can attend to all of it. More context is not free: it costs tokens and can lower answer quality through distraction.
- Barnett+ 2024, Seven Failure Points: failure taxonomy from real deployments.
- Liu+ 2023, Lost in the Middle: why position in context matters.
- Chroma, Context Rot: an industry study of how performance degrades as input length grows.
Interview question bank
When would you choose RAG over fine-tuning, and when over stuffing everything into a long context?
RAG over fine-tuning whenever the goal is knowledge: facts that change, need citations, or need per-user permissions. Fine-tuning injects facts unreliably and can't be scoped or updated quickly; controlled comparisons find RAG beats unsupervised fine-tuning for knowledge injection. Fine-tune for behaviour: format, tone, tool use, domain style, or to make the model better at using retrieved context. Versus long context: if the corpus is small (a few hundred pages), static, and shared by all users, putting it all in a cached prompt is simpler and may be more accurate for whole-document reasoning. RAG wins when the corpus is large, permissions vary per user, per-query cost and latency matter, or freshness matters. Hybrids are common: retrieve the relevant documents, then pass them whole rather than as tiny chunks.
Walk me through the InfoNCE loss and why batch size matters for training embedders.
For each query in a batch, compute similarities to its positive document and to all other documents in the batch, divide by a temperature, and apply softmax cross-entropy where the correct class is the positive: \(-\log \frac{e^{s(q,d^+)/\tau}}{\sum_j e^{s(q,d_j)/\tau}}\). The other positives in the batch serve as free negatives. Larger batches mean more negatives per query, a harder discrimination task and a better gradient estimate of the full softmax, so embedders train with very large batches (gradient caching or cross-device negative sharing make that feasible). Hard negatives mined with BM25 or an earlier model add difficulty but risk being unlabelled positives, so they are often filtered with a cross-encoder. Temperature controls how peaked the distribution is; too high blurs distinctions, too low makes training unstable.
You have 50M chunks embedded at 1024 dimensions. How much memory does an HNSW index need, and how would you reduce it?
Raw float32 vectors: 50M × 1024 × 4 B ≈ 205 GB. HNSW graph at M=16: about 2M × 4 B = 128 B per node at layer 0 plus a little for upper layers, so roughly 7 GB more. Total around 210 GB plus process overhead, which means a big memory instance or several shards. Reductions: int8 scalar quantization (4×, ≈ 51 GB) with near-lossless rescoring; MRL truncation to 512 or 256 dims if the model supports it (2–4×); binary quantization (32×, ≈ 6.4 GB for vectors) with rescoring of the top few hundred from float vectors on SSD; PQ (for example 128 bytes per vector ≈ 6.4 GB) in IVF-PQ; or DiskANN with compressed vectors in RAM and full vectors on SSD. Validate each against a recall@10 target on real queries.
Explain how HNSW search works and what M, efConstruction and efSearch control.
HNSW is a multi-layer proximity graph. Every node is on layer 0; each node also appears on higher layers with exponentially decreasing probability, so top layers are sparse with long-range links. Search starts at a fixed entry point on the top layer, greedily walks to whichever neighbour is closest to the query, and when it can't improve, drops down a layer and continues from there. On layer 0 it runs a beam search with candidate list size efSearch and returns the best k. M is the maximum number of links per node (2M on layer 0): higher means better recall and robustness in high dimensions but more memory and slower build. efConstruction is the beam width used when inserting nodes: higher builds a better graph more slowly, with no query-time cost. efSearch is the query-time recall/latency dial and can be set per query; it must be at least k.
How does product quantization compress vectors, and what is asymmetric distance computation?
Split each D-dimensional vector into m subvectors. For each subspace, run k-means to learn 256 centroids; encode each subvector as the one-byte id of its nearest centroid, so a vector becomes m bytes (768-d float32 at 3,072 B becomes 96 B with m=96). At query time, keep the query uncompressed and precompute, for each subspace, the distance from the query's subvector to all 256 centroids: an m × 256 lookup table. The approximate distance to any database vector is the sum of m table entries indexed by its code, with no decompression and very cache-friendly. "Asymmetric" means the query is exact while database vectors are quantized, which is more accurate than quantizing both. Because distances are approximate, systems usually re-rank the top candidates with full-precision vectors. In IVF-PQ the codes encode residuals relative to the IVF centroid, which improves accuracy.
Write the BM25 formula and explain k1 and b. When does BM25 beat dense retrieval?
score = Σ over query terms of IDF(t) × f(t,D)(k1+1) / (f(t,D) + k1(1 − b + b·|D|/avgdl)). IDF rewards rare terms. k1 controls term-frequency saturation: the TF part approaches k1+1, so repeated occurrences give diminishing returns (k1 around 1.2–2). b controls document-length normalisation: b=0.75 by default; b=0 ignores length, b=1 fully normalises against the average length. BM25 beats dense retrieval on exact identifiers (SKUs, error codes, function names), rare proper nouns, new jargon the embedder never saw, and out-of-domain corpora; on BEIR, many early dense models trained on MS MARCO underperformed BM25 zero-shot. Dense wins on paraphrase and semantic queries, which is why hybrid is the default.
What is Reciprocal Rank Fusion? Compute a small example.
RRF merges ranked lists using only ranks: score(d) = Σ over lists of 1/(k + rank(d)), usually with k=60, and documents missing from a list get nothing from it. It avoids calibrating incomparable scores (BM25 vs cosine). Example: document A is rank 1 in BM25 and rank 10 in dense: 1/61 + 1/70 ≈ 0.0164 + 0.0143 = 0.0307. Document B is rank 4 and rank 3: 1/64 + 1/63 ≈ 0.0156 + 0.0159 = 0.0315. B wins, showing RRF rewards consistent agreement over one strong vote. The constant k damps the top-rank advantage; smaller k makes the fusion more top-heavy. Weighted score fusion with normalised scores can do better if you tune the weight on labelled data, but RRF is a robust zero-tuning default.
Why do we need a reranker if we already have good embeddings? How do you fit it into a latency budget?
Bi-encoders compress each document into one vector before seeing the query, so they can't model fine query–document interactions such as negation, specific constraints or which entity is the subject. A cross-encoder reads query and passage together with full attention and scores relevance much more accurately, but needs a forward pass per pair at query time, so it can only be applied to a shortlist. The two-stage pattern is: retriever for recall (top 50–200), reranker for precision (top 5–10 into the prompt). Budget: a small cross-encoder on GPU can score ~100 short passages in tens of milliseconds batched; tune the candidate count, truncate passages, use a distilled model, and cache. Reranker scores also give a usable confidence threshold for abstaining. LLM listwise rerankers are more accurate still but cost hundreds of milliseconds to seconds.
Describe chunking strategies and how you'd pick one for a corpus of technical manuals.
Options: fixed-size, recursive (split on the largest natural separator that fits), structure-aware (headings, sections), semantic (split where adjacent-sentence similarity drops), overlap windows, parent-child (match small chunks, return the parent section), late chunking (embed the whole doc then pool per chunk), and contextual retrieval (prepend an LLM-written context to each chunk). For technical manuals, which have clear heading hierarchies, tables and procedures, I'd parse to Markdown with a layout-aware parser, split on headings, keep procedures and tables intact, sub-split long sections recursively to a few hundred tokens with modest overlap, prepend the heading path ("Model X > Maintenance > Replacing the filter") and use parent expansion so the LLM sees the full procedure. Then compare two or three variants on a golden set with recall@k and answer correctness.
Users with restricted access must not see confidential content. Design the permission model for a RAG system.
Sync ACLs from each source at ingest (users, groups, sharing links) and store allowed principals on every chunk. At query time, resolve the user's principals (user id plus groups from the IdP, cached briefly) and apply them as a filter inside both dense and sparse retrieval, so restricted chunks never reach the reranker or the prompt. Handle revocation with ACL-only updates that propagate within minutes, and optionally re-check access for the final top-k against the source API. Choose tenant isolation by risk: namespaces per tenant by default, dedicated indexes for large or regulated customers. Make sure derived artefacts (contextual summaries, community summaries, caches, semantic cache, logs) respect the same boundaries, and include the permission scope in cache keys. Test with users who have very narrow access, since that's where filtered ANN recall collapses.
Pre-filtering vs post-filtering in vector search: what goes wrong with each?
Post-filtering runs the ANN query for the top k′ and then discards results failing the filter. With a selective filter, most of the k′ results are discarded and you return too few, or zero, results; increasing k′ helps but costs latency and still fails for very selective filters. Pre-filtering computes the allowed set first and searches only within it. For brute force that's exact and fast when the set is small, but inside a graph index, restricting traversal to allowed nodes can leave them disconnected from the entry point, so the search gets stuck and recall drops. Mitigations: query planning by selectivity (brute-force small sets, ANN otherwise), filter-aware traversal that walks through disallowed nodes but only returns allowed ones, denser or filter-specific edges (ACORN, filterable HNSW), iterative scans (pgvector 0.8+), and partitioning by high-cardinality keys like tenant.
Define recall@k, MRR and nDCG. Which would you prioritise for RAG retrieval and why?
Recall@k is the fraction of all relevant items that appear in the top k. MRR averages 1/rank of the first relevant item over queries. nDCG@k sums graded relevance discounted by log2(rank+1), normalised by the ideal ordering's DCG, so it rewards putting highly relevant items first. For RAG, recall@k at the k you actually pass to the LLM matters most: if the evidence isn't in the context, the answer can't be grounded. For the candidate stage feeding a reranker, recall@100 matters; for the final context, precision and nDCG@k at small k matter because irrelevant chunks waste tokens and distract. MRR is useful when a single relevant passage suffices. I'd report recall@{5,20,100} and nDCG@10, sliced by query type.
A user reports a wrong answer. How do you determine whether retrieval or generation is at fault?
Pull the trace: rewritten query, filters, candidates and scores from each retriever, reranked list, final prompt, answer. Check whether the correct evidence exists in the corpus and was parsed correctly; whether it is in the candidates; whether it survived reranking into the context; and whether the LLM used it. If the evidence was in the context and the answer is still wrong or unfaithful, it's generation (prompt, conflicting chunks, position, model). If not, it's retrieval or ingestion, and you narrow further to parsing, chunking, embedding, filters or ANN recall. An offline oracle test, feeding the gold chunk directly, confirms generation capability. Then add the case to the golden set so it stays fixed.
Explain HyDE. When does it help and when does it hurt?
Hypothetical Document Embeddings: ask an LLM to write a plausible answer passage to the query, embed that passage, and use its embedding for search. It helps because a hypothetical answer looks like the target documents (same vocabulary, length and style) whereas short queries don't, reducing query–document asymmetry; the original paper used it for zero-shot dense retrieval without relevance labels. It hurts when the LLM lacks domain knowledge and invents specifics (wrong product names or numbers) that pull retrieval toward wrong documents, on exact-identifier lookups where BM25 is better anyway, and on latency, since it adds an LLM call before retrieval. Mitigations: combine HyDE results with the original query via RRF, and route only vague or natural-language questions to it.
What is GraphRAG and when would you use it instead of vector RAG?
GraphRAG uses an LLM at index time to extract entities and relationships from every chunk into a knowledge graph, clusters the graph into hierarchical communities (Leiden), and pre-generates summaries for each community. Global search map-reduces over community summaries to answer corpus-wide questions ("what are the main themes / risks across all these documents?") which vector RAG handles poorly because no single chunk contains the answer. Local search starts from entities matching the query and expands through the graph. Use it when questions are global or relational and the corpus is fairly stable. The costs are large index-time LLM spend, slow refresh, and dependence on extraction quality. For fact lookups, hybrid search plus reranking is cheaper and usually as good.
Design semantic search for an e-commerce catalog of 20M products with filters (price, brand, in-stock) and typo-heavy queries.
Represent each product with text (title, attributes, description) for BM25 with typo tolerance (fuzzy matching, n-gram analyzers) plus a dense embedding fine-tuned on query–click pairs, and possibly an image embedding. Index: 20M × 768 float32 ≈ 61 GB, so HNSW with int8 (≈ 15 GB) on a few replicas, or a search engine with native vectors and BM25. Filters are central: price and brand are structured filters, and in-stock changes constantly, so it should be a fast-updating field rather than requiring re-embedding; use an engine with filter-aware ANN. Retrieval: hybrid BM25 + dense with RRF, then a learning-to-rank or cross-encoder stage that also uses business signals (popularity, margin, availability). Query understanding extracts filters ("red nike shoes under $100" → color, brand, price). Evaluate offline with nDCG on click/purchase-labelled queries and online with A/B tests on conversion. Latency target likely under 100 ms, so keep the reranker small.
What are Matryoshka embeddings and binary quantization, and how would you combine them?
Matryoshka Representation Learning trains an embedder so that every prefix of the vector (64, 128, 256 … dims) is a usable embedding, by summing the contrastive loss at several nested lengths. You can truncate vectors (and re-normalise) to save memory with graceful quality loss. Binary quantization keeps one bit per dimension (the sign), 32× smaller than float32, with Hamming distance via XOR and popcount, which is very fast. Combined: index truncated binary vectors for first-pass candidate generation (e.g. 512 bits = 64 bytes per item), retrieve a few hundred candidates, then rescore with full-precision or int8 vectors stored on disk or in cheaper memory. Reported results show binary retaining roughly 90–96% of retrieval quality with rescoring, but it's model-dependent, so measure on your data.
How do you keep a RAG index fresh when source documents change frequently?
Treat ingestion as a change stream: webhooks or change feeds where available, otherwise periodic diffing on modified time or content hash. Use deterministic chunk ids and replace all of a document's chunks atomically on update, so stale chunks don't linger when the chunk count changes. Re-embed only chunks whose content hash changed. Propagate deletions and permission changes as first-class events (ACL-only updates without re-embedding). Choose an index that supports online upserts (HNSW with tombstones and periodic compaction, or segment-based engines that merge in the background), and retrain IVF centroids if the distribution drifts. Store version, indexed_at and embedding_model on each chunk, and monitor source-to-searchable lag with a canary document.
How would you build a golden evaluation set for a new internal-docs assistant with no query logs yet?
Start with stakeholder interviews and existing artefacts (support tickets, Slack questions, FAQ pages) to collect real question phrasings, aiming for 100–200 initially. Supplement with LLM-generated questions from sampled chunks, but rewrite or filter them to avoid copying the chunk's wording, and add multi-hop questions spanning documents. Label relevant documents and reference answers with domain experts. Include unanswerable questions, permission-restricted ones (tested with different user identities), time-sensitive ones, and table/numeric ones. Stratify by type, freeze a test split, and run it in CI against every pipeline change. After launch, mine logs (thumbs-down, rephrased follow-ups, escalations) to add real failure cases continuously.
What does faithfulness measure, how is it computed automatically, and what are its limits?
Faithfulness (groundedness) measures whether the claims in an answer are supported by the retrieved context, independent of whether they are true in the world. Automated versions use an LLM to decompose the answer into atomic claims, then check each against the context with an NLI-style judgement; the score is supported claims over total claims. Limits: it says nothing about whether the context was right or complete (pair it with context recall and answer correctness); LLM judges miss subtle numeric or negation errors and have biases; claim decomposition is itself noisy; and an answer of "I don't know" is trivially faithful. Calibrate the judge against a few hundred human labels and track agreement.
You need to search 1 billion 768-dim vectors with p99 under 50 ms. Sketch the design.
Raw float32 is about 3 TB, so full-precision in RAM means a large cluster. More economical: IVF-PQ with 64–96-byte codes (≈ 64–96 GB) sharded across a few nodes or GPUs, with full vectors on SSD for reranking the top few hundred; or DiskANN per shard with PQ vectors in RAM and graph plus full vectors on NVMe, giving a few SSD round-trips per query. Shard by hash (or by tenant if queries are tenant-scoped, which avoids scatter-gather), query all shards in parallel, merge top-k; p99 is dominated by the slowest shard, so over-provision replicas and use hedged requests. Tune nprobe or beam width to hit a recall@10 target, measured against exact search on a sample. Plan for ingestion (streaming inserts into fresh segments merged later), filtering strategy, and rebuild time when the embedding model changes.
What is late interaction (ColBERT), and why might you use ColPali for a document corpus?
ColBERT keeps one embedding per token for queries and documents, and scores with MaxSim: for each query token, take its maximum similarity over document tokens, then sum. Document token embeddings are precomputed, so it's far cheaper than a cross-encoder, while token-level matching gives better accuracy and out-of-domain robustness than single-vector models. The cost is storage (one vector per token), which ColBERTv2 reduces with residual compression. ColPali applies this to page images: a vision-language model produces patch embeddings for each page screenshot, matched against text query tokens with MaxSim. It's attractive for visually rich corpora (slides, reports with charts, scanned forms) because it skips brittle OCR and layout parsing, at the cost of many vectors per page and a multimodal generator downstream.
What is contextual retrieval and what does it cost?
Before indexing, each chunk is passed to an LLM together with its full document, and the LLM writes a short (roughly 50–100 token) context situating the chunk ("from ACME's Q2 2023 10-Q, discussing revenue"). That context is prepended to the chunk for both embedding and BM25 indexing, fixing chunks that lack their subject or time frame. Anthropic reported retrieval-failure reductions (1 − recall@20) of 35% with contextual embeddings, 49% adding contextual BM25, and 67% adding reranking, on their own benchmarks. Cost is one LLM call per chunk at index time; with prompt caching of the document, they estimated about $1 per million document tokens. It also increases re-indexing cost whenever documents change, and the generated contexts must respect the source document's permissions.