Track B · AI product engineering

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

RAGLong context (stuff everything)Fine-tuning
Corpus sizeUnboundedUp to the context window (and cost)N/A (bakes into weights)
FreshnessSeconds–minutes (re-index)Immediate (next prompt)Retrain cycle
Per-user permissionsYes, via filteringYes, if you assemble per userNo
CitationsNaturalPossible (quote spans)No
Cost per queryLow (few k tokens)High (all tokens, mitigated by caching)Low at inference
Failure modeRetrieval missesLost-in-the-middle, distraction, costHallucinated or stale facts
Best forLarge, changing, permissioned corporaSmall corpora, whole-document reasoningStyle, format, task behaviour
Interview angle

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

May be out of date

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.

Go deeper

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

OFFLINE: indexing Ingestconnectors, CDC ParsePDF, OCR, layout Chunk+ metadata, ACL Embeddense + sparse IndexANN + inverted Search store vectors · BM25 postings metadata · ACLs · versions ONLINE: per query Query+ user, tenant Understandrewrite, filters Retrievehybrid, top-100 Reranktop-5..10 Assembleorder, budget GenerateLLM Citespans ACL-filtered search
The offline path writes chunks into a store holding vectors, sparse postings, metadata and ACLs. The online path rewrites the query, retrieves with permission filters, reranks, packs a context window and generates a cited answer.
StageWhat it doesMain knobsTypical failure
IngestPull docs from sources (Drive, Confluence, S3, DBs, tickets), track versions and deletionsConnectors, change data capture, scheduleStale or missing docs; deletes never propagated
ParseTurn bytes into clean text plus structure (headings, tables, figures)Parser choice, OCR, layout model, VLMGarbled tables, lost reading order, headers/footers in every chunk
ChunkSplit into retrievable units with metadataSize, overlap, boundaries, parent links, context prefixAnswer split across chunks; chunk lacks the context that names its subject
EmbedDense (and/or sparse) vectors per chunkModel, dimension, quantization, instruction prefixesDomain mismatch; query/doc prefix mismatch
IndexANN index + inverted index + metadata storeHNSW M/ef, IVF nlist/nprobe, PQLow recall from bad params; filters return too few results
RetrieveCandidate generation, top-kk, hybrid weights, filtersRelevant chunk not in top-k
RerankPrecise relevance scoring of candidatesModel, candidate count, latency budgetAdded latency; reranker domain mismatch
AssembleDedup, order, trim to token budget, add source tagsBudget, ordering, parent expansionContext overflow, duplicates, lost-in-the-middle
GenerateAnswer conditioned on contextPrompt, model, temperature, abstain instructionIgnoring context, blending in parametric knowledge
CiteAttach source spans to claimsInline markers, post-hoc attribution checkCitations that don't support the claim
Intuition

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.

Go deeper

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.

ApproachHow it worksGood atWeak at
Text-layer extraction (pdfminer, PyMuPDF-style)Read embedded glyphs and positions, heuristically group into lines and blocksFast, cheap, exact characters for born-digital PDFsTables, multi-column order, scanned pages, equations
OCR (Tesseract-style, cloud OCR)Recognise characters from page imagesScanned docs, images of textLayout; 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/JSONStructure-preserving output; tables as real rows/cells; drops boilerplateSlower; GPU helps; still errs on unusual layouts
End-to-end OCR-to-markup models (e.g. Nougat)Image-to-sequence transformer emits Markdown/LaTeX directlyScientific papers, equationsHallucination/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 promptCharts, complex tables, forms, handwriting; describes figuresCost per page; can silently paraphrase or hallucinate; needs validation
Skip parsing: embed page images (ColPali-style)Retrieve directly over page screenshots with a vision retrieverVisually 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

Interview angle

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

May be out of date

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.

Go deeper
  • 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:

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.

