Naive RAG is four lines of code: embed the query, top-k nearest neighbours, stuff them in the prompt, generate. It usually scores around 60–75% on a task-specific eval — good enough for a demo, nowhere near good enough for production.
Getting from 75 to 95 is the whole discipline. This page is that delta, in concrete steps, ranked by how much return each move produces.
If your RAG is dense-only (pure vector search), adding BM25 keyword search and fusing with reciprocal rank fusion typically adds 5–15 points of recall at near-zero latency cost. Dense embeddings miss exact-string queries ("error 451", "SKU ABC-1234"); BM25 misses on semantic paraphrase. The fusion gets both.
Minimum RRF:
def rrf(dense_hits, bm25_hits, k=60):
scores = {}
for rank, d in enumerate(dense_hits):
scores[d.id] = scores.get(d.id, 0) + 1 / (k + rank + 1)
for rank, d in enumerate(bm25_hits):
scores[d.id] = scores.get(d.id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: -x[1])
See HybridRetrieval for the full ranking pipeline this wiki uses.
Do this first. Nothing else in this list beats the effort-to-payoff ratio of adding BM25.
"How do I chunk my documents" is the second question every RAG system gets wrong. The trap is either (a) fixed-size 500-token chunks that cut sentences in half, or (b) a section-header splitter that produces one 8000-token chunk and ten 50-token ones.
Working strategy:
{document_id, section_path, preceding_heading}. Your retriever uses the heading path for filtering and the context in the prompt.For structured content (code, tables, JSON) stop before applying text chunking. Treat code blocks and tables as atomic units. A chunk that contains half a table is actively harmful — the model will hallucinate the missing rows.
A cross-encoder reranker over the top-20 candidates typically adds another 3–8 points of nDCG. Cohere Rerank, BGE-reranker-large, or a locally hosted ms-marco-MiniLM-L-12-v2 all work.
Shape:
query → retrieve top 50 (hybrid) → cross-encode rerank → top 5-10 to prompt
Cross-encoders are slow compared to ANN (50ms vs 2ms for 50 candidates on GPU) but the recall gain outweighs it for anything with a human in the loop. Skip reranking only for real-time < 100ms budgets where you'd rather take the recall hit.
User queries are often bad retrieval queries. Three moves that help:
Use one of these, not all three. Stacking transformations adds latency faster than quality.
Most RAG failures in production aren't retrieval failures, they're context failures — the right information is retrieved but it's for the wrong tenant, wrong time period, or wrong document version. Metadata filtering is the defence.
Once you have the right chunks, the prompt determines whether the model uses them. Three rules:
[Source: doc-423, section "Returns Policy"] beats dumping raw text. The model cites correctly only if you tell it how.You cannot improve what you don't measure. Minimum RAG eval:
| Metric | What it catches | Cost |
|---|---|---|
| Retrieval recall@k | Did the right chunk surface? | Free once you have labelled query→doc pairs |
| nDCG@10 | Is the ranking sensible? | Same labels as recall |
| Answer faithfulness (RAGAS) | Does the answer stick to the retrieved context? | One LLM call per eval row |
| Answer correctness | Is the answer actually right? | Human label or LLM-as-judge |
| Latency p95 | Does the pipeline fit your budget? | Free |
Build a frozen eval set of 100–500 queries with gold-labelled relevant chunks and expected answers. Run it on every change. This wiki's own retrieval is evaluated this way — see HybridRetrieval and RetrievalExperimentHarness.
"Just put the whole corpus in a 1M-token context" sounds like it replaces RAG. It doesn't, for three reasons:
Long context is the right answer for some tasks (e.g. summarising a single 500-page contract). RAG is the right answer for retrieval over a knowledge base. They compose — use RAG to narrow to the relevant subset, then long-context for the deep analysis.
When RAG quality regresses in prod, look in this order:
Instrument each layer with a trace span so a single query shows retrieval, rerank, and generation cost/latency. Without this you'll spend hours reproducing intermittent issues by hand.