<?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 (@danishrazabangash).</description>
    <link>https://dev.to/danishrazabangash</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%2Ff5eb2cf2-c272-41c3-8547-2b1e9fda0ace.PNG</url>
      <title>DEV Community: Danish Raza</title>
      <link>https://dev.to/danishrazabangash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danishrazabangash"/>
    <language>en</language>
    <item>
      <title>Docker for MERN Developers: How I Actually Learned It (and Why It Clicked)</title>
      <dc:creator>Danish Raza</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:27:00 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/docker-for-mern-developers-how-i-actually-learned-it-and-why-it-clicked-35e5</link>
      <guid>https://dev.to/danishrazabangash/docker-for-mern-developers-how-i-actually-learned-it-and-why-it-clicked-35e5</guid>
      <description>&lt;p&gt;For the longest time, Docker was that thing on my resume-worthy-tools list that I hadn't really &lt;em&gt;earned&lt;/em&gt;. I knew the buzzwords — containers, images, &lt;code&gt;docker-compose&lt;/code&gt; — but I didn't know Docker the way you know a tool you've actually shipped something with.&lt;/p&gt;

&lt;p&gt;That changed while building &lt;strong&gt;BotForge&lt;/strong&gt;, my multi-tenant no-code AI chatbot builder. Here's how the pieces fell into place, and how Docker now fits naturally into my MERN workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "it works on my machine" wall
&lt;/h2&gt;

&lt;p&gt;BotForge isn't a single service. It's a MERN app with a Node/Express API, MongoDB, a four-tier RAG pipeline hitting Gemini embeddings, and integrations with WhatsApp and Telegram. Running all of that locally meant juggling environment variables, Node versions, and Mongo instances — and every time I handed the project to my teammate or tried deploying to Render, something that worked perfectly on my laptop broke somewhere else.&lt;/p&gt;

&lt;p&gt;That's the exact pain Docker exists to solve, and once I framed it that way, the concept stopped being abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that made it click
&lt;/h2&gt;

&lt;p&gt;Forget the textbook definitions for a second. Here's the analogy that actually worked for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image&lt;/strong&gt; = a recipe. It lists every ingredient and step needed to make your app run — Node version, dependencies, source code, start command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt; = the dish made from that recipe. You can make the same dish a hundred times, on a hundred different stoves, and it comes out identical every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; = the recipe card itself, written in a language Docker understands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once containers stopped feeling like "mini virtual machines" and started feeling like "a guaranteed-identical environment," everything else — layers, volumes, networking — became details I could look up as needed instead of concepts I had to memorize upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually learned it (step by step)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dockerized one thing first&lt;/strong&gt; — not the whole BotForge stack, just the Express API. A minimal &lt;code&gt;Dockerfile&lt;/code&gt;, one &lt;code&gt;EXPOSE&lt;/code&gt;, one &lt;code&gt;CMD&lt;/code&gt;. Got it running with &lt;code&gt;docker run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understood images vs. containers hands-on&lt;/strong&gt; by running the same image twice and watching two independent containers spin up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduced &lt;code&gt;docker-compose&lt;/code&gt;&lt;/strong&gt; once I needed Mongo alongside the API — this is where Docker started feeling &lt;em&gt;useful&lt;/em&gt; instead of academic, because one &lt;code&gt;docker-compose up&lt;/code&gt; replaced a page of setup instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broke things on purpose&lt;/strong&gt; — killed containers mid-run, misconfigured ports, forgot &lt;code&gt;.dockerignore&lt;/code&gt; and watched &lt;code&gt;node_modules&lt;/code&gt; bloat my image. Debugging is where the concepts actually stick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recently&lt;/strong&gt;, I turned this whole progression into an interactive Docker learning page for other MERN devs walking the same path — writing it out taught me as much as building the thing did.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where Docker actually fits in a MERN stack
&lt;/h2&gt;

