RAG: chunking, embeddings and hybrid retrieval
In one line
Retrieval-augmented generation (RAG) answers questions from your own data by finding the most relevant chunks of documents and putting them in the prompt; the answer can only be as good, as fresh and as permission-safe as the retrieval step.
What it is
An LLM only knows what was in its training data. RAG gives it your data at question time: an ingestion pipeline splits documents into chunks, turns each chunk into an embedding vector, and stores the text, vector and metadata. At query time the system retrieves the best chunks for the question and asks the model to answer using only those chunks, with citations.
Retrieval quality decides answer quality. Vector (semantic) search finds chunks with similar meaning even when words differ ('refund' vs 'money back'). Keyword search (BM25 or Postgres full-text) finds exact terms that embeddings handle badly: order ids, error codes, product names, section numbers. Hybrid retrieval runs both and merges the lists, usually with Reciprocal Rank Fusion (RRF), and often a reranker model reorders the top results.
RAG is cheaper and fresher than fine-tuning for knowledge: update a document and the next answer reflects it after re-indexing, with no training run. Fine-tuning is for changing behaviour or style, not for adding facts that change.
Why interviewers ask
RAG is the most common LLM backend pattern, so 'design a chatbot over company documents' and AI twists on search are frequent prompts. Interviewers look for the full pipeline (ingestion, chunking, embeddings, hybrid retrieval, reranking, prompt assembly), the two failure modes that hurt real products (bad retrieval and cross-tenant leaks), freshness when documents change, and a measurable way to know whether retrieval works, such as recall@k on a labelled set.
How it works
- 1Ingest: when a document is uploaded or changed, publish an event; an async worker extracts text (PDF, HTML, DOCX, OCR for scans), cleans it and keeps structure such as headings, tables and page numbers.
- 2Chunk: split into pieces of roughly 300 to 800 tokens with 10 to 20% overlap, preferring natural boundaries (headings, paragraphs). Attach metadata to each chunk: tenant_id, document_id, title, section path, page, access-control list, updated_at. Prepending the document title and section heading to each chunk's text improves retrieval.
- 3Embed: call an embedding model in batches (for example 100 chunks per request) to get vectors of a few hundred to a few thousand dimensions. Store the model name with each vector: changing the embedding model means re-embedding everything, because vectors from different models are not comparable.
- 4Store: text, vector and metadata, either in Postgres with pgvector (a tsvector column with a GIN index for keywords plus an HNSW index for vectors) or in a search engine such as OpenSearch, Elasticsearch, Vespa or Qdrant. Enforce tenant isolation in the store itself, for example Postgres Row-Level Security, not only in application WHERE clauses.
- 5Query understanding: optionally rewrite the user's question into a standalone search query (resolve 'it' and 'that order' from the chat history), detect language, and extract filters (product, date range).
- 6Hybrid retrieval: run vector search (top 50) and keyword search (top 50) with the same tenant and permission filters, then merge with RRF: score = sum over lists of 1 / (60 + rank). RRF needs no score normalisation, which is why it is the common default.
- 7Rerank: send the top 30 to 50 merged chunks to a cross-encoder reranker (a model that reads question and chunk together) and keep the best 5 to 10. This usually gives a large precision gain for tens of milliseconds.
- 8Assemble the prompt: instructions to answer only from the provided context and say 'I don't know' otherwise, then the chunks with ids, then the question. Ask for citations by chunk id and check that cited ids were actually provided.
- 9Keep it fresh: on document update or delete, re-chunk and replace that document's chunks atomically (delete old chunk ids, insert new) and invalidate cached answers that used them.
- 10Evaluate: keep a set of 50 to 500 real questions labelled with the chunks that should be found; track recall@10 for retrieval and faithfulness for answers on every change to chunking, embeddings or ranking.
Worked example · RRF merging for 'error E1043 when uploading invoice'
- Vector search returns: chunk A 'Upload failures and how to fix them' (rank 1), chunk B 'Invoice formats we accept' (rank 2), chunk C 'Error codes E1000 to E1099' (rank 9).
- Keyword search for 'E1043 upload invoice' returns: chunk C (rank 1, it contains the exact code E1043), chunk A (rank 4). Chunk B does not appear.
- RRF with k = 60: A = 1/61 + 1/64 = 0.0164 + 0.0156 = 0.0320. C = 1/69 + 1/61 = 0.0145 + 0.0164 = 0.0309. B = 1/62 = 0.0161.
- Merged order: A, C, B. Chunk C, the exact answer, was rank 9 in vector search alone and might have been cut off by a top-5 vector-only system. Hybrid lifts it to second.
- The reranker reads the question with each chunk and moves C to first place, because it directly explains E1043. The model receives C and A and answers with a citation to C.
- On the team's 200-question eval set, recall@10 was 71% vector-only, 64% keyword-only, 86% hybrid, 91% hybrid plus reranker. These measurements, not intuition, justify the extra components.
Picture it
RAG is an open-book exam. The model is a student who writes well but does not know your company. Retrieval is the act of finding the right pages in the book and putting them on the desk. If the wrong pages are on the desk, a fluent student still writes a fluent wrong answer; if pages from another student's book are on the desk, you have a privacy incident.
Reference code
-- Hybrid search in Postgres + pgvector. RLS on chunks filters by tenant automatically.
-- $1 = query embedding, $2 = query text
WITH vec AS (
SELECT id, RANK() OVER (ORDER BY embedding <=> $1) AS r
FROM chunks
ORDER BY embedding <=> $1
LIMIT 50
),
kw AS (
SELECT id, RANK() OVER (ORDER BY ts_rank(tsv, q) DESC) AS r
FROM chunks, websearch_to_tsquery('english', $2) AS q
WHERE tsv @@ q
ORDER BY ts_rank(tsv, q) DESC
LIMIT 50
)
SELECT c.id, c.document_id, c.content,
COALESCE(1.0 / (60 + vec.r), 0) + COALESCE(1.0 / (60 + kw.r), 0) AS rrf
FROM vec
FULL OUTER JOIN kw USING (id)
JOIN chunks c USING (id)
ORDER BY rrf DESC
LIMIT 10;Key patterns
Hybrid retrieval with RRF
When: Queries mix meaning and exact identifiers
How: Run keyword and vector search in parallel with the same filters, merge by 1/(k + rank) with k around 60, then rerank the top 30 to 50.
Permission filtering inside retrieval
When: Multi-tenant or per-document access control
How: Apply tenant and ACL filters in the search query itself (RLS, filtered vector search), never by removing chunks after the model has seen them. Retrieve with the end user's identity.
Contextual chunk headers
When: Chunks lose meaning out of context
How: Prepend document title and section path to each chunk before embedding, so 'The limit is 30 days' carries 'Refund policy > Electronics'.
Answer only from context
When: Factual question answering
How: Instruct the model to cite chunk ids and to say it does not know when context is missing; validate citations and show sources to users.
Real-world use
DoorDash Dasher support chatbot
DoorDash described a RAG system over its knowledge base for delivery partners, with an LLM guardrail that checks responses for grounding before they are shown and an LLM judge for offline quality evaluation.
Notion AI and workspace Q&A
Workspace assistants such as Notion's answer questions over a user's pages; the hard parts are respecting each page's permissions at retrieval time and keeping the index fresh as pages are edited all day.
Elasticsearch and OpenSearch hybrid search
Both engines support combining BM25 keyword results with dense-vector kNN results, including reciprocal rank fusion, which lets teams that already run a search cluster add semantic retrieval without a separate database.
Postgres with pgvector
Many startups keep chunks, vectors and full-text indexes in one Postgres database, using Row-Level Security for tenant isolation. It is simple to operate up to tens of millions of chunks before a dedicated engine is needed.
In a project
A Gurugram HR-tech SaaS wants a chatbot that answers employees' questions from their own company's policy documents (leave, reimbursement, travel). 2,000 client companies, about 3 million chunks in total, some questions in Hindi and Hinglish.
- Store chunks in Postgres with pgvector and a tsvector column; enable Row-Level Security on the chunks table with a tenant_id policy, connect as a non-owner role, and set the tenant per transaction with SET LOCAL.
- Add a CI test that queries as tenant A and asserts zero rows from tenant B on every table. It runs forever.
- Chunk policies by heading, about 500 tokens with 75 tokens overlap, and prepend 'Policy title > Section' to each chunk.
- Use a multilingual embedding model so Hindi and Hinglish questions match English policy text; use the 'simple' text-search configuration for non-English keywords because Postgres has no built-in Hindi stemmer.
- Hybrid retrieval (top 50 + top 50, RRF), then a multilingual reranker down to 6 chunks; the prompt requires citations and an 'I could not find this in your policies' answer when nothing relevant is retrieved.
- Ingestion runs on a queue: an upload returns 202 and the document becomes searchable in about a minute; edits replace the document's chunks in one transaction.
- Build a 300-question eval set from real anonymised questions per language and gate every change on recall@10 and answer faithfulness.
Impact: HR ticket volume for policy questions dropped by about 40%, the leak test has caught two RLS regressions in CI before release, and the team can show that hybrid retrieval beats vector-only by 15 points of recall on Hinglish questions.
Trade-offs
Use it when
- Answers must come from private, changing data: policies, product docs, tickets, contracts
- You need citations and the ability to say where an answer came from
- Data changes often enough that fine-tuning would always be out of date
- Different users may see different documents, so knowledge cannot be baked into a model
Avoid it when
- The question is really a structured query ('how many orders did I place last month'): use SQL or an API tool instead of text retrieval
- The whole corpus is small enough to fit in the prompt and is reused often; long context plus prompt caching may be simpler
- You need a change in tone, format or skill rather than knowledge; that is prompt engineering or fine-tuning
- Documents are mostly scanned images or complex tables you have not parsed properly; fix extraction first
Common mistakes
- Vector-only retrieval, which misses exact identifiers such as error codes, SKUs and section numbers.
- Filtering by tenant in application code only, or after retrieval, so one missed WHERE clause puts another customer's text in the prompt.
- Chunks that are too large (dilute the embedding and waste tokens) or too small (lose context), chosen without measuring recall.
- Changing the embedding model without re-embedding the whole corpus, mixing incomparable vectors in one index.
- No plan for document updates and deletes, so the bot keeps quoting a policy that was withdrawn.
- No labelled eval set, so nobody can say whether a chunking or ranking change helped.
- Stuffing 30 chunks into the prompt 'to be safe', which raises cost and often lowers answer quality.
Interview questions
Quick revision
- Pipeline: extract, chunk, embed, store; at query time rewrite, retrieve, rerank, assemble, generate with citations.
- Chunks: about 300-800 tokens, 10-20% overlap, split on headings, title and section prepended.
- Hybrid = keyword (exact ids) + vector (meaning), merged with RRF: sum of 1/(60 + rank).
- Rerank top 30-50 with a cross-encoder; send 5-10 chunks to the model.
- Tenant and ACL filters inside retrieval (RLS, filtered ANN), never after.
- New embedding model means re-embedding everything.
- Updates and deletes replace a document's chunks atomically and invalidate cached answers.
- Measure recall@10 on a labelled set before and after every change.
- Structured questions go to SQL or API tools, not text retrieval.
- Prompt: answer only from context, cite chunk ids, say 'I don't know' otherwise.
Check yourself
1. A user searches for 'GSTR-3B late fee'. Vector search returns general GST articles but misses the page that contains 'GSTR-3B'. What is the most direct fix?
2. With RRF (k = 60), chunk X is rank 1 in vector search and absent from keyword results; chunk Y is rank 3 in both. Which ranks higher?
3. Where should tenant filtering happen in a multi-tenant RAG system?
4. Your team switches to a new embedding model. What must happen?
See it in a full design
These walkthroughs use this lesson.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.