<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dicardo9</title>
    <description>The latest articles on DEV Community by Dicardo9 (@dicardo9).</description>
    <link>https://dev.to/dicardo9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4124721%2F1d21e823-1507-4424-b3d3-d2b0862f0d49.png</url>
      <title>DEV Community: Dicardo9</title>
      <link>https://dev.to/dicardo9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dicardo9"/>
    <language>en</language>
    <item>
      <title>Practice RAG Retrieval Metrics Offline — A Tiny Stdlib Eval Loop (Synthetic Data)</title>
      <dc:creator>Dicardo9</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:53:29 +0000</pubDate>
      <link>https://dev.to/dicardo9/practice-rag-retrieval-metrics-offline-a-tiny-stdlib-eval-loop-synthetic-data-dm3</link>
      <guid>https://dev.to/dicardo9/practice-rag-retrieval-metrics-offline-a-tiny-stdlib-eval-loop-synthetic-data-dm3</guid>
      <description>&lt;p&gt;If you are learning RAG, you eventually hit the same wall:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I can chat with my docs… but I have no idea if retrieval is actually improving.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most tutorials jump straight to embeddings, vector DBs, and LLM judges. That is fine for demos. It is a poor first step for &lt;strong&gt;understanding retrieval metrics&lt;/strong&gt;, because too many moving parts hide what the numbers mean.&lt;/p&gt;

&lt;p&gt;This post is a walkthrough of a &lt;strong&gt;deliberately tiny&lt;/strong&gt; retrieval eval loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synthetic docs + QA gold labels&lt;/li&gt;
&lt;li&gt;a toy lexical retriever (intentionally dumb)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;precision@k&lt;/code&gt; / &lt;code&gt;recall@k&lt;/code&gt; / &lt;code&gt;hit@k&lt;/code&gt; printed to the terminal&lt;/li&gt;
&lt;li&gt;Python &lt;strong&gt;stdlib-first&lt;/strong&gt; — no model weights, no API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest scope up front:&lt;/strong&gt; this is a learning harness, not a production RAG stack. Scores on synthetic data are &lt;strong&gt;not&lt;/strong&gt; business scores.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why start with retrieval metrics (not the generator)?
&lt;/h2&gt;

&lt;p&gt;RAG quality has at least two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt;  did we pull the right documents / chunks?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generation&lt;/strong&gt;  given that context, did the model answer faithfully?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;People often jump to “faithfulness” or answer correctness” first. Those are valuable — and expensive. They need a generator, a judge prompt, and careful sampling.&lt;/p&gt;

&lt;p&gt;Retrieval metrics are cheaper and clearer for beginners:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Plain-English meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Precision@k&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Of the top-k retrieved items, what fraction are gold?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recall@k&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Of all gold items, what fraction appear in top-k?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hit@k&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Did top-k contain &lt;em&gt;at least one&lt;/em&gt; gold item? (0/1 per query)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Hit@k is low, your generator never had a fair chance. Fix retrieval first.&lt;/p&gt;

&lt;p&gt;In a minimal kit, &lt;strong&gt;faithfulness is N/A&lt;/strong&gt;: there is no generator. Do not invent a fake 0.95 to look complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The smallest useful eval loop
&lt;/h2&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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..."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;QA gold&lt;/strong&gt; (one JSON object per line):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"qid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"q1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How many annual leave days?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gold_doc_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"d1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gold_answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15 days"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can later swap in your own &lt;strong&gt;desensitized&lt;/strong&gt; files with the same fields. Do not dump customer names, internal URLs, or secrets into a shareable folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  Toy retriever (on purpose)
&lt;/h3&gt;

&lt;p&gt;A lexical / overlap scorer is enough to practice the &lt;strong&gt;eval plumbing&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tokenize question + docs&lt;/li&gt;
&lt;li&gt;score by overlap&lt;/li&gt;
&lt;li&gt;return top-k ids&lt;/li&gt;
&lt;li&gt;score against &lt;code&gt;gold_doc_ids&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It will look “too good” or “too brittle” on tiny synthetic sets. That is fine. The goal is to learn the &lt;strong&gt;metric definitions and failure modes&lt;/strong&gt;, not to ship a search engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to read the numbers without lying to yourself
&lt;/h2&gt;

