Skip to content

Build1 publisher3 min readPublished

A substring-match scorer made a weekend RAG build look 13 points worse than it was

Exact-substring scoring put a developer's 700-line RAG tool at a 65% retrieval hit-rate at k=3, 13 points below what a token-overlap scorer found. The bug also made extra retrieved chunks look worthless, and a 20-question test set added 15 points of noise.

The Engineer · Build desk

Illustration accompanying A substring-match scorer made a weekend RAG build look 13 points worse than it was

What happened

  • A developer built a roughly 700-line RAG command-line tool on Bun, TypeScript, SQLite with sqlite-vec, Voyage embeddings and Claude, and called it a weekend's work.
  • The 918-question dataset does not label which passage answers each question, so a retrieval counted as a hit if the answer string appeared in a retrieved chunk.
  • Replacing exact-substring matching with an 80% content-word overlap test raised the measured hit-rate at k=3 from 65% to 78%.
  • A 20-question run measured 50% at k=3 where 100 questions with the same seed and code measured 65%.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision The scoring bug nearly changed a shipped default, because the flat curve argued for retrieving fewer chunks and the corrected metric shows that cut would have lowered hit-rate.
  • constraint Hand-sized eval sets cannot resolve the few-point differences that choosing k produces, so tuning decisions made on them are decisions made on noise.
  • capability Showing retrieved-but-uncited chunks on every query lets a developer spot weak retrieval during ordinary use, before any eval harness exists.

The miss that exposed the scorer was a question about Liechtenstein [5]. The answer key said "Switzerland and Austria." The top-ranked chunk described a country "bordered by Switzerland to its west and by Austria to its east" [5]. Retrieval had found the right passage. The substring check failed it because the exact phrase never appears in one piece [5].

A second bug had the same shape. The corpus wrote "around 60,000" and the answer key wrote "60000". Stripping punctuation split the first into two tokens that could never match the second [7]. Joining digit groups before normalizing recovered one more point [7]. That squares with the corrected curve: 78% after the overlap fix, plus one, is the 79% reported at k=3 [13].

The k curve is where the bug nearly changed the product. Retrieval pulls the top k chunks by L2 distance [3], so the top 10 for any question always contain its top 3. On a fixed question set, hit-rate can only rise with k [14]. Under the substring metric it rose two points between k=3 and k=10. Under the fixed metric it rose four [15]. "The broken metric didn't just report a wrong number, it pointed me at the wrong decision," the author wrote [16].

Both slopes are small. The 65% starting point matches the 100-question run [9]. If the curves used that set, the broken curve's rise was two questions and the fixed curve's was four [17].

Sample size is the second problem. The post puts the 95% interval at 20 questions at roughly plus or minus 22 points [10]. The normal approximation agrees: 1.96 times the square root of 0.25/20 is about 0.22 [18]. At 100 questions and a 65% rate, the same formula gives about 9 points [19]. Scoring all 918 questions in the dataset would bring it under 3 points [20]. A hit-rate check needs only a question embedding and a vector lookup, with no call to Claude [25]. "If your eval set is small enough to hand-write in an afternoon, it is small enough to tell you whatever you want to hear," the author wrote [23].

The corrected scorer brings its own error. A rule that credits any single chunk holding 80% of the answer's content words [6] will also credit a chunk that names both countries for some unrelated reason. The post does not report checking the new scorer against hand-labelled retrievals. In my view the phrase and digit fixes are correct. I would still treat 79% as an estimate with unknown bias until a few dozen hits are labelled by hand.

The best engineering in the post is one line of output. Each query prints its sources and marks the cited ones with an asterisk [12]. In the Uruguay example, the answer cited two of the three retrieved chunks and ignored the third [12]. The author wrote that this gap "is the first place you see retrieval quality without running an eval at all" [21].

What to watch

  • The outcome of the author's third experiment, hybrid retrieval merging BM25 keyword ranking with vector ranking, measured under the fixed scorer.
  • A hand-labelled sample of hits and misses under the 80% overlap rule, to measure how often it credits the wrong chunk.
  • A rerun of the k=3, 5 and 10 curve on all 918 questions, where the interval narrows to under 3 points.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories