<?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: ihsan_kutluk</title>
    <description>The latest articles on DEV Community by ihsan_kutluk (@jasstt).</description>
    <link>https://dev.to/jasstt</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%2F3972978%2F4354481d-8507-4a11-9678-46db70fe31f2.png</url>
      <title>DEV Community: ihsan_kutluk</title>
      <link>https://dev.to/jasstt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasstt"/>
    <language>en</language>
    <item>
      <title>AI Writes the Code. But Who Checks It?</title>
      <dc:creator>ihsan_kutluk</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:46:47 +0000</pubDate>
      <link>https://dev.to/jasstt/-ai-writes-the-code-but-who-checks-it-29kd</link>
      <guid>https://dev.to/jasstt/-ai-writes-the-code-but-who-checks-it-29kd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hunting down three critical bugs in a real optimization project showed us the biggest blind spot in modern software development — and pointed us toward a fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;June 2026 · &lt;a href="https://github.com/jasstt/stochastic-VRP-decision-focused-learning" rel="noopener noreferrer"&gt;stochastic-VRP-decision-focused-learning&lt;/a&gt; · github/spec-kit&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Picture this: you hand a complex optimization problem to an AI. Within hours, 17 files are pushed. Tables, charts, numbers — everything looks polished and complete.&lt;/p&gt;

&lt;p&gt;But what if three critical bugs are hiding inside? And none of them were caught by code review, a linter, or any architectural rule?&lt;/p&gt;

&lt;p&gt;This is exactly that story. More importantly: these bugs revealed a problem modern software development still hasn't solved. And we're proposing something to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the project actually does
&lt;/h2&gt;

&lt;p&gt;We're solving a vehicle routing problem for a Cash-in-Transit company. Twenty ATMs, a handful of vehicles, each ATM with different cash demand on different days. The classical approach uses averages — and it fails badly. &lt;strong&gt;42% of ATMs run out of cash&lt;/strong&gt;, costing serious money.&lt;/p&gt;

&lt;p&gt;The project tackles this in two layers: a deterministic baseline (CVRP), then a decision-focused machine learning model (SPO+) that learns to account for demand uncertainty. The result: the stockout rate drops from 42% to 25% — nearly matching a theoretical oracle with perfect information.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📉 Stockout rate (classical)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;42%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📉 Stockout rate (SPO+)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;💰 Cost reduction&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Strong numbers. But the real story starts here.&lt;/p&gt;




&lt;h2&gt;
  
  
  The quantum experiment: good idea, misleading results
&lt;/h2&gt;

&lt;p&gt;Vehicle routing belongs to the class of combinatorial optimization problems that quantum computing theoretically targets. So we asked a natural question: &lt;em&gt;"What happens if we use Q# and QAOA here?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We fed the question to an AI. Hours later, a complete-looking implementation arrived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# QAOA Comparison Table — First Version

Method          &amp;lt;H&amp;gt; Energy   Constraints
PuLP/MILP       -2.5599      ✅ Satisfied
QAOA p=1        -2.5599      ⚠️  Violation
QAOA p=3        -2.5599      ⚠️  Violation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. QAOA is violating constraints — but returning the &lt;strong&gt;exact same energy&lt;/strong&gt; as MILP? And why do p=1 and p=3 (different circuit depths) agree to four decimal places?&lt;/p&gt;

&lt;p&gt;This is mathematically inconsistent. A solution that violates constraints should score worse — that's the whole point of penalty terms. Something was wrong. We dug in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three bugs, three different faces
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug 1 — The penalty weight was invisibly small
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# phase2c_qaoa_simulator.py, line 56
&lt;/span&gt;
&lt;span class="n"&gt;LAMBDA_C&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;  &lt;span class="c1"&gt;# ← Way too small
&lt;/span&gt;
&lt;span class="c1"&gt;# Load: 330k TL, capacity limit: 250k TL
# Violation penalty: 0.5 × (1.32 - 1.0)² = 0.051 units
# Route cost range: 1.2 – 3.5 units
# Penalty is ~1.5% of cost — QAOA literally cannot see it
&lt;/span&gt;
&lt;span class="n"&gt;LAMBDA_C&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;40.0&lt;/span&gt;  &lt;span class="c1"&gt;# Fix: penalty now exceeds max route cost (3.5)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;QAOA was violating the capacity constraint, but couldn't "feel" it — because the penalty for violating it was nearly the same as not violating it. The penalty term had dissolved into the cost function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2 — MILP contradicted itself but silently called it "Optimal"
&lt;/h3&gt;

