Retrieval
After indexing, the online job is find the chunks the LLM should see. Default: dense top-k. If quality stalls, add filters, keyword hybrid, and optional rerank.
Push filters into the store (Chroma where, pgvector WHERE, Qdrant query_filter). Do not fetch 1000 rows and drop them in Python.
Choosing k
Bigger k is not better: noise fills the window and the model skips the one good paragraph. Freeze k, then change chunking using hit@k in Evaluation.
Metadata filters
“How many leave days does HR allow?” must not retrieve the engineering wiki. Write dept, lang, and source at index time; pass them at query time:
- Chroma:
where={"dept": "hr"} - pgvector:
WHERE dept = 'hr' ORDER BY embedding <=> %s - Qdrant:
Filter(must=[FieldCondition(...)])
Filters are an access boundary, not a leaderboard trick. Tenant IDs must be enforced server-side; the model must not choose them.
Hybrid search (vectors + keywords)
Dense vectors paraphrase well (“return” ≈ “refund”). Keywords win on clause IDs, SKUs, and error codes (KH-8842, ECONNRESET). A simple fuse: take top-n from each list, merge with Reciprocal Rank Fusion.
Real BM25 can be PostgreSQL full-text search or Qdrant sparse vectors (see the Qdrant teaser). Do not add a fancy hybrid before you measure hit@k.
Optional rerank
Retrieve 20, score (question, document) pairs, send 4 to the LLM. A cross-encoder runs on CPU (no GPU required):
If you cannot load a model yet, lexical overlap is a teaching rerank (not a substitute):
LangChain / LlamaIndex / Dify rerank components do the same job: LangChain RAG, Dify Knowledge Base.
Prompt constraints
Accurate retrieval still fails if the prompt invites improvisation. Keep three rules:
- Answer only from the context below.
- Do not invent facts missing from context (including numbers, IDs, dates).
- If evidence is missing, say you do not know and list the
sourcevalues you used.
Generation quality is measured as faithfulness in the next chapter.