Overview
The generator writes confidently from whatever the retriever hands it, so retrieval sets the ceiling for a RAG system. These rules cover which retrievers to run, how to merge them, how to filter, and how to size the candidate pool. For chunk preparation see rag-chunking; for the step after retrieval see rag-reranking.
Run dense and sparse retrieval in parallel, then fuse
Dense embeddings handle paraphrase and meaning. Sparse retrieval (BM25, Postgres full-text, SPLADE) handles exact identifiers, rare names, version numbers, and one-word queries, which dense models fail on predictably (EADDRINUSE, pg_dump --jobs=4, SKUs). Query both with the same filters and merge the two top-k lists. Do not run them in sequence; a keyword filter after the dense step throws away candidates before the merge.
Reciprocal Rank Fusion needs no score calibration. Each document scores 1 / (k + rank) per list, summed across lists, with rank starting at 1.
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)k = 60is the common default; lower values weight top ranks more. Qdrant’s server-side RRF documents a default of 2, so passkexplicitly when you move between systems.- Weaviate hybrid queries default to relative score fusion (v1.24+) and use
alphafrom 0 (keyword only) to 1 (vector only);rankedFusionis the RRF-style alternative. - Pinecone supports one index holding dense and sparse vectors with a client-side
alpha, or separate indexes fused client-side. - Confirm on your golden set that hybrid beats dense alone; the gain concentrates on identifier-heavy queries. See rag-eval.
For the keyword arm inside Postgres, see postgres-full-text-search.
Filter on metadata before the vector step
Filter by tenant_id, lang, doc_type, and date range inside the query, not on the merged list. Post-filtering a fixed top-k can return nothing when the filter is selective.
results = collection.query(
query_embeddings=[query_vec],
where={"$and": [{"tenant_id": "t_123"}, {"updated_ts": {"$gte": 1767225600}}]}, # 2026-01-01 UTC
n_results=50,
)- ChromaDB range operators (
$gt,$gte,$lt,$lte) take numbers only, so store dates as epoch integers, and combine several conditions with$and. - pgvector applies a
WHEREclause after an approximate index scan. Since 0.8.0, sethnsw.iterative_scantostrict_orderorrelaxed_orderso the scan continues until enough rows pass the filter. - Check how your store filters before you trust a selective filter; see rag-vector-databases.
Retrieve broad, rerank narrow
Take 20 to 50 candidates from each retriever, merge to about 50, and let a reranker choose the 5 to 20 chunks that enter the prompt. Skipping the reranker stage costs borderline queries; see rag-reranking. Sweep k against recall@k on the golden set and pick the point where recall plateaus; setting k by feel under-retrieves hard queries and overpays on easy ones.
Route queries that do not need hybrid
If logs show many exact-identifier lookups, send those to BM25 alone and skip the embedding call. Send conceptual queries to dense, ambiguous ones to hybrid. Add a router only when the saved embedding calls outweigh its maintenance; for low-volume systems, always run hybrid.
Expand the query when its wording differs from the corpus
Multi-query expansion asks a small fast model (for example claude-haiku-5-5 or a local model) for 3 to 5 paraphrases, retrieves on each, and merges with RRF. HyDE has the model draft a plausible answer and embeds that instead of the question.
- HyDE helps short, vague queries (“what is X”). It hurts long, precise ones (“Postgres 17 JIT compile flags”); skip it there.
- Both add a model call to every query. Keep them only where
recall@kimproves on the golden set.
Diagnose recall before precision
When answers are wrong, check recall@k first. Low recall points at chunking or the embedding model (rag-chunking, embeddings). High recall with wrong chunks in the prompt points at the reranker. High recall with the right chunks and a wrong answer points at generation or citations (rag-citations). Most “the model is hallucinating” reports are retrieval misses.