<?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: Danish Raza</title>
    <description>The latest articles on DEV Community by Danish Raza (@danish_raza_dev).</description>
    <link>https://dev.to/danish_raza_dev</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%2F4019486%2F62b073bd-b745-49ba-acc4-43cb54512eb8.png</url>
      <title>DEV Community: Danish Raza</title>
      <link>https://dev.to/danish_raza_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danish_raza_dev"/>
    <language>en</language>
    <item>
      <title>Building a Four-Tier Parallel RAG Pipeline with Gemini</title>
      <dc:creator>Danish Raza</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:39:27 +0000</pubDate>
      <link>https://dev.to/danish_raza_dev/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa</link>
      <guid>https://dev.to/danish_raza_dev/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When building BotForge, our AI no-code chatbot platform, we needed a retrieval system that could handle messy, real-world user queries — typos, partial phrases, semantically similar-but-differently-worded questions.&lt;/p&gt;

&lt;p&gt;A naive vector search alone wasn't good enough. It's powerful but brittle to out-of-vocabulary terms and exact keyword lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Four-Tier Parallel Retrieval
&lt;/h2&gt;

&lt;p&gt;We ran four retrieval strategies &lt;strong&gt;simultaneously&lt;/strong&gt; using &lt;code&gt;Promise.all\&lt;/code&gt;, then merged results with a weighted scoring function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
const [semanticResults, textResults, regexResults, fuzzyResults] = await Promise.all([&lt;br&gt;
  semanticSearch(query, embeddings),      // weight 1.8x&lt;br&gt;
  mongoFullTextSearch(query),             // weight 1.5x&lt;br&gt;
  regexKeywordSearch(query),              // weight 1.0x&lt;br&gt;
  fuzzyPerWordMatch(query),               // weight 0.6x&lt;br&gt;
])&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 1: Semantic Search (1.8× weight)
&lt;/h3&gt;

&lt;p&gt;Using Gemini &lt;code&gt;gemini-embedding-2\&lt;/code&gt; to produce &lt;strong&gt;3072-dimensional vectors&lt;/strong&gt;, we compute cosine similarity against stored document embeddings. This catches meaning — "how do I reset my login?" matches "account recovery options" even with no shared words.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: MongoDB Full-Text Search (1.5× weight)
&lt;/h3&gt;

&lt;p&gt;A native MongoDB Atlas text index for fast, exact keyword hits. Great for technical terms, product names, and precise phrases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 3: Regex Keyword Matching (1.0× weight)
&lt;/h3&gt;

&lt;p&gt;Each significant word in the query is compiled to a case-insensitive regex. Catches partial matches and hyphenated variants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 4: Fuzzy Per-Word Matching (0.6× weight)
&lt;/h3&gt;

&lt;p&gt;Levenshtein distance matching per query word — handles typos and misspellings like "configuraton" → "configuration".&lt;/p&gt;

&lt;h2&gt;
  
  
  Weighted Score Merging
&lt;/h2&gt;

&lt;p&gt;Each result carries a base score from its retrieval strategy. We deduplicate by chunk ID, sum scores across strategies, and sort descending:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
function mergeResults(tiers, weights) {&lt;br&gt;
  const scoreMap = new Map()&lt;br&gt;
  tiers.forEach((results, i) =&amp;gt; {&lt;br&gt;
    results.forEach(({ id, score, chunk }) =&amp;gt; {&lt;br&gt;
      const weighted = score * weights[i]&lt;br&gt;
      scoreMap.set(id, {&lt;br&gt;
        chunk,&lt;br&gt;
        total: (scoreMap.get(id)?.total || 0) + weighted,&lt;br&gt;
      })&lt;br&gt;
    })&lt;br&gt;
  })&lt;br&gt;
  return [...scoreMap.values()].sort((a, b) =&amp;gt; b.total - a.total).slice(0, 5)&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~90% retrieval accuracy&lt;/strong&gt; on held-out test queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~780ms median latency&lt;/strong&gt; end-to-end (including embedding generation)&lt;/li&gt;
&lt;li&gt;Robust to typos, paraphrasing, and domain-specific terminology&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parallel architecture was the key insight — running all four strategies concurrently keeps latency close to the slowest individual strategy (semantic search) rather than multiplying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cache embeddings for frequently asked questions&lt;/li&gt;
&lt;li&gt;Add a re-ranking step (cross-encoder) for the top 10 candidates&lt;/li&gt;
&lt;li&gt;Explore ColBERT-style late interaction for finer-grained scoring&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>mongodb</category>
      <category>node</category>
    </item>
  </channel>
</rss>