StrategyMechanismProsCons / when to use
Fixed-sizeEvery N tokensTrivial, predictable sizesCuts mid-sentence or mid-table. Baseline only.
Recursive character/tokenTry to split on the largest separator (section, paragraph, newline, sentence, word) that keeps chunks under NRespects natural boundaries cheaply; good defaultStill structure-blind beyond separators
Sliding window / overlapConsecutive chunks share e.g. 10–20% of tokensReduces "answer straddles boundary" missesIndex grows by the overlap fraction; near-duplicate hits
Structure-aware (Markdown/HTML/code)Split on headings, sections, functions; attach heading path as metadataSemantically coherent units; heading path gives contextNeeds good parsing; sections vary wildly in size (sub-split big ones, merge tiny ones)
Semantic chunkingEmbed sentences, start a new chunk where similarity between neighbours dropsTopic-coherent chunks without structureExtra embedding cost; gains over recursive splitting are inconsistent in practice
Parent-child / small-to-bigIndex small child chunks (or sentences); at query time return the parent section or a window around the hitPrecise matching and rich contextTwo-level storage; more tokens per hit; must dedup parents
Late chunkingRun a long-context embedder over the whole document, then pool token embeddings per chunk spanEach chunk vector "sees" the whole document (resolves pronouns, references)Needs a long-context embedder and token-level outputs; doc length limited
Contextual retrievalLLM writes a short context ("This chunk is from ACME's Q2 2023 10-Q, revenue section…") prepended before embedding and BM25 indexingLarge measured recall gains; works with any embedderOne LLM call per chunk at index time (cheap with prompt caching)
Propositions / LLM summariesLLM rewrites the doc into atomic facts or summaries that are indexed, pointing back to source textVery precise matchingIndex-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.

Original chunk: "The company's revenue grew by 3% over the previous quarter." Contextualised chunk: "This chunk is from ACME Corp's SEC filing for Q2 2023; the previous quarter's revenue was $314M. The company's revenue grew by 3% over the previous quarter." -> embedded AND added to the BM25 index; the original text is what you show the LLM/user (optional)

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.

Common mistake

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.

Interview angle

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.

Go deeper

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:

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:

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:

Model family (examples)TypeNotes
E5, BGE, GTE, Arctic-Embed, Nomic, MiniLMOpen encoder-styleSmall to large; self-hostable; good baselines Merrick+ 2024
BGE-M3Open, multilingualDense + sparse + multi-vector from one model
Qwen3-Embedding (0.6B–8B) and Qwen3-RerankerOpen, LLM-basedTopped the MTEB multilingual leaderboard at release (mid-2025) Zhang+ 2025
EmbeddingGemmaOpen, small (~300M)On-device class, Matryoshka dims Vera+ 2025
Gemini Embedding, OpenAI text-embedding-3, Voyage, Cohere EmbedCommercial APIsStrong general quality; MRL-style dimension control on several; data leaves your boundary Lee+ 2025
May be out of date

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.

Interview angle

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

Go deeper

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:

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.

Go deeper

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-encoderLate interaction (ColBERT)Cross-encoder
Doc precomputation1 vector1 vector per tokenNone
Query-time cost1 encode + ANN1 encode + MaxSim over candidates1 forward pass per (q, d) pair
StorageSmallLarge (compressible)None
AccuracyGoodBetter, robust out of domainBest
RoleFirst-stage retrievalFirst stage or rerankerReranker only
Go deeper

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:

Layer 2Layer 1Layer 0 query q entry point greedy, ef = 1 beam, ef = efSearch
HNSW search: greedy hops on sparse upper layers find the right region quickly; layer 0 (all nodes) is searched with a beam of size efSearch.
ParameterEffectTypical values
MMax links per node (2M on layer 0). Higher: better recall, especially at high dimension, more memory, slower build8–64; 16 is a common default (pgvector default 16)
efConstructionBeam width during insertion. Higher: better graph quality, slower build. No query-time cost64–512 (pgvector default 64)
efSearchBeam width at query time, must be ≥ k. The main recall/latency dial, tunable per query40–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.

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.

1. Coarse quantizer (k-means, nlist centroids) c1c2c3c4… c_nlist query probes nprobe = 2 nearest lists 2. Inverted lists: each vector stored as PQ code of residual r = x − c id 17: [12 201 7 88]id 42: [3 19 250 61]id 93: [77 4 130 9] id 5: [140 2 66 31]id 61: [9 9 201 180]id 88: [55 230 1 17] 3. PQ: split D dims into m sub-vectors, 256-centroid codebook each → m bytes per vector sub 1 → 12sub 2 → 201sub 3 → 7sub 4 → 88 4. Asymmetric distance (ADC) Per query & list: table T[j][k] = ‖q_res,j − codebook_j[k]‖² (m × 256 floats) Distance to code [12 201 7 88] = T[1][12]+T[2][201]+T[3][7]+T[4][88] m lookups, no decompression; optionally rerank top hits with full vectors
IVF-PQ: the coarse quantizer limits which lists are scanned; inside each list vectors are compact PQ codes, scored with per-query lookup tables.

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.