&lt;p&gt;The MILP model had two constraints: "visit all ATMs" and "don't exceed vehicle capacity." But total demand (330k TL) already exceeded the capacity limit (250k TL) — meaning no feasible solution could satisfy both simultaneously.&lt;/p&gt;

&lt;p&gt;PuLP/CBC returned &lt;strong&gt;"Optimal."&lt;/strong&gt; Silently. No warning. The numbers appeared in the table. Nobody questioned them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3 — The wrong metric was being measured
&lt;/h3&gt;

&lt;p&gt;p=1 and p=3 showed identical results because the comparison code measured the argmax-bit solution instead of QAOA's expectation energy. For every value of p, argmax selected &lt;code&gt;[1,1,1,1,1]&lt;/code&gt; — yielding the same number every time. The circuits were actually running differently. The measurement just couldn't tell.&lt;/p&gt;

&lt;h3&gt;
  
  
  Corrected results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;&amp;lt;H&amp;gt;&lt;/code&gt; Energy&lt;/th&gt;
&lt;th&gt;Bit Energy&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;Constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brute Force&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;−34.856&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MILP (fixed)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;−34.856&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QAOA p=1&lt;/td&gt;
&lt;td&gt;−32.686&lt;/td&gt;
&lt;td&gt;−34.047&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QAOA p=2&lt;/td&gt;
&lt;td&gt;−31.689&lt;/td&gt;
&lt;td&gt;−33.622&lt;/td&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QAOA p=3&lt;/td&gt;
&lt;td&gt;−32.815&lt;/td&gt;
&lt;td&gt;−34.047&lt;/td&gt;
&lt;td&gt;2.3%&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The corrected picture is actually more convincing: QAOA works, circuit depth genuinely matters, but even at this small scale it trails optimal by 2–4%. That's consistent with the literature and physically expected behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why didn't anyone catch this?
&lt;/h2&gt;

&lt;p&gt;Look at what all three bugs have in common: the code was &lt;strong&gt;structurally correct&lt;/strong&gt;. No syntax errors, sensible variable names, functions calling functions, charts rendering. No linter would flag any of this. No architecture rule would fire. In code review it would read as "looks good."&lt;/p&gt;

&lt;p&gt;The problem was behavioral: penalty weight too small, constraints contradicting each other, wrong metric chosen. You can only catch these by &lt;strong&gt;running the code&lt;/strong&gt; and comparing its actual behavior against what was intended.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Code looking correct isn't enough. It has to be compared against expected behavior.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And right now, there's no automated tool that does this in a spec-driven workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Golden Demo idea
&lt;/h2&gt;

&lt;p&gt;The Spec-Driven Development world already has strong tools: Architecture Guard enforces architectural rules, DocGuard audits documentation quality, Security Review scans for vulnerabilities. All valuable. But all &lt;strong&gt;static&lt;/strong&gt; — they read code, they don't run it.&lt;/p&gt;

&lt;p&gt;Golden Demo does something different:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;During planning&lt;/strong&gt; — from the acceptance criteria and examples written in the spec, generate a small, runnable reference implementation — the &lt;em&gt;golden example&lt;/em&gt;. A deterministic, executable representation of intended behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After implementation&lt;/strong&gt; — run both the golden example and the real code against the same test vectors. Compare outputs. If there's a gap, generate a drift report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At merge time&lt;/strong&gt; — you know two things: does the code run? And does it do what was intended? These are the two questions most easily skipped right now.&lt;/p&gt;

&lt;p&gt;How would Golden Demo have handled the three bugs in this project?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bug 1:&lt;/strong&gt; If the spec had said &lt;em&gt;"penalty term must exceed maximum route cost,"&lt;/em&gt; that becomes a test vector. With &lt;code&gt;lambda=0.5&lt;/code&gt;, it fails immediately at merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug 2:&lt;/strong&gt; &lt;em&gt;"A solution returned as Optimal must satisfy all constraints"&lt;/em&gt; would automatically verify the solver's output — no silent infeasibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug 3:&lt;/strong&gt; &lt;em&gt;"Expectation energy must improve as circuit depth increases"&lt;/em&gt; would halt the test the moment both p values returned the same number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these require a human to think of checking them on the day. They're encoded once, in the spec, and verified automatically every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  One more thing: the quantum question
&lt;/h2&gt;

