<?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: fr_trenton02</title>
    <description>The latest articles on DEV Community by fr_trenton02 (@faridgnank02).</description>
    <link>https://dev.to/faridgnank02</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%2F4078961%2F59803128-4123-4d7a-b753-3ff0fdad56cb.jpg</url>
      <title>DEV Community: fr_trenton02</title>
      <link>https://dev.to/faridgnank02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faridgnank02"/>
    <language>en</language>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: Results Appendix (P1–P4)</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:23:40 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8</guid>
      <description>&lt;p&gt;This is the data appendix for &lt;strong&gt;Posts 1–4&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
The narrative and takeaways live in the main posts. This page is pure measurement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eval set:&lt;/strong&gt; 22 questions (P1) → expanded to 31 questions (P2 onward)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Corpus evolution:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;P1/P2: ~3,700 docs (raw threads + code chunks)&lt;/li&gt;
&lt;li&gt;P3/P4: 16,315 docs (distilled threads + bursts + code)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick comparison (same 31-question set)
&lt;/h2&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;Vector P2&lt;/th&gt;
&lt;th&gt;Hybrid P2&lt;/th&gt;
&lt;th&gt;Vector P3&lt;/th&gt;
&lt;th&gt;Hybrid P3&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hybrid + Rerank (P4)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.87&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Hybrid alone never beat pure vector on this corpus.&lt;br&gt;&lt;br&gt;
Hybrid + LLM rerank is the first clear win.&lt;/p&gt;




&lt;h2&gt;
  
  
  P1 — Naive vector baseline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Corpus:&lt;/strong&gt; 3,000 raw issue threads + 687 code chunks&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Embeddings:&lt;/strong&gt; BGE-M3 (1024d), max_seq_length=1024, HNSW cosine&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbers (22 questions)
&lt;/h3&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;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;1.00 (22/22)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.77 (17/22)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Main k=1 misses
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Exact error pastes (&lt;code&gt;TypeError: Object of type int64...&lt;/code&gt;, &lt;code&gt;AttributeError: 'Depends'...&lt;/code&gt;) — ranked 4–5 instead of 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonable_encoder&lt;/code&gt; code chunk outranked by issues &lt;em&gt;about&lt;/em&gt; the function&lt;/li&gt;
&lt;li&gt;API key header implementation (code vs similar issues)&lt;/li&gt;
&lt;li&gt;Paraphrase questions (dependency injection outside routes, custom 404)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pattern:&lt;/strong&gt; Dense search is strong on recall@10 but weak when the query has a sharp lexical signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ops notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ingest wall time ~40 min (GitHub API is the bottleneck)&lt;/li&gt;
&lt;li&gt;BGE-M3 OOM on Apple Silicon fixed by capping &lt;code&gt;max_seq_length=1024&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Python 3.13 + uv editable install issue fixed by pinning 3.12&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  P2 — Hybrid (vector + FTS + RRF)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Corpus:&lt;/strong&gt; Same size as P1, with better comment pagination and symbol-based code IDs&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Eval set:&lt;/strong&gt; Expanded to 31 questions (added exact error pastes + rare identifiers)&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbers
&lt;/h3&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;Vector&lt;/th&gt;
&lt;th&gt;FTS&lt;/th&gt;
&lt;th&gt;Hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;0.48&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Hybrid is &lt;strong&gt;not&lt;/strong&gt; a strict win over vector-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where hybrid helped
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Exact error pastes (e.g. &lt;code&gt;TypeError: int64 is not JSON serializable&lt;/code&gt;) → moved from rank 5 → 1&lt;/li&gt;
&lt;li&gt;Near-duplicate titles (HTTPBearer 403 vs 401) → correctly disambiguated&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where hybrid hurt
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Code-symbol questions (&lt;code&gt;jsonable_encoder&lt;/code&gt;, background tasks, APIKeyHeader) — FTS noise pushed the correct code chunk out of the top ranks&lt;/li&gt;
&lt;li&gt;Strong semantic matches were diluted by diffuse lexical overlap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pattern:&lt;/strong&gt; Hybrid helps when the lexical signal is sharp. It hurts when the signal is diffuse or the answer is a code chunk.&lt;/p&gt;




&lt;h2&gt;
  
  
  P3 — Distillation + Bursting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Corpus rebuilt:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3,002 issue parents (2,542 distilled + 460 raw fallback)&lt;/li&gt;
&lt;li&gt;12,626 burst rows (high-signal comments as separate vector-only documents)&lt;/li&gt;
&lt;li&gt;687 code chunks
→ &lt;strong&gt;16,315 total documents&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Numbers vs P2
&lt;/h3&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;Vector P2&lt;/th&gt;
&lt;th&gt;Vector P3&lt;/th&gt;
&lt;th&gt;Hybrid P2&lt;/th&gt;
&lt;th&gt;Hybrid P3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.52&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.71&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.63&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; Distillation + bursting raised the recall ceiling but made top-of-ranking worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why distillation hurt vector
&lt;/h3&gt;

&lt;p&gt;The embedding is now computed on the clean LLM summary, not the raw thread.&lt;br&gt;&lt;br&gt;
Exact error strings and identifiers that used to live in the vector are gone → error-paste queries drop out of the top-10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why bursting helped
&lt;/h3&gt;

&lt;p&gt;High-signal comments get their own embedding instead of being buried in long threads.&lt;br&gt;&lt;br&gt;
This is what pushed hybrid recall@10 from 0.90 → 0.94.&lt;/p&gt;