&lt;p&gt;This is the part a lot of tutorials skip: Docker isn't one container, it's usually &lt;strong&gt;several, talking to each other&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A typical MERN &lt;code&gt;docker-compose.yml&lt;/code&gt; looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./server&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000:5000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MONGO_URI=mongodb://mongo:27017/botforge&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;

  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./client&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;

  &lt;span class="na"&gt;mongo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo:7&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-data:/data/db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service — React frontend, Express API, MongoDB — gets its own container, its own isolated environment, and they talk to each other over Docker's internal network using service names instead of &lt;code&gt;localhost&lt;/code&gt;. For BotForge specifically, this meant my RAG pipeline, the API layer, and the database could all be versioned and deployed together, and the exact same setup that ran on my machine ran identically on Render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;The real win wasn't learning Docker commands — it was what it &lt;em&gt;removed&lt;/em&gt;: no more "install Node 18.x, then Mongo, then set these seven env vars" onboarding docs. Just &lt;code&gt;docker-compose up&lt;/code&gt;. For a multi-tenant app like BotForge, where consistency across environments actually matters, that's not a nice-to-have, it's the difference between a demo and something deployable.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're a MERN dev starting today
&lt;/h2&gt;

&lt;p&gt;Don't start with &lt;code&gt;docker-compose&lt;/code&gt;. Don't start with Kubernetes (please don't). Start by Dockerizing one Express route. Watch it run identically twice. Then add Mongo. Then add your frontend. The stack builds itself once the first container clicks — and it will click faster than the docs make it seem.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>mern</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>Building for Bursty Traffic: Queues and Real-Time Design for a World Cup App July 11, 2026</title>
      <dc:creator>Danish Raza</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:33:45 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/building-for-bursty-traffic-queues-and-real-time-design-for-a-world-cup-appjuly-11-2026-2fm9</link>
      <guid>https://dev.to/danishrazabangash/building-for-bursty-traffic-queues-and-real-time-design-for-a-world-cup-appjuly-11-2026-2fm9</guid>
      <description>&lt;p&gt;Sports traffic is a brutal test case for system design. Nothing happens for 89 minutes, then a goal fires and every connected client wants an update in the same second. I hit this directly building a World Cup 2026 companion app — live scores, match analysis, notifications — using Next.js, Socket.io, and Redis-backed job queues (BullMQ).&lt;/p&gt;

&lt;p&gt;This isn't a system handling millions of concurrent users. But the design questions that traffic pattern forces on you are the same ones you'd face at much larger scale — you just get more room to experiment before it actually breaks. Here's what I learned about async processing and real-time delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Async-first beats synchronous-and-hope
&lt;/h2&gt;

&lt;p&gt;Handling bursty traffic synchronously — API request comes in, app does the work, app responds — falls over exactly when it matters most, because your spikes are correlated with the thing your users actually care about. A missed goal notification during the one minute it mattered is worse than no feature at all.&lt;/p&gt;

&lt;p&gt;So anything that wasn't required to happen &lt;em&gt;in&lt;/em&gt; the request-response cycle got pushed into a queue: fetching and processing match events, fanning out notifications, running post-match analysis. The web server's job became "accept the request fast and hand off the work," not "do the work." Redis-backed queues gave me retry logic and backpressure almost for free — if a burst of events comes in faster than workers can process them, they queue instead of timing out or crashing the server.&lt;/p&gt;

&lt;p&gt;The tradeoff is real: async systems are harder to reason about, harder to debug (where did this job go?), and you now have eventual consistency to think about instead of "it just happened." But for anything bursty, that tradeoff is almost always worth it over a synchronous system that's simpler right up until the one moment it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Real-time doesn't mean "broadcast to everyone"
&lt;/h2&gt;

&lt;p&gt;Socket.io makes it dangerously easy to broadcast every update to every connected client. That works fine in a demo. It does not work once you have thousands of people watching different matches, because now every client's browser is filtering out most of the messages it receives just to find the one it cares about.&lt;/p&gt;

&lt;p&gt;The fix was scoping connections into rooms — one per match — so a client only receives events for what it's actually watching. This is an obvious idea in hindsight, but it's the kind of thing that's easy to skip when you're focused on "does real-time work at all," and only becomes a problem once your Socket.io server is doing meaningfully more work than it needs to per connected client.&lt;/p&gt;

&lt;p&gt;The general principle: &lt;strong&gt;real-time infrastructure scales by what you &lt;em&gt;don't&lt;/em&gt; send, not just by how fast you send it.&lt;/strong&gt; Scoping and targeting your broadcasts is usually a bigger scalability lever than optimizing the transport itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your real bottleneck is probably someone else's API
&lt;/h2&gt;

&lt;p&gt;The app leans on third-party services for live data, AI-powered analysis (Gemini), and billing (Stripe) — each with its own rate limits and failure modes that don't show up until traffic actually spikes. A burst of match events can mean a burst of downstream calls to generate analysis or trigger notifications, and none of those upstream services promise to absorb that burst gracefully just because your own queue can.&lt;/p&gt;

&lt;p&gt;That pushed rate-limiting and backoff logic down into the worker layer itself, not just the request layer — workers pulling jobs off the queue needed to respect external rate limits independently of how fast jobs were arriving. The queue smooths out &lt;em&gt;your&lt;/em&gt; bursts; it doesn't automatically smooth out the burst you're about to hand to someone else's API.&lt;/p&gt;

&lt;p&gt;The lesson: when you sketch your architecture diagram, the boxes representing external APIs deserve as much design attention as the boxes you control. Queueing your own work is necessary but not sufficient — you have to think about what your workers do when they hit someone else's limits too.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cheap infrastructure, disciplined architecture
&lt;/h2&gt;

&lt;p&gt;This system runs on free/low-tier hosting, not a fleet of load-balanced servers — because that's the reality of building as a student. That constraint was useful. It forced decisions that scale regardless of the infrastructure underneath: stateless services that don't care which instance handles a request, background work that lives in a queue instead of memory, and real-time connections scoped tightly enough that they don't waste bandwidth they don't need.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that most "scalability" problems people reach for infrastructure to solve are actually architecture problems. A tightly-coupled, synchronous system doesn't get more scalable by throwing more servers at it — it just gets more expensive in the same shape. Fixing the shape first, on cheap infrastructure, means that if I do need to scale the infrastructure later, I'm scaling something already designed to be scaled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push bursty, non-critical-path work into queues&lt;/strong&gt; — synchronous systems fail exactly when traffic spikes matter most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope real-time broadcasts&lt;/strong&gt; — sending less is usually a bigger lever than sending faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit at the worker layer, not just the request layer&lt;/strong&gt; — your queue smooths your own bursts, not the ones you hand downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the architecture before reaching for infrastructure&lt;/strong&gt; — cheap hosting with disciplined design scales further than you'd expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this required expensive infrastructure or massive traffic to learn — it just required paying attention to &lt;em&gt;why&lt;/em&gt; a design choice mattered, not just whether it worked in the demo. That's the part that transfers, regardless of what you're building next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a final-year CS student building AI-integrated MERN stack applications. Part one of this series covers the multi-tenant RAG side of things, built for a very different kind of load — drop a comment if you've hit similar queueing or real-time tradeoffs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>realtime</category>
      <category>node</category>
    </item>
    <item>
      <title>Designing a Multi-Tenant RAG Pipeline: Lessons From Building an AI Chatbot Platform</title>
      <dc:creator>Danish Raza</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:26:28 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/designing-a-multi-tenant-rag-pipeline-lessons-from-building-an-ai-chatbot-platform-149f</link>
      <guid>https://dev.to/danishrazabangash/designing-a-multi-tenant-rag-pipeline-lessons-from-building-an-ai-chatbot-platform-149f</guid>
      <description>&lt;p&gt;Most "scalable system design" articles describe theoretical systems handling millions of users that neither the writer nor the reader will ever actually build. This isn't that. This is what I actually learned architecting a multi-tenant AI chatbot builder — the kind of system where dozens of businesses share one backend, each with their own bots, knowledge bases, and messaging integrations — on a student budget, with free-tier infrastructure.&lt;/p&gt;

&lt;p&gt;The system doesn't serve millions of requests a day. But the design questions you hit at 100 requests a day are the same ones you hit at 100,000 — you just get more room to be wrong and fix it. Here's what I learned about retrieval-augmented generation (RAG) and multi-tenancy before I actually had scale to worry about.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Design for tenants before you design for traffic
&lt;/h2&gt;

&lt;p&gt;The chatbot builder needed to support many businesses (tenants), each with their own bots, knowledge bases, WhatsApp/Telegram connections, and usage limits — all on one shared backend.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to bolt tenancy on later: one database, a &lt;code&gt;userId&lt;/code&gt; field sprinkled into every collection, and hope nothing leaks. I went the other way and treated tenant isolation as a first-class part of the schema from day one — every document that mattered (bots, conversations, knowledge base chunks, subscription state) carried a tenant identifier that every query had to pass through, no exceptions.&lt;/p&gt;

&lt;p&gt;The tradeoff: it's slower to build features early on, because you can't cut corners on "whose data is this." The payoff: when you add a new feature later, you're not doing an emergency audit of every collection to figure out where tenant data might be leaking into another tenant's view. Isolation-by-default is one of those decisions that's nearly free at the start and extremely expensive to retrofit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Not every request needs the same amount of work
&lt;/h2&gt;

&lt;p&gt;The chatbot's core feature is retrieval-augmented generation: a user asks a question, the system searches a knowledge base, and an LLM answers using the retrieved context. The naive version of this does one thing — embed the query, run a vector search, stuff results into a prompt — for every single request, regardless of how the question is phrased or what's actually being asked.&lt;/p&gt;

&lt;p&gt;That's wasteful. A one-word follow-up question and a complex multi-part query don't need the same retrieval effort, but they'll cost the same if you treat them identically.&lt;/p&gt;

&lt;p&gt;I ended up building the retrieval pipeline in tiers — cheaper, faster paths handle requests that don't need deep retrieval, and progressively heavier tiers only kick in when the query actually warrants it. In production this pipeline consistently retrieves relevant context with around 90% accuracy at roughly 780ms average latency — good enough to feel responsive in a chat interface, without paying full retrieval cost on every message.&lt;/p&gt;

&lt;p&gt;The general lesson isn't "build a four-tier pipeline." It's: &lt;strong&gt;look for the cases where you're doing expensive work uniformly across requests that don't uniformly need it.&lt;/strong&gt; That gap is usually where your easiest scalability wins live, and it's almost always cheaper than reaching for more infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your real bottleneck is probably someone else's API
&lt;/h2&gt;

&lt;p&gt;The platform leans heavily on third-party APIs — Gemini for embeddings and generation, WhatsApp's Cloud API and Telegram's Bot API for messaging. These have their own rate limits, latency, and failure modes that you don't control, and at small scale it's easy to forget they're there until a burst of traffic hits one at once.&lt;/p&gt;

&lt;p&gt;Multi-tenant WhatsApp integration made this concrete: each business brings its own WhatsApp number via Meta's Embedded Signup flow, but the app still has to be a good citizen of Meta's platform-wide rate limits across every tenant combined, not just per-tenant. That means rate limiting and backoff logic had to be designed at the application layer, not assumed away because "each tenant has their own number."&lt;/p&gt;

&lt;p&gt;The lesson: when you sketch your architecture diagram, the boxes representing external APIs deserve as much design attention as the boxes you control. They will rate-limit you, they will have outages, and they will not always fail gracefully. Plan for it before you need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cheap infrastructure, disciplined architecture
&lt;/h2&gt;

&lt;p&gt;This system runs on free/low-tier hosting (Render), not a fleet of load-balanced servers — because that's the reality of building as a student. That constraint was useful. It forced decisions that scale regardless of the infrastructure underneath them: strict tenant isolation, tiered retrieval instead of brute-force retrieval, and rate-limit-aware integrations instead of naive ones.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that most "scalability" problems people reach for infrastructure to solve are actually architecture problems. Fixing the shape of the system first, on cheap infrastructure, means that if I do need to scale the infrastructure later, I'm scaling something already designed to be scaled — not retrofitting scalability onto something that was never built for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolate tenants at the schema level from day one&lt;/strong&gt; — retrofitting isolation later means auditing everything you've already built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't do uniform work for non-uniform requests&lt;/strong&gt; — tiering retrieval effort to what a query actually needs is often a bigger win than adding infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for the external APIs you don't control&lt;/strong&gt; — they're often the actual bottleneck, not your own code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the architecture before reaching for infrastructure&lt;/strong&gt; — cheap hosting with disciplined design scales further than you'd expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this required expensive infrastructure or massive traffic to learn — it just required paying attention to &lt;em&gt;why&lt;/em&gt; a design choice mattered, not just whether it worked in the demo. That's the part that transfers, regardless of what you're building next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a final-year CS student building AI-integrated MERN stack applications. Part two of this series covers the async and real-time side of things, built for a very different kind of load — drop a comment if you've hit similar RAG or multi-tenancy tradeoffs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>node</category>
    </item>
    <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/danishrazabangash/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa</link>
      <guid>https://dev.to/danishrazabangash/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>