Recall–latency–memory trade-offs

IndexMemory per vector (768-d)Recall potentialQuery latencyBuild / updatesSweet spot
Flat (exact)3 KB (float32)100%Linear in N; ms at 100k, ~0.1–1 s at 10M (CPU)Trivial / trivialSmall corpora, heavily filtered subsets, ground truth for evals
HNSW (float)3 KB + ~100–300 B graphVery high (95–99%+)~1 ms-class at millionsSlow build; deletes via tombstonesDefault for up to ~10s of millions in RAM
HNSW + int8/binary + rescore0.1–0.8 KB + graphHigh with rescoringFaster than floatAs HNSWSame, at lower RAM cost
IVF-Flat3 KBHigh with enough nprobems to tens of msNeeds k-means training; easy appendsModerate scale, cheaper build than HNSW
IVF-PQ~16–128 BModerate; high with rerankms; very high throughput, GPU-friendlyTraining; easy appendsHundreds of millions to billions, memory-bound
DiskANN / Vamana~32–128 B in RAM, full on SSDVery highFew ms (SSD reads)Heavy build; Fresh variants handle streamingBillion-scale on one node, cost-sensitive
ScaNNQuantized codes + optional fullVery high for MIPSVery fast on CPUTrainingLarge-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.

Interview angle

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

Common mistake

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.

Go deeper

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.

SystemWhat it isNotable traits
pgvectorPostgres extensionHNSW 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 / OpenSearchLucene-based search engines with vector fieldsMature BM25, analyzers, aggregations; HNSW-based kNN with quantization options; native hybrid queries. OpenSearch can also use Faiss engines Elastic docs OpenSearch docs
PineconeFully managed (serverless) vector DBNo ops; namespaces for tenant isolation; metadata filtering; sparse-dense hybrid Pinecone docs
WeaviateOpen-source (Go), cloud offeringHNSW, built-in hybrid BM25 + vector fusion, native multi-tenancy, model integrations Weaviate docs
QdrantOpen-source (Rust), cloud offeringFilterable HNSW with payload indexes, quantization (scalar, binary, PQ), sparse vectors, multi-vector Qdrant docs
Milvus (Zilliz)Open-source distributed vector DBDisaggregated storage and compute, many index types (HNSW, IVF variants, DiskANN, GPU indexes), built for very large scale Milvus docs
LanceDBEmbedded / serverless, on the Lance columnar formatRuns in-process or on object storage; IVF-PQ and other indexes; good for multimodal and data-lake workflows LanceDB docs
turbopufferManaged search on object storageState in object storage with NVMe/memory caching; vector + BM25 full-text + hybrid; namespaces, aimed at many-tenant workloads at low cost turbopuffer docs
OthersVespa, Chroma, Redis, MongoDB Atlas, cloud-native (Vertex, Azure AI Search, OpenSearch Serverless), Faiss as a libraryPick by ecosystem fit

Choosing criteria

Interview angle

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.

May be out of date

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.

Go deeper

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

Common mistake

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.