&lt;h3&gt;
  
  
  Remaining hard misses (even at k=10)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;“Which function converts arbitrary objects into JSON-compatible data structures?” (&lt;code&gt;jsonable_encoder&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;“Where does FastAPI implement running background tasks after a response is returned?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are natural-language → code-location lookups. Retrieval tuning alone cannot solve them.&lt;/p&gt;




&lt;h2&gt;
  
  
  P4 — Hybrid + LLM Rerank
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Corpus:&lt;/strong&gt; Same as P3 (16,315 docs)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Mode:&lt;/strong&gt; hybrid + LLM reranker over the fused top-20&lt;/p&gt;

&lt;h3&gt;
  
  
  Final numbers
&lt;/h3&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;Vector&lt;/th&gt;
&lt;th&gt;Hybrid&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hybrid + Rerank&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.87&lt;/strong&gt; (27/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.94&lt;/strong&gt; (29/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.94&lt;/strong&gt; (29/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Headline:&lt;/strong&gt; This is the first unqualified win in the series.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hybrid supplies the high-recall pool (0.94 &lt;a class="mentioned-user" href="https://dev.to/10"&gt;@10&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Reranker supplies the ordering (0.90 MRR)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Still missing (by design)
&lt;/h3&gt;

&lt;p&gt;The same two code-location questions.&lt;br&gt;&lt;br&gt;
Reranking can only reorder what is already in the pool. These two never enter the top-20 → they require a different retriever (symbol / grep style), which is the subject of later posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost note
&lt;/h3&gt;

&lt;p&gt;One extra LLM call per query (≈20 candidates). Retrieval legs remain LLM-free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary of the arc so far
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;What improved&lt;/th&gt;
&lt;th&gt;What got worse&lt;/th&gt;
&lt;th&gt;Net effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1 Naive vector&lt;/td&gt;
&lt;td&gt;Strong baseline recall@10&lt;/td&gt;
&lt;td&gt;Weak precision on error pastes&lt;/td&gt;
&lt;td&gt;Good starting point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2 Hybrid&lt;/td&gt;
&lt;td&gt;Better on sharp lexical queries&lt;/td&gt;
&lt;td&gt;Worse on code + paraphrase&lt;/td&gt;
&lt;td&gt;Slight regression overall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P3 Distill + Burst&lt;/td&gt;
&lt;td&gt;Higher recall ceiling (0.94 &lt;a class="mentioned-user" href="https://dev.to/10"&gt;@10&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Lower precision&lt;/td&gt;
&lt;td&gt;Raised the ceiling for P4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 + Rerank&lt;/td&gt;
&lt;td&gt;Large jump in precision (MRR 0.90)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Clear winner&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Current default:&lt;/strong&gt; &lt;code&gt;hybrid --rerank&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>vectordatabase</category>
      <category>llm</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: the web UI and a look back</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:20:45 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-the-web-ui-and-a-look-back-4k2m</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-the-web-ui-and-a-look-back-4k2m</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-mcp-server-5app"&gt;Post 6&lt;/a&gt; handed the retrieval tools to agents over MCP. This last post builds the other front end (a plain web page) for the humans who don't have an MCP client sitting in their editor. And since it is the end of the series, it is also where I add up the scoreboard.&lt;/p&gt;

&lt;p&gt;The UI is deliberately small: one FastAPI app, two JSON endpoints, and a single static HTML file with no build step. The same no-frameworks rule that governed retrieval (no LangChain, no vector-store SDK) governs the front end too (no React, no bundler, no npm).&lt;/p&gt;

&lt;h2&gt;
  
  
  Two endpoints, which are the two halves of the series
&lt;/h2&gt;

&lt;p&gt;The whole back end is a thin wrapper over the pipeline built in posts 1–5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ask_fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# LLM-free retrieval  (posts 1–4)
&lt;/span&gt;    &lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/ask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# full pipeline       (post 5)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create_app&lt;/code&gt; takes the two functions as arguments rather than building them: the same seam the tests use to inject fakes, and the same seam &lt;code&gt;main()&lt;/code&gt; uses to wire in the real &lt;code&gt;hybrid_search&lt;/code&gt; and &lt;code&gt;run_ask&lt;/code&gt;. The endpoints mirror the two things the series built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/api/search&lt;/code&gt;&lt;/strong&gt; runs hybrid retrieval and returns serialized hits, with no model call. This is posts 1 through 4 in one route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/api/ask&lt;/code&gt;&lt;/strong&gt; runs the whole post-5 loop (plan, retrieve, rerank, expand, synthesize) and returns the answer, the tools the planner chose, the numbered evidence, and any people. This is post 5 in one route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The front-end toggle makes that split literal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"mode"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"search"&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; search (LLM-free)&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"mode"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"ask"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; ask (full pipeline)&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see the architecture from the home page: one radio button for retrieval, one for retrieval plus reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually returns
&lt;/h2&gt;

&lt;p&gt;Real responses from the running app against the live corpus. &lt;code&gt;search&lt;/code&gt; mode is instant and free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/search {"query": "how do I add middleware", "limit": 3}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"results"&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="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_3027"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&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 to add a header field to the request in a middleware?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/fastapi/fastapi/issues/3027"&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="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_10180"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0162&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Mounting sub-applications under `APIRouter`"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…/10180"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ask&lt;/code&gt; mode spends the LLM calls and hands back a written, cited answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/ask {"question": "How do I add a custom middleware in FastAPI?"}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&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;"search"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"You can add custom middleware in FastAPI in several ways.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### 1. HTTP
             middleware with `@app.middleware(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)` ... async def
             add_process_time_header(request, call_next): ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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="nl"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_5071"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/fastapi/fastapi/issues/5071"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# Update Middleware Documentation ..."&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page renders the answer in one block and the evidence as a citation list under it: the &lt;code&gt;[n]&lt;/code&gt; markers in the prose line up with the numbered sources, so every claim is one click from the thread it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole front end is one file
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt; is ~125 lines: inline CSS with a &lt;code&gt;prefers-color-scheme&lt;/code&gt; dark mode, a form, three result containers, and ~50 lines of vanilla &lt;code&gt;fetch&lt;/code&gt; that POST to the two endpoints and render the JSON. No framework, no state library, no build. It is served as a string straight from the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;index_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;knowbase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire deployment story: &lt;code&gt;kb-web&lt;/code&gt; starts uvicorn on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;, and the one file it needs travels inside the package. For a knowledge base that a handful of people query, a single static page is not a compromise; it is the right size.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;p&gt;Seven posts, one rule: every post had to fix a failure the previous one demonstrated. Here is the whole arc on the fixed 31-question eval (MRR, the metric that tracked the story best):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;MRR†&lt;/th&gt;
&lt;th&gt;What it taught&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;naive vector&lt;/td&gt;
&lt;td&gt;0.77*&lt;/td&gt;
&lt;td&gt;the baseline is stronger than you expect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;+ hybrid (RRF)&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;fusion is confidence-blind; it regressed vs vector (0.77)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+ distill &amp;amp; burst&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;corpus rebuild hurt precision (vector fell to 0.63 too) but lifted recall@10 to 0.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+ rerank&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the reranker converts recall into precision (the win)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;planner + synthesis&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;retrieval becomes cited answers; "misses" get answered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;the tools become an agent's, not just ours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;web UI&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;…and a human's&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;†The MRR column traces the hybrid pipeline (posts 2–4); post 1 is its vector-only predecessor, the thing hybrid replaced. *Posts 1–2 measured on the ~3,700-doc pre-distillation corpus; 3–4 on the 16,315-doc distilled/burst corpus. Same 31 questions throughout.&lt;/p&gt;

&lt;p&gt;The shape of that table is the honest lesson of the series. The naive baseline was good. The two changes that looked like obvious wins (hybrid search, LLM distillation) each lost in isolation, and stayed in only because they were scaffolding: hybrid supplied a recall pool and distillation/bursting raised its ceiling to 0.94, and then one LLM reranker turned that pool into MRR 0.90. If I had shipped hybrid on faith in post 2 and never measured, I would have quietly made the system worse and called it progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd still fix
&lt;/h2&gt;

&lt;p&gt;The series ends honest about its edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The eval grades retrieval, not answers.&lt;/strong&gt; Every number above is "is the right document in the ranking". There is no automated score for whether the synthesized paragraph is correct or its citations are faithful (post 5's answers are demonstrated, not measured). A real answer-quality eval is the obvious next project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search_code&lt;/code&gt; is only as good as the pattern it is given&lt;/strong&gt; (post 6): a bare symbol can drown in documentation matches while &lt;code&gt;def X&lt;/code&gt; lands the definition. It should rank indexed code above docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two eval questions still "miss" on retrieval&lt;/strong&gt; (the &lt;code&gt;jsonable_encoder&lt;/code&gt; and background-tasks code-location lookups), even though &lt;code&gt;ask&lt;/code&gt; answers both from issue evidence. Closing that gap means better code retrieval, not a better ranker.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The system that started as one embeddings table and a cosine query ended as a planner routing across three retrievers, an LLM reranker, grounded synthesis with citations, an MCP server, and a web page; and the single most valuable component was the one I almost didn't measure carefully enough to keep. Build naive first, make the eval honest before you make the system clever, and let every addition prove it earned its place. That is the whole method; the FastAPI knowledge base was just where I ran it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading all seven. The retrieval numbers live in the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;; the web front end is &lt;code&gt;knowbase.web&lt;/code&gt; plus one static &lt;code&gt;index.html&lt;/code&gt;. &lt;br&gt;
Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>agents</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: an MCP server</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:20:38 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-mcp-server-5app</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-mcp-server-5app</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23"&gt;Post 5&lt;/a&gt; built an agent loop: a planner picks tools, the tools retrieve, a synthesizer writes a cited answer. All of that intelligence (the planning, the reranking, the synthesis) was mine, running as my code and using my API budget.&lt;/p&gt;

&lt;p&gt;Post 6 is the same tools with the loop taken out. Instead of being the agent, the knowledge base becomes a set of tools that someone else's agent (Claude Code, Claude Desktop, any MCP client) can call. The server does the retrieval; the client is the brain. It is a small amount of code and a genuinely different posture for the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inversion
&lt;/h2&gt;

&lt;p&gt;Every LLM call in posts 1–5 lived on my side: distilling threads, reranking pools, planning, synthesizing. The MCP server flips that. It exposes raw, LLM-free retrieval and lets the connected agent do the thinking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;MCP server exposing raw, LLM-free retrieval tools.

The client agent (e.g. Claude Code) does the orchestration: it decides which tool to call, reads the evidence, and synthesizes its own answer.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment is the whole design idea. The server makes zero model calls. It is fast, cheap, and deterministic; the agent on the other end supplies the judgment that posts 3–5 spent LLM calls on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three tools, and what they return
&lt;/h2&gt;

&lt;p&gt;The server is &lt;a href="https://github.com/modelcontextprotocol" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; over stdio, exposing exactly three tools (the retrievers from post 5, minus the planner and synthesizer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hybrid semantic + keyword search over FastAPI issues and code.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Exact-pattern grep over the indexed source tree.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="nd"&gt;@server.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;who_knows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;People most involved in the issue threads matching a topic.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each returns lightweight JSON (a source id, a score, a one-line snippet, and a URL to follow), not walls of text. The agent scans the list and decides what to pull. Real output against the live corpus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// search("how do I add middleware")&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"issue_3027"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0164&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&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 to add a header field to the request in a middleware?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/fastapi/fastapi/issues/3027"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// search_code("def jsonable_encoder")&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fastapi/encoders.py#jsonable_encoder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fastapi/encoders.py jsonable_encoder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fastapi/encoders.py#L129-L192"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// who_knows("dependency injection")&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sneakers-the-rat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"issues"&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;"issue_13399"&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;h2&gt;
  
  
  The one deliberate omission: no rerank
&lt;/h2&gt;

&lt;p&gt;Look closely at &lt;code&gt;search&lt;/code&gt; and you will notice what is not there. In post 4 the shipped default was hybrid + rerank (MRR 0.90). The MCP &lt;code&gt;search&lt;/code&gt; tool is hybrid without the reranker: it returns the fused pool (recall@10 0.94, ordering MRR ~0.57) and stops.&lt;/p&gt;

&lt;p&gt;That is not an oversight; it is the point. The reranker was an LLM reading candidates and deciding which one answers the question. On the other side of MCP there is already an LLM doing exactly that: the client agent reads the snippets and decides what to open. Running my own rerank first would be paying for judgment the agent is about to apply anyway. So the server hands over recall and lets the agent supply the ordering. The division of labor from post 5 survives; the synthesizer and reranker just moved across the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the client's judgment actually matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;search_code&lt;/code&gt; makes the hand-off concrete, because its quality depends entirely on the pattern the agent chooses. Under the hood it greps the cloned source, then maps hits back to indexed code chunks; and it greps the whole tree, docs included, capping at 20 files. Watch what that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;search_code("jsonable_encoder")      -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;          &lt;span class="c"&gt;# 20 doc/*.md hits, none indexed as code&lt;/span&gt;
&lt;span class="gp"&gt;search_code("def jsonable_encoder")  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fastapi/encoders.py#jsonable_encoder
&lt;span class="gp"&gt;search_code("class APIKeyHeader")    -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fastapi/security/api_key.py#APIKeyHeader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare token floods on documentation prose and returns nothing useful; &lt;code&gt;def X&lt;/code&gt; / &lt;code&gt;class X&lt;/code&gt; lands the definition instantly. A capable agent naturally reaches for the precise form, which is the whole bet of this post: the server stays simple and fast, and the intelligence (including "phrase the grep like a programmer") lives in the client. (It is also a fair to-do: &lt;code&gt;search_code&lt;/code&gt; could rank indexed code files above docs instead of relying on the caller's precision.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it up
&lt;/h2&gt;

&lt;p&gt;The server ships as a console script, &lt;code&gt;kb-mcp&lt;/code&gt;, that connects to the same Postgres and config as the CLI. Point any MCP client at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"knowbase"&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="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kb-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"KB_DSN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql://knowbase:knowbase@localhost:5433/knowbase"&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt;
  &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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now inside Claude Code you can ask a question about FastAPI and watch the agent call &lt;code&gt;search&lt;/code&gt;, read the snippets, call &lt;code&gt;search_code&lt;/code&gt; for the exact symbol, follow a URL, and write its own answer, with my knowledge base as the retrieval substrate and none of my LLM budget involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What post 6 is
&lt;/h2&gt;

&lt;p&gt;No new retrieval math and no new eval number: the tools are post 5's, and the same caveat holds (the eval scores retrieval, not what an agent does with it). What changed is who runs the loop. Posts 1–5 built a self-contained question-answering system; post 6 unbundles it into composable tools and gives them to whatever agent you already use. A knowledge base is more useful as a tool your assistant can reach for than as a separate app you have to visit.&lt;/p&gt;

&lt;p&gt;Which is the segue to the last post. &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-the-web-ui-and-a-look-back-4k2m"&gt;Post 7&lt;/a&gt; builds the other front end (a plain web UI) for the humans who don't have an MCP client sitting in their editor.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tool output here is real &lt;code&gt;kb-mcp&lt;/code&gt; retrieval against the corpus from&lt;br&gt;
the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;; the server is &lt;code&gt;knowbase.mcp_server&lt;/code&gt; and its three tools wrap the same &lt;code&gt;hybrid_search&lt;/code&gt;, &lt;code&gt;grep_code&lt;/code&gt;, and &lt;code&gt;who_knows&lt;/code&gt; used in &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23"&gt;post 5&lt;/a&gt;. &lt;br&gt;
Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: planner, tools, and synthesis</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:20:30 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-llm-reranker-4016/"&gt;Post 4&lt;/a&gt; ended on a wall: two questions missed no matter how good the reranker got.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Which function converts arbitrary objects into JSON-compatible data structures?"&lt;/em&gt;
(&lt;code&gt;jsonable_encoder&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Where does FastAPI implement running background tasks after a response is returned?"&lt;/em&gt;
(&lt;code&gt;background.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I framed them as code-location lookups that similarity search structurally can't reach, and promised post 5 would route them to a &lt;code&gt;grep_code&lt;/code&gt; tool that could. Then I built the whole &lt;code&gt;ask&lt;/code&gt; pipeline, ran those two questions through it, and got a surprise that changed what this post is about: both of them answer correctly, and not because &lt;code&gt;grep_code&lt;/code&gt; rescued them. This post is that surprise, and the machinery behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "misses" weren't misses
&lt;/h2&gt;

&lt;p&gt;Here is the first question, run end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kb ask "Which function converts arbitrary objects into JSON-compatible data structures?"
tools: search

The function is `fastapi.encoders.jsonable_encoder()`. It recursively converts supported objects—such as Pydantic models, dataclasses, datetime values, enums, and standard data structures—into JSON-compatible values. [1][3]

Sources:
[1] issue_3301  https://github.com/fastapi/fastapi/issues/3301
[3] issue_4834  https://github.com/fastapi/fastapi/issues/4834
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kb ask "Where does FastAPI implement running background tasks after a response is returned?"
tools: search

FastAPI wires `BackgroundTasks` into the response in `fastapi/routing.py`, while the actual post-response execution is handled by Starlette's response/background-task machinery. Tasks are attached to the response and run after it has been sent successfully. [1][2][4]
They do not run if the endpoint raises an exception instead of returning a response. [3][4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both correct. Both from &lt;code&gt;tools: search&lt;/code&gt; alone, with no &lt;code&gt;grep_code&lt;/code&gt; involved. What happened is that the eval grades recall against one gold document per question: for the first, the &lt;code&gt;encoders.py&lt;/code&gt; chunk that defines &lt;code&gt;jsonable_encoder&lt;/code&gt;. That chunk never enters the retrieval pool, so recall@10 scores it a miss. But the answer doesn't need that chunk; a dozen issue threads discuss &lt;code&gt;jsonable_encoder&lt;/code&gt; by name, and synthesizing across them produces exactly the right answer with citations.&lt;/p&gt;

&lt;p&gt;That is the real lesson to carry out of the retrieval posts: recall@k against a fixed gold doc undersells a system that can synthesize. The document was "missing"; the answer was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline that makes an answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kb ask&lt;/code&gt; is four stages, and only the first and last cost a model call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Plan.&lt;/strong&gt; One LLM call picks tools under a strict schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;KNOWN_TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grep_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;who_knows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;search&lt;/code&gt; is the entire post-1-through-4 pipeline (hybrid + rerank) and is almost always on.&lt;br&gt;
&lt;code&gt;grep_code&lt;/code&gt; and &lt;code&gt;who_knows&lt;/code&gt; switch on only when the question calls for them. If the planner call fails twice, it falls back to &lt;code&gt;search&lt;/code&gt;-only; the planner can add precision, never subtract it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Retrieve.&lt;/strong&gt; Each selected tool runs and returns rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Build evidence.&lt;/strong&gt; Every row becomes a numbered block &lt;code&gt;[n]&lt;/code&gt; with a real URL (&lt;code&gt;path#Lstart-Lend&lt;/code&gt; for code, the issue link for a thread). Outputs from multiple tools merge, deduped by source and renumbered, so nothing appears twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Synthesize.&lt;/strong&gt; One final LLM call, handed only the numbered evidence and one instruction: answer using only these blocks, cite &lt;code&gt;[n]&lt;/code&gt; after each claim, and if the evidence doesn't answer the question, say so rather than guess. That last clause is the difference between grounding and a plausible-sounding hallucination.&lt;/p&gt;
&lt;h2&gt;
  
  
  grep_code: exact lookup, when you name the thing
&lt;/h2&gt;

&lt;p&gt;So what is &lt;code&gt;grep_code&lt;/code&gt; for? It fires when the question names a concrete symbol. Ask it the same jsonable_encoder question, but by name instead of by description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kb ask "Where is the jsonable_encoder function defined?"
tools: search, grep_code

The `jsonable_encoder` function is defined in `fastapi/encoders.py`, at approximately lines 129–192. [1]

Sources:
[1] fastapi/encoders.py#jsonable_encoder  fastapi/encoders.py#L129-L192
[2] fastapi/encoders.py#jsonable_encoder@2  fastapi/encoders.py#L193-L242
[3] issue_1978  https://github.com/fastapi/fastapi/issues/1978
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the planner routes to &lt;code&gt;grep_code&lt;/code&gt;, and the exact code chunk arrives at rank 1 with a precise line range. Under the hood &lt;code&gt;grep_code&lt;/code&gt; is deliberately simple: it shells out to &lt;code&gt;rg&lt;/code&gt; (or &lt;code&gt;git grep&lt;/code&gt;) over the cloned source for the pattern, then maps each &lt;code&gt;path:line&lt;/code&gt; hit back onto the code chunks in the database by file and line range, scoring a chunk by how many matched lines fall inside it. No embeddings, no fusion; if the identifier is in the tree, the chunk that contains it comes back.&lt;/p&gt;

&lt;p&gt;The honest boundary is the routing: &lt;code&gt;grep_code&lt;/code&gt; triggers on questions that name a symbol ("where is &lt;code&gt;jsonable_encoder&lt;/code&gt;"), not on questions that describe one ("which function converts arbitrary objects…"), because there is nothing to grep for in a description. That is the correct behavior, and it is exactly why the two P4 eval questions went through &lt;code&gt;search&lt;/code&gt; alone: neither names its target. The synthesizer answered them anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-attaching the context the chunk boundary cut
&lt;/h2&gt;

&lt;p&gt;Chunking splits documents mid-thought. Before synthesis, each &lt;code&gt;search&lt;/code&gt; hit gets its neighbors re-attached, appended as a &lt;code&gt;[context]&lt;/code&gt; block (and only ever the neighbor text, never a second copy of the hit). Two real cases from the corpus:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A burst hit&lt;/strong&gt; for &lt;em&gt;"return a custom status code from a dependency"&lt;/em&gt; surfaces this fragment, which is a follow-up comment that barely stands alone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;@… I would also like to know how I can implement this. I want my dependency to trigger a redirect when it can't find a certain cookie…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Expansion re-attaches the parent thread, restoring the actual question the "this" refers to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[context]&lt;/strong&gt; # Returning a RedirectResponse in a dependency … How can I return a Redirect in a dependency to be executed in a conditional? … &lt;code&gt;raise HTTPException(status_code=303, …)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;A code hit&lt;/strong&gt; for the same query matches &lt;code&gt;fastapi/applications.py#put@2&lt;/code&gt;, the middle of the &lt;code&gt;put&lt;/code&gt; method, where the &lt;code&gt;status_code&lt;/code&gt; parameter is documented. On its own it is a floating docstring. Expansion pulls in the neighboring chunk with the method's actual signature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[context]&lt;/strong&gt; &lt;code&gt;def put(self, path: Annotated[str, Doc(…)], *, response_model: Annotated[Any, …&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reranker decides which document; expansion makes sure that document arrives with enough around it to be worth reading. It doesn't move the retrieval numbers (&lt;code&gt;kb eval&lt;/code&gt; scores retrieval, not the synthesized answer), but it is the difference between a usable citation and a dangling fragment.&lt;/p&gt;

&lt;h2&gt;
  
  
  who_knows: the same search, pointed at people
&lt;/h2&gt;

&lt;p&gt;The third tool falls out for free. Ask who knows about a topic and &lt;code&gt;who_knows&lt;/code&gt; runs the same hybrid search, then credits the people on the top threads (each thread's author at full weight by rank, each commenter at half) and returns a ranked list of names with the issues that earned them the score. No extra model call; expertise is just retrieval with the scores aimed at authors instead of documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What post 5 is, and what it isn't
&lt;/h2&gt;

&lt;p&gt;The series runs on honest framing, so: the eval measures retrieval, not answers. The recall/MRR numbers from posts 1–4 grade whether the right document is in the ranking; there is no automated grade for answer quality, citation accuracy, or the planner's routing. So post 5's evidence is demonstrative (the worked &lt;code&gt;kb ask&lt;/code&gt; transcripts above), not a new row in the table. That is also the honest ceiling of this post: I can show these examples are right; I can't yet report a synthesis-quality number across the whole set.&lt;/p&gt;

&lt;p&gt;What it is: the point where the knowledge base becomes usable. Posts 1–4 built a retriever that ranks the right document first for most questions. Post 5 wraps it in a planner that routes symbol questions to exact lookup, re-attaches the context chunking cut, and turns the whole thing into a cited answer, including for the two questions the retrieval metric had written off.&lt;/p&gt;

&lt;p&gt;The last two posts leave the terminal behind: &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-mcp-server-5app"&gt;post 6&lt;/a&gt; exposes these tools over MCP so an assistant can call them, and &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-the-web-ui-and-a-look-back-4k2m"&gt;post 7&lt;/a&gt; puts a web UI on top.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Transcripts here are real &lt;code&gt;kb ask&lt;/code&gt; output against the corpus measured in the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;; the architecture is the &lt;code&gt;planner&lt;/code&gt;, &lt;code&gt;grep_code&lt;/code&gt;, &lt;code&gt;who_knows&lt;/code&gt;, &lt;code&gt;expand&lt;/code&gt;, &lt;code&gt;evidence&lt;/code&gt;, and &lt;code&gt;synthesize&lt;/code&gt; components. &lt;br&gt;
Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>agents</category>
      <category>llm</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: an LLM reranker</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:20:15 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-llm-reranker-4016</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-llm-reranker-4016</guid>
      <description>&lt;p&gt;Three posts in, the scoreboard was not encouraging. Naive vector search (&lt;a href="https://dev.to/faridgnank02/rebuilbing-the-cerebras-knowledge-base-the-simple-vector-baseline-11hm"&gt;post 1&lt;/a&gt;) was a decent baseline. Hybrid retrieval (&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-adding-hybrid-search-45je"&gt;post 2&lt;/a&gt;) made it worse. Distilling and bursting the corpus (&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-llm-distillation-and-bursting-3fno"&gt;post 3&lt;/a&gt;) made plain vector worse still (MRR 0.77 down to 0.63), while doing one useful thing: raising hybrid's recall@10 to 0.94. The right answer was in the top 10 for 29 of 31 questions; it just wasn't at the top.&lt;/p&gt;

&lt;p&gt;This post is where that pays off. One LLM call per query, and MRR goes from 0.57 to 0.90.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was never recall; it was ordering
&lt;/h2&gt;

&lt;p&gt;By the end of post 3 the diagnosis was precise. Hybrid retrieval fuses a dense-vector list and a keyword list with reciprocal-rank fusion, and RRF is confidence-blind: it knows a document ranked #1 in the vector list, but not that #1 was a cosine-0.9 lock while the&lt;br&gt;
keyword competitors were noise. A confident vector hit and a lukewarm keyword hit enter the fusion as equals. That is why hybrid's recall@10 is excellent (0.94) and its recall@1 is poor (0.39): the answer is in the pool, buried under topically-related decoys that RRF had no way to rank below it.&lt;/p&gt;

&lt;p&gt;You cannot fix ordering with more retrieval. You fix it with something that can read two candidates and say which one actually answers the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reranker
&lt;/h2&gt;

&lt;p&gt;After hybrid produces its fused top-20, one LLM call reranks the pool. The candidates go in (id, source, a ~1.2k-character snippet each) and the model returns a ranked list of ids with scores, under a strict JSON schema. If the call fails or returns something malformed, it falls back to the original RRF order, so the reranker can only help, never break retrieval.&lt;br&gt;
It is wired in as &lt;code&gt;--rerank&lt;/code&gt; and requires &lt;code&gt;--mode hybrid&lt;/code&gt; (it reorders the fused pool; there is nothing to rerank without one).&lt;/p&gt;

&lt;p&gt;No new index, no new retriever, no corpus change. Same 16,315-document P3 corpus, same 31 questions. The only new thing is one model call per query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&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;vector&lt;/th&gt;
&lt;th&gt;fts&lt;/th&gt;
&lt;th&gt;hybrid&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;hybrid + rerank&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.87&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.71&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.63&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the last column against the one before it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MRR 0.57 to 0.90&lt;/strong&gt; (+0.33 from reordering a list we already had).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recall@1 0.39 to 0.87:&lt;/strong&gt; the reranker moves the correct document to the very top for 27 of 31 questions, more than double hybrid alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;recall@10 stays 0.94:&lt;/strong&gt; this is the tell that the reranker is doing exactly what it should and nothing more. It reorders the pool; it can't add a document that wasn't fused in. Post 3 built the pool; post 4 sorts it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first time in the series, a change beat every alternative on every metric. Hybrid + rerank doesn't just recover plain vector's precision (MRR 0.63); it far exceeds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This settles the question open since post 2
&lt;/h2&gt;

&lt;p&gt;Since post 2 there has been one unresolved decision: hybrid loses to plain vector on MRR, so do we keep it as the default, or revert? Post 3 confirmed the loss survives a cleaner corpus (hybrid 0.57 vs vector 0.63). Taken alone, hybrid never earned its place.&lt;/p&gt;

&lt;p&gt;But it was never meant to stand alone. Hybrid + rerank dominates every single-retriever configuration on every metric, and it only works because hybrid supplies the high-recall pool (0.94 at recall@10) for the reranker to sort. The two are a unit:&lt;/p&gt;

&lt;p&gt;The default is &lt;strong&gt;hybrid + rerank.&lt;/strong&gt; Hybrid provides the recall; the reranker provides the ordering. Neither is sufficient alone, which is why the previous three posts each looked like a failure in isolation.&lt;/p&gt;

&lt;p&gt;Plain vector stays as a no-LLM fallback (MRR 0.63, one embedding, zero model calls); keyword-only is there for exact-string debugging. The shipped default is the pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-attaching the context the chunk boundary cut
&lt;/h2&gt;

&lt;p&gt;The other half of post 4 doesn't move the retrieval numbers (because &lt;code&gt;kb eval&lt;/code&gt; measures retrieval, not the final answer), but it matters the moment you actually read a result. Chunking splits documents, and the split routinely lands between a function and the decorator or imports that explain it, or isolates a two-line resolving comment from the thread that gives it meaning. So before synthesis, &lt;code&gt;kb ask&lt;/code&gt; re-attaches neighbors: adjacent code chunks for a code hit, the full raw thread for a burst hit.&lt;/p&gt;

&lt;p&gt;A concrete case: a query about returning a custom status code from a dependency matches &lt;code&gt;fastapi/applications.py#put@2&lt;/code&gt; (the middle of the &lt;code&gt;put&lt;/code&gt; method, where the &lt;code&gt;status_code&lt;/code&gt; parameter is documented). On its own that chunk is a floating docstring. Expansion re-attaches the neighboring chunk that holds the method's actual signature (&lt;code&gt;def put(self, path: Annotated[str, …], *, response_model: …&lt;/code&gt;), so the citation arrives with the code that gives it meaning instead of a fragment.&lt;/p&gt;

&lt;p&gt;The reranker decides which document; expansion makes sure that document arrives with enough around it to be worth reading. &lt;a href="//post-5-planner-synthesis.md"&gt;Post 5&lt;/a&gt; shows the mechanism in full, including a burst hit reunited with its thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling the reranker can't lift
&lt;/h2&gt;

&lt;p&gt;Two questions miss under hybrid, and they miss under rerank too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Which function converts arbitrary objects into JSON-compatible data structures?"&lt;/em&gt;
(&lt;code&gt;jsonable_encoder&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Where does FastAPI implement running background tasks after a response is returned?"&lt;/em&gt;
(&lt;code&gt;background.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a tuning failure, and no reranker prompt will fix it: the reranker can only reorder the fused top-20. If the correct code chunk never enters that pool, it cannot be ranked into it. Both are natural-language code-location lookups (the answer is a code chunk that shares only diffuse vocabulary with the question), so neither the vector leg nor the keyword leg surfaces it into the pool in the first place.&lt;/p&gt;

&lt;p&gt;The fix isn't a better ranker; it is a different retriever, one that treats "where is &lt;code&gt;jsonable_encoder&lt;/code&gt; defined" as a symbol lookup, not a similarity search. That is &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23"&gt;post 5&lt;/a&gt;: a planner that routes a question to &lt;code&gt;grep_code&lt;/code&gt; / symbol retrievers when it looks like a code-location query, instead of sending everything through the same dense-plus-keyword pool. Post 4 got the ordering right; post 5 goes after the two questions that were never in the pool.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every number here comes from &lt;code&gt;uv run kb eval&lt;/code&gt; on the same fixed 31-question set used since post 2; the full per-mode output is in the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;, with the corpus -rebuild details in the same post. &lt;br&gt;
Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>vectordatabase</category>
      <category>llm</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: LLM distillation and bursting</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:20:06 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-llm-distillation-and-bursting-3fno</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-llm-distillation-and-bursting-3fno</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-adding-hybrid-search-45je"&gt;Post 2&lt;/a&gt; ended with a diagnosis and a promise. &lt;br&gt;
The diagnosis: hybrid retrieval lost to plain vector search not because keyword search is useless, but because the corpus is the problem (raw issue threads bury one resolving comment under dozens of noisy ones, so topical chatter outranks real answers). The promise: post 3 rebuilds the corpus and re-runs the exact same eval to see whether a cleaner corpus flips the verdict.&lt;/p&gt;

&lt;p&gt;I rebuilt it and re-ran the eval, and plain vector search got worse (MRR 0.77 down to 0.63). That is two "made it worse" posts in a row, but this one is worse in a useful way: the same rebuild that hurt precision raised the recall ceiling to the level post 4 needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in the corpus
&lt;/h2&gt;

&lt;p&gt;Two things, both aimed at the "answer drowns in its own thread" problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distillation.&lt;/strong&gt; Every issue thread now goes through one LLM call that rewrites it into a clean question-and-answer document (the problem, the resolution, the relevant symbols) with a strict JSON schema and a fall back to the raw thread if the model returns nothing usable.&lt;br&gt;
The distilled text becomes the document we embed. The raw thread is kept in &lt;code&gt;raw_content&lt;/code&gt; (it still feeds keyword search), and each result is cached by a content hash so re-ingesting is idempotent and doesn't re-pay the call.&lt;/p&gt;

&lt;p&gt;On this corpus: &lt;strong&gt;2,542 of 3,002 threads distilled cleanly; ~15% fell back to raw.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bursting.&lt;/strong&gt; A long thread has one comment that resolves it and eighty that don't. Embedding the whole thread as one vector means the resolving comment is averaged into oblivion. So I "burst" the high-signal comments out into their own vector-only rows (&lt;code&gt;issue_N#burst_i&lt;/code&gt;), so each resolving comment gets its own embedding. Bursts are excluded from keyword search and from IDF stats, and they canonicalize back to their parent issue at scoring time so a thread can never occupy two slots.&lt;/p&gt;

&lt;p&gt;The corpus went from ~3,700 documents to &lt;strong&gt;16,315&lt;/strong&gt; (3,002 issue parents, 12,626 burst rows, 687 code chunks). Same 31 questions as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&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;vector (P2 corpus)&lt;/th&gt;
&lt;th&gt;vector (P3)&lt;/th&gt;
&lt;th&gt;hybrid (P2)&lt;/th&gt;
&lt;th&gt;hybrid (P3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.52&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.71&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.81&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.94&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.63&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;0.57&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vector dropped on every metric. Hybrid still loses to vector on MRR. The verdict from post 2 did not flip. The one number that moved the right way is hybrid's recall@10 (0.90 up to 0.94). Hold onto that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why distillation hurt vector search
&lt;/h2&gt;

&lt;p&gt;I spent some time to correctly understand what happened.&lt;/p&gt;

&lt;p&gt;We embed the distilled document now, not the raw thread. Distillation does its job (it produces a tidy summary), and in doing so it throws away the verbatim text: the exact tracebacks, the literal error strings, the copy-pasted identifiers. The dense vector no longer reflects any of it.&lt;/p&gt;

&lt;p&gt;What happens to the error-paste queries, which were hybrid's showcase in post 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TypeError: Object of type int64 is not JSON serializable&lt;/code&gt;&lt;/strong&gt; was post 2's single best hybrid win (vector rank 5, promoted to hybrid rank 1 by the exact string). On the distilled corpus it falls out of vector's top 10 entirely; the summary dropped the literal string, so even the marginal rank-5 signal is gone.&lt;/li&gt;
&lt;li&gt;The three error pastes vector already missed in post 2 (a websocket ASGI &lt;code&gt;RuntimeError&lt;/code&gt;, a pydantic &lt;code&gt;SchemaError&lt;/code&gt;, a &lt;code&gt;ValueError: invalid literal for int()&lt;/code&gt;) stay missed, and now distillation has stripped the text keyword search leaned on too.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonable_encoder&lt;/code&gt; (the "which function converts arbitrary objects…" paraphrase) went from a vector rank-3 hit to a vector miss: the summary reads further from the query's wording than the original thread did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Distillation trades verbatim-match ability for semantic tidiness. For a pure dense retriever scoring against the summary, that is a losing trade on this eval. It is a real cost, and the honest version of this series has to name it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bursting raised the ceiling anyway
&lt;/h2&gt;

&lt;p&gt;Bursts are the counterweight. Giving the one resolving comment its own embedding (instead of averaging it into a 90-comment thread) is exactly the fix for "the answer drowns in its own thread". But the effect only shows up at depth: hybrid recall@10 climbed from 0.90 to 0.94. Bursts pull one more class of answer into the top 10.&lt;/p&gt;

&lt;p&gt;They do nothing for recall@1 or @3, and that is not bursting's fault; it is the same structural weakness from post 2. Reciprocal-rank fusion is rank-based and confidence-blind: it knows a document ranked #1 in the vector list, but not that #1 was at cosine 0.9 while the keyword competitors were marginal. More candidates in the pool, same inability to order them. Bursting fills the pool; it can't sort it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what did P3 actually buy?
&lt;/h2&gt;

&lt;p&gt;Not a better score, but a better ceiling. After distillation and bursting, the correct document is in hybrid's top-10 for 29 of 31 questions; the recall pool is as good as it is going to get. What is missing is ordering: the right answer is in there, just not at the&lt;br&gt;
top.&lt;/p&gt;

&lt;p&gt;That is a different problem from "the answer isn't retrieved at all", and it has a different fix. You don't need more recall; you need something that can look at 20 candidates and say which one answers the question. Rank-based fusion can't; an LLM can.&lt;/p&gt;

&lt;p&gt;Which is &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-llm-reranker-4016/"&gt;post 4&lt;/a&gt;. The reranker goes over hybrid's fused top-20 and reorders it, and for the first time in this series a change is an unqualified win: MRR 0.57 to 0.90, recall@1 0.39 to 0.87, on this exact corpus and eval set. P3 didn't raise the score; it built the pile of candidates that post 4's reranker finally sorts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two that P3 couldn't touch
&lt;/h2&gt;

&lt;p&gt;Two questions miss even at hybrid recall@10, and they miss in post 4 too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;"Which function converts arbitrary objects into JSON-compatible data structures?"&lt;/em&gt;
(&lt;code&gt;jsonable_encoder&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Where does FastAPI implement running background tasks after a response is returned?"&lt;/em&gt;
(&lt;code&gt;background.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are code-location lookups phrased in English: the answer is a code chunk that shares only diffuse vocabulary with the question. Distillation and bursting only touch the issue side of the corpus (they never rewrite code), so these were always out of reach here. They are the clean argument for &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-planner-tools-and-synthesis-1g23"&gt;post 5&lt;/a&gt;'s dedicated symbol retrievers. Retrieval tuning won't reach them; a different retriever will.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every number here comes from &lt;code&gt;uv run kb eval&lt;/code&gt; on the same fixed 31-question set used since post 2; the per-mode output and the query-by-query breakdown are in the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;. &lt;br&gt;
Code for the series:&lt;br&gt;
&lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>vectordatabase</category>
      <category>llm</category>
    </item>
    <item>
      <title>Rebuilding the Cerebras Knowledge Base: adding hybrid search</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:19:59 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-adding-hybrid-search-45je</link>
      <guid>https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-adding-hybrid-search-45je</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/faridgnank02/rebuilbing-the-cerebras-knowledge-base-the-simple-vector-baseline-11hm"&gt;Post 1&lt;/a&gt; ended with a clear setup. Naive vector search had solved recall@10 but leaked on a specific class of query: exact error pastes and identifier lookups, where a sharp lexical signal gets buried under semantic lookalikes. That is the canonical argument for hybrid retrieval: combine dense vectors with keyword search so the exact match can't hide.&lt;/p&gt;

&lt;p&gt;I built it, then I measured it. On my corpus, hybrid retrieval &lt;strong&gt;regressed&lt;/strong&gt; against plain vector search. This post is about why, because the failure is more useful than the feature would have been.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, sharpen the test
&lt;/h2&gt;

&lt;p&gt;Before adding anything, I had to make the eval honest. My post-1 hypothesis was about lexical queries, but my 22-question set only had a couple of those. Grading hybrid on that set would grade it on a test that can't see its main advantage or its main cost. So I expanded the eval to &lt;strong&gt;31 questions&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;migrated the 6 code questions from line-range ids to symbol ids (&lt;code&gt;path#symbol&lt;/code&gt;), so they survive re-chunking;&lt;/li&gt;
&lt;li&gt;added &lt;strong&gt;5 exact error-paste&lt;/strong&gt; questions (tracebacks that appear verbatim in exactly one thread);&lt;/li&gt;
&lt;li&gt;added &lt;strong&gt;4 exact-identifier&lt;/strong&gt; questions (rare function/class names).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also re-ingested with a few fixes the raw corpus needed: paginating comment threads past the 100-comment API boundary, symbol-based code chunk ids, and a stale-row sweep after a full re-walk. Then I re baselined naive vector on the new corpus and eval set:&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;Naive vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.68 (21/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.84 (26/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90 (28/31)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scores dropped from post 1, not because retrieval got worse, but because the new questions are harder on purpose. And there is the signal I was looking for: &lt;strong&gt;3 of the 5 error-paste questions don't appear in the top 10 at all.&lt;/strong&gt; Strings like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: invalid literal for int() with base 10: '5XX'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are maximally distinctive lexically, and the dense embedding misses them completely (beaten by threads that are merely &lt;em&gt;about&lt;/em&gt; similar errors). This is exactly the hole hybrid is supposed to plug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the hybrid retriever
&lt;/h2&gt;

&lt;p&gt;Two new pieces, both hand-rolled (no search framework).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-text search&lt;/strong&gt; rides Postgres' built-in GIN index over the raw thread text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I score matches by summing &lt;strong&gt;IDF&lt;/strong&gt; over the query's matched lexemes (document frequencies precomputed with &lt;code&gt;ts_stat&lt;/code&gt;). Call it BM25-lite: rare words count for more, but there is no term-frequency weighting and (keep this in mind) no length normalization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reciprocal-rank fusion&lt;/strong&gt; merges the two ranked lists. Each result scores &lt;code&gt;Σ 1/(k + rank)&lt;/code&gt; across the lists it appears in, with &lt;code&gt;k = 60&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;RRF_K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rrf_fuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RRF_K&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... return results sorted by fused score
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full pipeline: vector top-50 and FTS top-50, RRF fuse, cap at 3 results per source file (so one chatty file can't flood the page), then a small recency tie-breaker (&lt;code&gt;ε·exp(−age/τ)&lt;/code&gt;, τ = 180 days). No LLM anywhere; deliberately mechanical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;Same corpus, same 31 questions, three modes:&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;vector&lt;/th&gt;
&lt;th&gt;fts&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;hybrid&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.68&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;0.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.84&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.48&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MRR&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.77&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hybrid ties vector on recall@10 and beats FTS-alone on everything, but it &lt;strong&gt;loses to plain vector&lt;/strong&gt; on recall@1 (0.61 vs 0.68), recall@3 (0.65 vs 0.84), and MRR (0.67 vs 0.77). The metric that shows it most clearly is recall@3 (0.84 down to 0.65). Folding in keyword search made the top of the ranking worse.&lt;/p&gt;

&lt;p&gt;That is not the result I wanted to write up, so I dug into individual queries, and the picture split cleanly in two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where hybrid won (exactly as predicted)
&lt;/h2&gt;

&lt;p&gt;On sharp-lexical queries, hybrid did its job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TypeError: Object of type int64 is not JSON serializable&lt;/code&gt;:&lt;/strong&gt; vector rank 5 to hybrid rank 1. The exact string lives in the target thread and nowhere near as densely in the decoys; IDF-weighted FTS finds it and RRF promotes it to the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Why does my HTTPBearer endpoint return 403 instead of 401…":&lt;/strong&gt; vector rank 2 to hybrid rank 1. Two near-duplicate issues have almost identical titles; vector picks the wrong one, and FTS's overlap with the correct thread's exact wording flips it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AssertionError: Status code 204 must not have a response body&lt;/code&gt;:&lt;/strong&gt; vector rank 2 to hybrid rank 1. Same mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is an error paste or a near-duplicate-title disambiguation (the case I built hybrid for). When the lexical signal is sharp, fusion works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lost (and why)
&lt;/h2&gt;

&lt;p&gt;The problem is everywhere else. Two failure modes did the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code-symbol lookups drown in chatter.&lt;/strong&gt; Ask &lt;em&gt;"Where does FastAPI implement running background tasks after a response is returned?"&lt;/em&gt; Vector ranks the &lt;code&gt;background.py&lt;/code&gt; module #1 (exactly right); hybrid drops it out of the top 5. Five different issue threads say "background task" over and over in prose, while the module's own code has thin, diffuse lexical overlap by comparison, so FTS floods the top with threads and RRF has no way to say "but the vector match was already confident". &lt;code&gt;jsonable_encoder&lt;/code&gt; (rank 3 to gone) and the API-key-header class (rank 5 to gone) died the same way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paraphrase questions get diluted.&lt;/strong&gt; &lt;em&gt;"How do I use dependency injection outside path operations…"&lt;/em&gt; went from vector rank 2 to hybrid rank 5; FTS surfaced three other dependency-injection threads that share vocabulary but aren't the answer.&lt;/p&gt;

&lt;p&gt;Both failures trace to two structural weaknesses I built right in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RRF is rank-based and confidence-blind.&lt;/strong&gt; It knows a document was #1 in the vector list; it has no idea that #1 was at cosine 0.9 while the FTS competitors were marginal. A confident vector hit and a lukewarm keyword hit enter the fusion as equals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IDF-sum FTS has no length normalization.&lt;/strong&gt; For queries built from common words ("json", "object", "header"), the score spreads thin across many long threads, and the one short code chunk that actually answers the question can't win.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On this ~3,700-document corpus, the queries where hybrid hurts outnumber the ones where it helps. The win on error pastes is real, but it is a rounding error against the damage to code lookups and paraphrase questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  So do you ship it?
&lt;/h2&gt;

&lt;p&gt;Not as the default. On this corpus I would keep plain vector as the default retriever and reach for hybrid only on obvious error-paste input. That is the honest call the numbers support.&lt;/p&gt;

&lt;p&gt;But "revert it" is the wrong lesson. Hybrid isn't failing because keyword search is useless; it is failing for two fixable reasons, and neither is "add lexical search":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The corpus is the problem.&lt;/strong&gt; Raw issue threads bury one resolving comment under dozens of noisy ones, which is why topical chatter outranks real answers.
&lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-llm-distillation-and-bursting-3fno"&gt;Post 3&lt;/a&gt; attacks this directly: distill each thread with an LLM, and "burst" out the high-signal comments so the answer isn't drowned by its own thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fusion is the problem.&lt;/strong&gt; RRF can't express confidence, so &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-an-llm-reranker-4016/"&gt;post 4&lt;/a&gt; puts an LLM reranker over the fused pool, which can look at a candidate and say "no, that is just topically related".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hybrid retrieval isn't the fix; it is the scaffolding the actual fixes hang off of. &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-llm-distillation-and-bursting-3fno"&gt;Post 3&lt;/a&gt; rebuilds the corpus and re-runs this exact eval to find out whether a cleaner corpus changes the verdict.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Every number here comes from &lt;code&gt;uv run kb eval&lt;/code&gt; on a fixed 31-question set; the full per-mode output and the query-by-query breakdown are the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;. &lt;br&gt;
Code forthe series: &lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>vectordatabase</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Rebuilbing the Cerebras Knowledge Base : the simple vector baseline</title>
      <dc:creator>fr_trenton02</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:19:38 +0000</pubDate>
      <link>https://dev.to/faridgnank02/rebuilbing-the-cerebras-knowledge-base-the-simple-vector-baseline-11hm</link>
      <guid>https://dev.to/faridgnank02/rebuilbing-the-cerebras-knowledge-base-the-simple-vector-baseline-11hm</guid>
      <description>&lt;p&gt;After encountering a blog post on Twitter/X from Cerebras on how they build their internal knowledge base &lt;a href="https://www.cerebras.ai/blog/how-we-built-our-knowledge-base" rel="noopener noreferrer"&gt;&lt;em&gt;How We Built Our Knowledge Base&lt;/em&gt;&lt;/a&gt; , I tried to rebuild the architecture from from scratch (no retrieval frameworks, one component at a time). Here, I propose what I worked on in a series of 7 post. The rule for the series is simple: every post fixes a failure the previous post demonstrated. I don't add a reranker because the papers recommend it; I add it when I can show the query it gets wrong today.&lt;/p&gt;

&lt;p&gt;Post 1 is the naive version: one embeddings table, dense vectors, cosine similarity, top-k. It is the version most people start with. The useful question is how far it gets, and where it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corpus
&lt;/h2&gt;

&lt;p&gt;I needed a body of knowledge with the same shape as an internal company one: an authoritative source of truth, plus a messy running conversation where the real answers live. Cerebras had their docs and their Slack. I used a single open-source project with two views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; the FastAPI source, chunked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues:&lt;/strong&gt; the 3,000 most recent FastAPI issue threads (issue plus all comments), pulled raw from the GitHub API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The issues are the Slack analog: someone pastes a traceback, a few people discuss it, and a maintainer leaves the one comment that resolves it. If retrieval can't find that comment, it can't do the job.&lt;/p&gt;

&lt;p&gt;For post 1 the threads go in raw (no summarization, no cleanup). That is deliberate: I want to see what naive retrieval does with real, noisy text before changing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive design
&lt;/h2&gt;

&lt;p&gt;The whole thing is one Postgres table with pgvector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;source&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- 'github_issue' | 'github_code'&lt;/span&gt;
    &lt;span class="n"&gt;source_id&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- 'issue_1234', 'fastapi/encoders.py#jsonable_encoder'&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- the text that gets embedded&lt;/span&gt;
    &lt;span class="n"&gt;raw_content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;-- original thread/chunk, for display&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;   &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;-- BGE-M3, HNSW cosine index&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt;    &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embeddings are &lt;strong&gt;BGE-M3&lt;/strong&gt; (1024 dims) run locally through sentence-transformers (no embedding API). Code is split with a small language-aware recursive chunker (split on class/function boundaries, not arbitrary line counts). Search is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed the query, order by cosine distance, return the top k. No FTS, no reranking, no query planning. That is the entire retriever.&lt;/p&gt;

&lt;p&gt;That gave me 3,000 issue threads plus 687 code chunks in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring it
&lt;/h2&gt;

&lt;p&gt;You can't claim a retriever is good by eyeballing three queries, and you can't claim the next version is better without a fixed yardstick. So before writing any improvement, I wrote an eval harness: 22 hand-written questions, each tagged with the &lt;code&gt;source_id&lt;/code&gt; that answers it. The metric is &lt;strong&gt;recall@k&lt;/strong&gt; (is the right document in the top k?), reported at k = 1, 3, 10.&lt;/p&gt;

&lt;p&gt;I chose recall@k because it maps to how the tool gets used: k=10 is "is the answer somewhere on the results page"; k=1 is "did we lead with it". The gap between them is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&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;Naive vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;recall@10&lt;/td&gt;
&lt;td&gt;1.00 (22/22)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@3&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recall@1&lt;/td&gt;
&lt;td&gt;0.77 (17/22)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On a ~3,700-document corpus, naive cosine similarity put the correct document in the top 10 every time. The "you need a sophisticated hybrid pipeline" narrative undersells where a plain vector index starts.&lt;/p&gt;

&lt;p&gt;So if you're building internal search over a few thousand documents and skip straight to a multi-stage pipeline, you may be solving a problem you don't have yet. recall@10 was never the problem; precision was. recall@1 sits at 0.77, meaning one query in four doesn't lead with the right answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually fails
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't the score; it's the shape of the 23% that miss at k=1. A few representative cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TypeError: Object of type int64 is not JSON serializable&lt;/code&gt;&lt;/strong&gt; (a verbatim error paste). 
The exact string appears in one thread's comments. Naive vector ranks that thread 5th, behind threads that are &lt;em&gt;about&lt;/em&gt; JSON serialization in general. The dense embedding treats "semantically nearby" and "contains this exact string" as the same thing, and there are many nearby decoys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AttributeError: 'Depends' object has no attribute ...&lt;/code&gt;&lt;/strong&gt; (same story). 
The canonical thread ranks 4th behind lookalikes. An exact lexical match arguably should win, and cosine similarity has no notion of "exact".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Which function converts arbitrary objects into JSON-compatible data structures?"&lt;/strong&gt; The answer is the &lt;code&gt;jsonable_encoder&lt;/code&gt; code chunk, but issue threads discussing &lt;code&gt;jsonable_encoder&lt;/code&gt; outrank the function's own definition. This "chatter about the code beats the code" pattern recurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a pattern here: naive vector search struggles where the query has a sharp lexical signal (an exact error string, a specific identifier) that the dense embedding smears out. That is a nameable weakness, and it points at an obvious next move: add keyword search back in.&lt;/p&gt;

&lt;p&gt;I'll note a counter-example, because it changes the plan. For a query like &lt;em&gt;"&lt;code&gt;allow_inf_nan=False&lt;/code&gt; is not enforced..."&lt;/em&gt; (a rare token, exactly what keyword search is supposed to own) naive vector already ranks the right issue #1. At this corpus size, dense search is stronger than the hybrid pitch assumes. So whatever I add next has to be argued at &lt;strong&gt;k=1 precision on error-paste queries&lt;/strong&gt;, not accepted on general principle. That is the bar for post 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs the first real run found
&lt;/h2&gt;

&lt;p&gt;A note for anyone doing this locally, because both cost me time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BGE-M3 ran out of memory on Apple Silicon.&lt;/strong&gt; Its default &lt;code&gt;max_seq_length&lt;/code&gt; is 8192; encoding real issue threads at that length exhausts MPS memory. Capping the sequence length to 1024 and lowering the encode batch size fixed it with no measurable quality loss on this corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.13 silently ignored my editable install.&lt;/strong&gt; On macOS the &lt;code&gt;uv&lt;/code&gt; editable &lt;code&gt;.pth&lt;/code&gt; file gets the &lt;code&gt;UF_HIDDEN&lt;/code&gt; flag, and Python 3.13 skips hidden &lt;code&gt;.pth&lt;/code&gt; files without a word, so imports failed. Pinning to Python 3.12 fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing that worked on the first try: incremental ingest. Each connector keeps a watermark, so re-running the ingest after it has caught up writes ~0 rows. Boring and correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Naive vector-only search solved recall@10 and left a specific hole: exact-string and error-paste queries where a lexical match is buried under semantic lookalikes. That is the textbook motivation for hybrid retrieval (dense vectors and keyword search, fused).&lt;/p&gt;

&lt;p&gt;So in &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-adding-hybrid-search-45je"&gt;post 2&lt;/a&gt; I add full-text search and reciprocal-rank fusion, re-run the same eval, and find out whether hybrid fixes those queries. It is not a clean win, and the way it fails is more instructive than a win would have been.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full 22-question eval and the k=1 miss list are in the &lt;a href="https://dev.to/faridgnank02/rebuilding-the-cerebras-knowledge-base-results-appendix-p1-p4-4km8"&gt;&lt;code&gt;Results Appendix&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
Code for the series: &lt;a href="https://github.com/faridgnank02/cerebras_knowledge_base" rel="noopener noreferrer"&gt;github.com/faridgnank02/cerebras_knowledge_base&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>vectordatabase</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