&lt;p&gt;A fair question worth addressing directly: &lt;em&gt;"If a quantum computer tries all solutions at once, why can't it just win?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer lives in the difference between &lt;strong&gt;superposition&lt;/strong&gt; and &lt;strong&gt;entanglement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Superposition means a qubit carries both 0 and 1 simultaneously before measurement — theoretically enabling parallel search across the entire solution space. Entanglement is different: it correlates qubits so that measuring one instantly tells you something about others. That's not variability; it's a strong dependency. In optimization, the real magic is superposition. Entanglement supports it.&lt;/p&gt;

&lt;p&gt;The problem is measurement: superposition collapses to a single classical result. QAOA's job is to amplify the probability of the right answer through interference — the way waves reinforce or cancel each other. On today's NISQ hardware, that interference control is too noisy to work reliably.&lt;/p&gt;

&lt;p&gt;Our 20-ATM problem needs roughly &lt;strong&gt;1.2 million physical qubits&lt;/strong&gt;. The best current hardware sits somewhere between 1,000 and 10,000. No practical advantage exists at this scale until fault-tolerant hardware matures — likely 10 to 15 years out.&lt;/p&gt;

&lt;p&gt;That doesn't mean "quantum is useless here." The resource estimation analysis in this project shows that as problem size grows (20 → 50 → 100 → 200 ATMs), a threshold emerges where classical MILP struggles and quantum could theoretically contribute. Knowing that the threshold can't be reached today isn't a reason to stop — it's a reason to prepare for the right moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're doing now
&lt;/h2&gt;

&lt;p&gt;This project became a live case study for why the &lt;strong&gt;Golden Demo + Behavioral Drift&lt;/strong&gt; extension we're proposing for the GitHub Spec Kit ecosystem needs to exist. Not an abstract idea — a demonstrated need, with three real bugs as evidence.&lt;/p&gt;

&lt;p&gt;We've posted an RFC in the spec-kit Discussions. The v1 scope is deliberately narrow: pure functions only, explicit input/output relationships, no side effects. The real risk with any new validation tool is generating noisy false positives and having everyone disable it on day two.&lt;/p&gt;

&lt;p&gt;If this problem feels familiar — approving AI-generated code because it &lt;em&gt;looks&lt;/em&gt; right, then discovering a behavioral bug weeks later — we'd love your input on the RFC.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jasstt/stochastic-VRP-decision-focused-learning" rel="noopener noreferrer"&gt;github.com/jasstt/stochastic-VRP-decision-focused-learning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spec Kit Discussions · &lt;a href="https://github.com/github/spec-kit/discussions" rel="noopener noreferrer"&gt;https://github.com/github/spec-kit/discussions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I made a mistake, please mention it below:)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: Python · Q# · PuLP · Microsoft QDK · Azure Quantum Resource Estimator&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>softwaredevelopment</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why Dense Search Fails in Production RAG — And How Hybrid Search Fixes It</title>
      <dc:creator>ihsan_kutluk</dc:creator>
      <pubDate>Sun, 07 Jun 2026 21:10:38 +0000</pubDate>
      <link>https://dev.to/jasstt/why-dense-search-fails-in-production-rag-and-how-hybrid-search-fixes-it-237k</link>
      <guid>https://dev.to/jasstt/why-dense-search-fails-in-production-rag-and-how-hybrid-search-fixes-it-237k</guid>
      <description>&lt;p&gt;I built a RAG system following the standard tutorial approach — embed, store, retrieve by cosine similarity. It worked fine until I asked it a technical question and got back two completely unrelated chunks about feature engineering. That's when I started digging.&lt;/p&gt;

&lt;p&gt;This article explains exactly why this happens — and how &lt;strong&gt;hybrid search&lt;/strong&gt; with Reciprocal Rank Fusion (RRF) and an LLM reranker solves the problem. All results come from a real pipeline I built and tested.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem — Dense Search Fails on Exact Keywords
&lt;/h2&gt;