Go deeper

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 typeHow it scoresQualityLatency / cost
Pointwise cross-encoderIndependent score per (q, d)HighLow–moderate; batchable
Late interaction (ColBERT) as rerankerMaxSim over token vectorsGoodLow if doc vectors stored
LLM pointwisePrompt "rate relevance 0–3" or use yes/no token logprobHigh; follows instructions ("prefer recent docs")High; parallelisable
LLM listwiseGive the LLM the query and a list of passages, ask for a permutation; sliding windows for long listsVery high in studiesHighest; sequential windows; position bias
PairwiseCompare two passages at a timeHighO(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.

Interview angle

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

Go deeper

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.

TechniqueMechanismHelps withCost / risk
Conversational rewritingRewrite the latest turn into a standalone query using chat historyFollow-ups, pronouns ("it", "that policy")One small LLM call; essential for chat
Query expansion / multi-queryGenerate several paraphrases; retrieve for each; fuse with RRFVocabulary mismatch, recallN× retrieval cost; drift if paraphrases wander
HyDELLM writes a hypothetical answer document; embed that and searchShort queries vs long passages (asymmetric match)LLM latency; hallucinated specifics can mislead
Query2docAppend an LLM-generated pseudo-document to the query (works for sparse and dense)Recall, especially BM25As HyDE
DecompositionSplit multi-part or multi-hop questions into sub-questions, retrieve per sub-question, possibly sequentiallyComparisons, multi-hop ("CEO of the company that acquired X")More calls; needs orchestration
Step-back promptingAsk a more general question first ("what are the principles of X?") and retrieve for bothQuestions needing background or principlesExtra retrieval
RoutingClassify the query and send it to the right index, tool or strategy (SQL vs docs vs web; no-retrieval for chit-chat)Heterogeneous sources, costRouter 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 queryPrecision; queries with time or entity constraintsWrong 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.

Common mistake

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.

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.

User question │ ▼ ┌───────────────────────── agent loop (max N steps, token budget) ─────────────────────────┐ │ think: what do I need? ──► tool call: search(query, filters) ──► read top results │ │ ▲ │ │ │ └──────────── not enough evidence / contradictions ◄────────────────┘ │ └────────────────────────────────── enough evidence ────────────────────────────────────────┘ │ ▼ Answer with citations to the passages actually read

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

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.

Interview angle

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.

May be out of date

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.

Go deeper

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.

Common mistake

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.

Interview angle

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

Intuition

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:

$$\text{DCG@}k = \sum_{i=1}^{k} \frac{2^{\text{rel}_i}-1}{\log_2(i+1)}, \qquad \text{nDCG@}k = \frac{\text{DCG@}k}{\text{IDCG@}k}$$

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

MetricQuestion it answersHow it is typically computed
Faithfulness / groundednessIs 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 relevanceDoes the answer address the question?LLM judge, or generate questions from the answer and compare their embeddings to the original
Context precisionAre the relevant chunks ranked high among those retrieved?Per-chunk relevance judgement, rank-weighted
Context recallDoes the context contain everything needed for the reference answer?Split the reference answer into statements; check each is attributable to the context
Answer correctnessDoes it match the reference answer?LLM judge vs reference, or exact/F1 match for short answers
Citation accuracyDo cited sources actually support the cited claims?Check each (claim, citation) pair
Abstention qualityDoes 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
Interview angle

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

Go deeper

Failure modes and debugging

The first debugging question for any bad answer is attribution: was the right evidence in the context window?

Bad answer │ ├─ Does the answer exist in the corpus at all? no ─► content gap / ingest failure (connector, deletion, ACL) │ yes ├─ Was it parsed correctly (look at the raw chunk text)? no ─► parsing: tables, OCR, reading order │ yes ├─ Is it in a single self-contained chunk? no ─► chunking: size, boundaries, add context / parent expansion │ yes ├─ Is that chunk in the top-100 candidates? no ─► retrieval: embedder domain, add BM25/hybrid, query rewrite, filters too strict, ANN recall │ yes ├─ Is it in the final top-k sent to the LLM? no ─► ranking: reranker, fusion weights, k too small │ yes ├─ Did the LLM use it correctly? no ─► generation: prompt, conflicting chunks, lost-in-the-middle, model capability │ yes └─ Is the citation right? no ─► attribution: citation formatting, post-hoc verification

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.

SymptomLikely causeFix
Misses on product codes, names, error stringsDense-only retrievalAdd BM25 hybrid; check tokenisation/analyzers
Retrieves the right document, wrong sectionChunks too big or too small; lacking contextStructure-aware chunking; contextual prefixes; rerank
Answers from an outdated versionStale chunks not deleted; no recency signalDelete-on-update; version metadata; recency boost or filter
Confident answer when nothing relevant existsNo sufficiency check; prompt pushes the model to answerReranker score threshold; explicit abstain instruction; unanswerable cases in evals
Wrong numbers from tablesTable flattened during parsingLayout-aware parsing; keep tables whole with headers
Good on tests, poor in productionSynthetic golden set unlike real queriesMine real queries from logs; stratify
Filtered queries return few or no resultsPost-filtering with selective filtersFilter-aware ANN, iterative scans, brute force for small subsets, partitioning
Follow-up questions failNo conversational rewritingStandalone-query rewrite using history
Contradictory chunks lead to a blended answerMultiple versions or sources disagreeDedup by version; surface the conflict; source-priority metadata
Model follows instructions found inside documentsIndirect prompt injectionTreat retrieved text as data (delimiters, instructions), limit tool permissions, sanitise sources, monitor
Latency spikesRerank over too many candidates; multi-query fan-out; cold cachesCap candidates; route simple queries to the cheap path; cache embeddings and results
Common mistake

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.

Intuition

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.

Go deeper

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.