&lt;p&gt;Suppose &lt;code&gt;k=3&lt;/code&gt; and each question has exactly one gold doc.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precision@3&lt;/strong&gt; often sits near &lt;code&gt;0.33&lt;/code&gt; even when you “hit — because only 1 of 3 slots can be gold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall@3&lt;/strong&gt; can look perfect (&lt;code&gt;1.0&lt;/code&gt;) while the ranking is still noisy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit@3&lt;/strong&gt; collapses to “did we get the right doc somewhere in top-3?” — useful, but easy questions make everyone score &lt;code&gt;1.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common misreads:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;High synthetic scores ≠ production readiness.&lt;/strong&gt; Tiny fake corpora overfit your toy retriever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Precision dropped while recall rose.&lt;/strong&gt; You may be retrieving wider and noisier. Whether that is “better” depends on whether you fear misses more than hallucinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing moved when you toggled a knob.&lt;/strong&gt; Your questions may not discriminate. Add harder / boundary cases before more tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faithfulness filled in by hand.&lt;/strong&gt; If you did not run a generator+judge, leave it N/A.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule of thumb for experiments: &lt;strong&gt;change one variable at a time&lt;/strong&gt;, keep the same dataset and same &lt;code&gt;k&lt;/code&gt;, then compare.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chunking and rerank: when they matter (and when they don't)
&lt;/h2&gt;

&lt;p&gt;Once the single-path loop makes sense, the next educational step is &lt;strong&gt;controlled contrasts&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Knob&lt;/th&gt;
&lt;th&gt;What you are testing&lt;/th&gt;
&lt;th&gt;Honest caveat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chunk strategy (&lt;code&gt;none&lt;/code&gt; / &lt;code&gt;fixed&lt;/code&gt; / &lt;code&gt;sliding&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Does splitting long docs help or cut answers apart?&lt;/td&gt;
&lt;td&gt;More chunks ≠ better; can add noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toy rerank on/off&lt;/td&gt;
&lt;td&gt;Does reordering top candidates change Hit/P/R?&lt;/td&gt;
&lt;td&gt;A demo toggle is &lt;strong&gt;not&lt;/strong&gt; a production cross-encoder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Heuristics (not promises):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long docs, answers buried mid/late → try sliding / size first.&lt;/li&gt;
&lt;li&gt;First-pass recall OK but users always click result #2 → consider a &lt;strong&gt;real&lt;/strong&gt; reranker later.&lt;/li&gt;
&lt;li&gt;Pretty metrics, ugly production  usually a &lt;strong&gt;eval-set gap&lt;/strong&gt;, not “need a bigger model.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With few questions (e.g. &amp;lt;30), do not make product decisions on a &lt;code&gt;0.02&lt;/code&gt; swing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just use Ragas / TruLens / ?
&lt;/h2&gt;

&lt;p&gt;Those tools are excellent when you already know what you are measuring and can afford API/judge cost.&lt;/p&gt;

&lt;p&gt;A tiny offline harness is for a different moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want to &lt;strong&gt;feel&lt;/strong&gt; P@k / R@k / Hit@k on a laptop&lt;/li&gt;
&lt;li&gt;you do not want to configure a vector DB yet&lt;/li&gt;
&lt;li&gt;you want an empty &lt;code&gt;requirements.txt&lt;/code&gt; (stdlib-first) so the lesson is the metrics, not the dependency graph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built (and what I did not claim)
&lt;/h2&gt;

&lt;p&gt;I packaged the above as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG Eval Lite&lt;/strong&gt; — one-shot loop: synthetic docs/QA → toy retrieve → print mean P@k / R@k / Hit@k- &lt;strong&gt;RAG Eval Pro&lt;/strong&gt;  same idea plus chunk-strategy switches, a toy rerank toggle, and a metrics table (&lt;code&gt;md&lt;/code&gt;/&lt;code&gt;csv&lt;/code&gt;) with short “how to read the table” notes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not included / not claimed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not a production RAG platform&lt;/li&gt;
&lt;li&gt;not real-corpus benchmarks&lt;/li&gt;
&lt;li&gt;no model weights, no API keys, no scraped course content&lt;/li&gt;
&lt;li&gt;faithfulness remains &lt;strong&gt;N/A&lt;/strong&gt; unless &lt;em&gt;you&lt;/em&gt; wire a generator yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a short discussion thread on r/LocalLLaMA if you want community critique on metric defaults:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/LocalLLaMA/comments/1wg4gwm/tiny_stdlib_rag_retrieval_eval_harness_synthetic/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/LocalLLaMA/comments/1wg4gwm/tiny_stdlib_rag_retrieval_eval_harness_synthetic/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Soft CTA  packs (optional)
&lt;/h2&gt;

&lt;p&gt;If you want the ready-made kits instead of rebuilding from scratch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lite ($2.99)&lt;/strong&gt;  minimal retrieval eval loop: &lt;a href="https://payhip.com/b/cmjJk" rel="noopener noreferrer"&gt;https://payhip.com/b/cmjJk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro ($11.99)&lt;/strong&gt; — chunk / toy-rerank harness + metrics table notes: &lt;a href="https://payhip.com/b/UdQ0M" rel="noopener noreferrer"&gt;https://payhip.com/b/UdQ0M&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Author: &lt;strong&gt;Dicardo9 / Moxuan Ding&lt;/strong&gt;. Paid digital packs on Payhip; I made them. Educational use first — treat synthetic scores as practice, not as proof for production.&lt;/p&gt;

&lt;p&gt;If you already have an eval set: what &lt;code&gt;k&lt;/code&gt; and Hit@k vs Recall@k defaults do you actually use day to day? Critique welcome.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>python</category>
      <category>machinelearning</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
