If you are learning RAG, you eventually hit the same wall:
“I can chat with my docs… but I have no idea if retrieval is actually improving.”
Most tutorials jump straight to embeddings, vector DBs, and LLM judges. That is fine for demos. It is a poor first step for understanding retrieval metrics, because too many moving parts hide what the numbers mean.
This post is a walkthrough of a deliberately tiny retrieval eval loop:
- synthetic docs + QA gold labels
- a toy lexical retriever (intentionally dumb)
-
precision@k/recall@k/hit@kprinted to the terminal - Python stdlib-first — no model weights, no API keys
I shipped this as two small paid packs (Lite / Pro) on Payhip. Soft links are at the end. The article itself is meant to stand alone as an educational note.
Honest scope up front: this is a learning harness, not a production RAG stack. Scores on synthetic data are not business scores.
Why start with retrieval metrics (not the generator)?
RAG quality has at least two layers:
- Retrieval did we pull the right documents / chunks?
- Generation given that context, did the model answer faithfully?
People often jump to “faithfulness” or answer correctness” first. Those are valuable — and expensive. They need a generator, a judge prompt, and careful sampling.
Retrieval metrics are cheaper and clearer for beginners:
| Metric | Plain-English meaning |
|---|---|
| Precision@k | Of the top-k retrieved items, what fraction are gold? |
| Recall@k | Of all gold items, what fraction appear in top-k? |
| Hit@k | Did top-k contain at least one gold item? (0/1 per query) |
If Hit@k is low, your generator never had a fair chance. Fix retrieval first.
In a minimal kit, faithfulness is N/A: there is no generator. Do not invent a fake 0.95 to look complete.
The smallest useful eval loop
Conceptually:
docs.jsonl ─► toy retriever ──► top-k doc ids
qa.jsonl ──► gold_doc_ids ──► compare ──► P@k / R@k / Hit@k```
### Example data shapes
**Document** (one JSON object per line):
```json
{"doc_id": "d1", "title": "Leave policy", "text": "Annual leave is 15 days..."}
QA gold (one JSON object per line):
{"qid": "q1", "question": "How many annual leave days?", "gold_doc_ids": ["d1"], "gold_answer": "15 days"}
You can later swap in your own desensitized files with the same fields. Do not dump customer names, internal URLs, or secrets into a shareable folder.
Toy retriever (on purpose)
A lexical / overlap scorer is enough to practice the eval plumbing:
- tokenize question + docs
- score by overlap
- return top-k ids
- score against
gold_doc_ids
It will look “too good” or “too brittle” on tiny synthetic sets. That is fine. The goal is to learn the metric definitions and failure modes, not to ship a search engine.
How to read the numbers without lying to yourself
Suppose k=3 and each question has exactly one gold doc.
-
Precision@3 often sits near
0.33even when you “hit — because only 1 of 3 slots can be gold. -
Recall@3 can look perfect (
1.0) while the ranking is still noisy. -
Hit@3 collapses to “did we get the right doc somewhere in top-3?” — useful, but easy questions make everyone score
1.0.
Common misreads:
- High synthetic scores ≠ production readiness. Tiny fake corpora overfit your toy retriever.
- Precision dropped while recall rose. You may be retrieving wider and noisier. Whether that is “better” depends on whether you fear misses more than hallucinations.
- Nothing moved when you toggled a knob. Your questions may not discriminate. Add harder / boundary cases before more tuning.
- Faithfulness filled in by hand. If you did not run a generator+judge, leave it N/A.
Rule of thumb for experiments: change one variable at a time, keep the same dataset and same k, then compare.
Chunking and rerank: when they matter (and when they don't)
Once the single-path loop makes sense, the next educational step is controlled contrasts:
| Knob | What you are testing | Honest caveat |
|---|---|---|
Chunk strategy (none / fixed / sliding) |
Does splitting long docs help or cut answers apart? | More chunks ≠ better; can add noise |
| Toy rerank on/off | Does reordering top candidates change Hit/P/R? | A demo toggle is not a production cross-encoder |
Heuristics (not promises):
- Long docs, answers buried mid/late → try sliding / size first.
- First-pass recall OK but users always click result #2 → consider a real reranker later.
- Pretty metrics, ugly production usually a eval-set gap, not “need a bigger model.”
With few questions (e.g. <30), do not make product decisions on a 0.02 swing.
Why not just use Ragas / TruLens / ?
Those tools are excellent when you already know what you are measuring and can afford API/judge cost.
A tiny offline harness is for a different moment:
- you want to feel P@k / R@k / Hit@k on a laptop
- you do not want to configure a vector DB yet
- you want an empty
requirements.txt(stdlib-first) so the lesson is the metrics, not the dependency graph
When you outgrow it, replace the toy retriever with your real pipeline and keep the same gold JSONL + metric definitions. That migration path is the point.
What I built (and what I did not claim)
I packaged the above as:
-
RAG Eval Lite — one-shot loop: synthetic docs/QA → toy retrieve → print mean P@k / R@k / Hit@k- RAG Eval Pro same idea plus chunk-strategy switches, a toy rerank toggle, and a metrics table (
md/csv) with short “how to read the table” notes
Not included / not claimed:
- not a production RAG platform
- not real-corpus benchmarks
- no model weights, no API keys, no scraped course content
- faithfulness remains N/A unless you wire a generator yourself
There is also a short discussion thread on r/LocalLLaMA if you want community critique on metric defaults:
Soft CTA packs (optional)
If you want the ready-made kits instead of rebuilding from scratch:
- Lite ($2.99) minimal retrieval eval loop: https://payhip.com/b/cmjJk
- Pro ($11.99) — chunk / toy-rerank harness + metrics table notes: https://payhip.com/b/UdQ0M
Author: Dicardo9 / Moxuan Ding. Paid digital packs on Payhip; I made them. Educational use first — treat synthetic scores as practice, not as proof for production.
If you already have an eval set: what k and Hit@k vs Recall@k defaults do you actually use day to day? Critique welcome.
Top comments (1)
Starting with a deliberately dumb retriever is good teaching - when retrieval is the only moving part, precision@k actually means something. The honest scope note deserves a nod too: synthetic scores tell you whether your loop measures anything, not whether your production retrieval is good. The natural next step that keeps the spirit: swap one real embedding model in behind the same harness and watch the same three numbers move.