&lt;p&gt;Here's a concrete example. I asked my RAG system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"What are the advantages of the Transformer architecture over traditional RNNs?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With &lt;strong&gt;dense-only search&lt;/strong&gt; (ChromaDB + &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;), the top 3 retrieved chunks were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Chunk ID&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Relevant?&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;&lt;code&gt;chunk_4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nlp_temelleri.txt&lt;/td&gt;
&lt;td&gt;✅ Yes — Transformer &amp;amp; self-attention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chunk_11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;veri_bilimi.txt&lt;/td&gt;
&lt;td&gt;❌ No — MSE, MAE error metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chunk_8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;veri_bilimi.txt&lt;/td&gt;
&lt;td&gt;❌ No — Feature engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model saw "model evaluation" and "Transformer model performance" as semantically close — because they are, in embedding space. But they're not what I was asking about. Dense search had no way to know that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Hybrid Search?
&lt;/h2&gt;

&lt;p&gt;Hybrid search combines two fundamentally different retrieval strategies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dense Retrieval (Semantic Search)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses neural embeddings (e.g., &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Captures semantic meaning: "automobile" matches "car"&lt;/li&gt;
&lt;li&gt;Great for paraphrase-style queries&lt;/li&gt;
&lt;li&gt;Weak at: exact technical terms, proper nouns, version numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Sparse Retrieval (BM25)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A classic probabilistic keyword matching algorithm&lt;/li&gt;
&lt;li&gt;Scores documents based on term frequency and inverse document frequency (TF-IDF family)&lt;/li&gt;
&lt;li&gt;Great at: exact keyword matching ("Transformer", "RNN", "CUDA")&lt;/li&gt;
&lt;li&gt;Weak at: synonyms and semantic variations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is perfect alone. Together, they cover each other's blind spots. A query like &lt;em&gt;"Transformer architecture vs RNN"&lt;/em&gt; benefits from BM25 catching the exact term "Transformer" while dense search handles the conceptual framing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reciprocal Rank Fusion (RRF)
&lt;/h2&gt;

&lt;p&gt;Once you have two ranked lists — one from dense, one from BM25 — you need to merge them intelligently. A naive approach (averaging scores) fails because the score scales are completely different: ChromaDB returns cosine distances while BM25 returns TF-IDF-based scores.&lt;/p&gt;

&lt;p&gt;RRF solves this with a rank-based formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RRF_score(doc) = Σ  1 / (k + rank_i(doc))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where &lt;code&gt;k&lt;/code&gt; is a constant (typically 60) and &lt;code&gt;rank_i(doc)&lt;/code&gt; is the document's position in the i-th ranked list.&lt;/p&gt;

&lt;p&gt;The beauty of RRF is that it only cares about &lt;em&gt;rank position&lt;/em&gt;, not raw score magnitudes. A document that ranks #1 in dense and #3 in BM25 will score much higher than one that ranks #20 in both — regardless of the underlying score scales. This makes it robust across completely different retrieval systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Reranker
&lt;/h2&gt;

&lt;p&gt;After RRF produces a merged list of ~20 candidates, sending all of them to the LLM for generation would be noisy and expensive. The reranker cuts this down to the top 5 that actually matter.&lt;/p&gt;

&lt;p&gt;Rather than another embedding model, I send all 20 candidates to &lt;strong&gt;Gemini&lt;/strong&gt; in a single prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Given this question: [query]
Rank the following 20 passages by relevance.
Return only: {"ranking": [idx1, idx2, idx3, idx4, idx5]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is effectively a &lt;strong&gt;cross-encoder&lt;/strong&gt; pattern: the LLM reads the query and all passages together, allowing it to consider &lt;em&gt;interaction effects&lt;/em&gt; between the query and each passage — something bi-encoder embedding models cannot do. The trade-off is cost and latency, but since we're calling it once per query (not once per document), it's manageable.&lt;/p&gt;

&lt;p&gt;The reranker also includes a &lt;strong&gt;retry + fallback mechanism&lt;/strong&gt;: if the API returns a &lt;code&gt;503 UNAVAILABLE&lt;/code&gt;, it waits 5 seconds and retries up to 3 times. On total failure, it falls back to the top 5 from RRF directly — so the pipeline never crashes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Results
&lt;/h2&gt;

&lt;p&gt;Here's what happened when I ran the same query with both approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query:&lt;/strong&gt; &lt;em&gt;"What are the advantages of the Transformer architecture over traditional RNNs?"&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Dense Only&lt;/th&gt;
&lt;th&gt;Hybrid (Dense + BM25 + RRF)&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;
&lt;code&gt;chunk_4&lt;/code&gt; ✅ nlp_temelleri.txt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chunk_4&lt;/code&gt; ✅ nlp_temelleri.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chunk_11&lt;/code&gt; ❌ veri_bilimi.txt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chunk_3&lt;/code&gt; ✅ nlp_temelleri.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chunk_8&lt;/code&gt; ❌ veri_bilimi.txt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chunk_11&lt;/code&gt; ❌ veri_bilimi.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;BM25 caught "Transformer" and "RNN" as exact keywords and boosted &lt;code&gt;chunk_3&lt;/code&gt; — a passage about word embeddings and NLP context — from outside the top 3 into rank #2. The two irrelevant data science chunks dropped out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation across 5 questions:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;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;Overall Accuracy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80% (4/5)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Citation Coverage&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;14/14&lt;/strong&gt; successful citations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid vs Dense&lt;/td&gt;
&lt;td&gt;BM25 removed 2 irrelevant chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resilience&lt;/td&gt;
&lt;td&gt;503 errors handled via retry + fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every answer cites its source inline (e.g., &lt;code&gt;[1]&lt;/code&gt;, &lt;code&gt;[2]&lt;/code&gt;) with the actual filename, so users can verify the origin of each claim.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sentence-transformers&lt;/code&gt; (&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector DB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chromadb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparse retrieval&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rank_bm25&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fusion&lt;/td&gt;
&lt;td&gt;Custom RRF implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reranker + Generator&lt;/td&gt;
&lt;td&gt;Google Gemini API (&lt;code&gt;google-genai&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python-dotenv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/jasstt/rag_project" rel="noopener noreferrer"&gt;github.com/jasstt/rag_project&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/jasstt/rag_project.git
&lt;span class="nb"&gt;cd &lt;/span&gt;rag_project
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="c"&gt;# Add your Gemini API key to .env&lt;/span&gt;
python src/ingest.py
python main.py
python src/eval.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;I'm not saying dense search is bad. For most casual queries it works fine. But the moment your users start asking technical questions — exact model names, function signatures, version numbers — BM25 starts pulling its weight. Adding it took maybe 20 minutes. Two irrelevant chunks disappeared from the results without touching anything else in the pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  v1.1 Update: Community Feedback in Action
&lt;/h2&gt;

&lt;p&gt;Shortly after publishing the initial version of this pipeline, I received some incredible feedback from the engineering community. I've integrated three major improvements directly into the codebase:&lt;/p&gt;

&lt;p&gt;**1. Sentence-Aware Chunking &lt;br&gt;
Instead of blindly cutting text at 500 characters, &lt;code&gt;src/ingest.py&lt;/code&gt; now uses NLTK/regex to detect sentence boundaries. It never splits a sentence in half, and it specifically preserves table-like structures (e.g., lists with pipes or colons) by keeping those rows together. This drastically improves the semantic quality of the chunks.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;2. Skip-Rerank Optimization &lt;br&gt;
LLM rerankers introduce latency. To fix this, I added a confidence check in &lt;code&gt;src/rerank.py&lt;/code&gt;. If the top 1 result from RRF has a score significantly higher than the top 2 result (configured via &lt;code&gt;SKIP_RERANK_THRESHOLD&lt;/code&gt;), the pipeline assumes high confidence and *skips the LLM reranker entirely&lt;/em&gt;, dropping latency to near-zero for easy questions.&lt;/p&gt;

&lt;p&gt;**3. Local Cross-Encoder Reranker &lt;br&gt;
To remove the hard dependency on Gemini for reranking, I integrated &lt;code&gt;cross-encoder/ms-marco-MiniLM-L-6-v2&lt;/code&gt;. You can now switch &lt;code&gt;RERANK_MODE = "local"&lt;/code&gt; in the config to run a fully offline, local cross-encoder that evaluates interactions between the query and the retrieved chunks without hitting any external APIs.&lt;/p&gt;




&lt;p&gt;Building in public is a cheat code. A huge thanks to the community for the suggestions.&lt;/p&gt;

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