# Building a Four-Tier Parallel RAG Pipeline with Gemini

> Source: <https://dev.to/danish_raza_dev/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa>
> Published: 2026-07-07 12:39:27+00:00

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.

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

We ran four retrieval strategies **simultaneously** using `Promise.all\`

, then merged results with a weighted scoring function.

`\`

`javascript`

const [semanticResults, textResults, regexResults, fuzzyResults] = await Promise.all([

semanticSearch(query, embeddings), // weight 1.8x

mongoFullTextSearch(query), // weight 1.5x

regexKeywordSearch(query), // weight 1.0x

fuzzyPerWordMatch(query), // weight 0.6x

])

\`\`

Using Gemini `gemini-embedding-2\`

to produce **3072-dimensional vectors**, 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.

A native MongoDB Atlas text index for fast, exact keyword hits. Great for technical terms, product names, and precise phrases.

Each significant word in the query is compiled to a case-insensitive regex. Catches partial matches and hyphenated variants.

Levenshtein distance matching per query word — handles typos and misspellings like "configuraton" → "configuration".

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

`\`

`javascript`

function mergeResults(tiers, weights) {

const scoreMap = new Map()

tiers.forEach((results, i) => {

results.forEach(({ id, score, chunk }) => {

const weighted = score * weights[i]

scoreMap.set(id, {

chunk,

total: (scoreMap.get(id)?.total || 0) + weighted,

})

})

})

return [...scoreMap.values()].sort((a, b) => b.total - a.total).slice(0, 5)

}

\`\`

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.
