Building a RAG System with MongoDB and Node.js

Authors
  • avatar
    Name
    Hamza Rahman
Published on
-
4 mins read

If you already run MongoDB, you do not need a separate vector database to add retrieval to your app. MongoDB's built-in text search can find the documents relevant to a question, and you pass those into the model to answer from. This is the keyword-based middle ground between passing whole files into the prompt and a full vector database like Pinecone.

When MongoDB fits for RAG

MongoDB text search is a good choice when you already use MongoDB, you want keyword retrieval without new infrastructure, and your documents are searchable by the words they contain. It is not the right tool when you need semantic search that matches on meaning rather than exact words. For that, see the Pinecone version.

Setup

Install the dependencies:

npm install openai mongodb dotenv

Put your keys in a .env file:

OPENAI_API_KEY=your-api-key-here
MONGODB_URI=your-mongodb-connection-string

Connect to OpenAI and MongoDB:

import { OpenAI } from 'openai'
import { MongoClient } from 'mongodb'
import dotenv from 'dotenv'
dotenv.config()
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const client = new MongoClient(process.env.MONGODB_URI)
const db = client.db('your-db')

Create a text index

Text search needs a text index. Create one on the fields you want to search, and weight the title higher than the body so title matches rank first.

async function setupTextSearch() {
await db.collection('documents').createIndex(
{ content: 'text', title: 'text' },
{ weights: { title: 3, content: 1 } }
)
}

You only need to run this once per collection.

Add documents

Store each document with whatever metadata you want to keep alongside it.

async function indexDocument(document) {
await db.collection('documents').insertOne({
title: document.title,
content: document.content,
metadata: {
source: document.source,
date: new Date(),
category: document.category,
},
})
}

The RAG query

The flow is the same as any RAG system: find relevant documents, combine them into context, and ask the model to answer from that context.

async function mongoRAG(question) {
// 1. Search MongoDB for relevant documents
const results = await db
.collection('documents')
.find({ $text: { $search: question } }, { score: { $meta: 'textScore' } })
.sort({ score: { $meta: 'textScore' } })
.limit(3)
.toArray()
// 2. Combine the matched documents into one context string
const context = results.map((doc) => doc.content).join('\n')
// 3. Ask the model, grounded in that context
const response = await openai.chat.completions.create({
model: 'gpt-5.4-mini',
messages: [
{
role: 'system',
content: 'Answer using only the provided context. If the answer is not there, say so.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${question}`,
},
],
})
return response.choices[0].message.content
}

$text runs the search, textScore ranks results by relevance, and .limit(3) keeps only the top matches so the context stays small.

Two small variations cover most needs.

Search within a category, so a question only pulls from the right section of your data:

async function categorySearch(question, category) {
return db
.collection('documents')
.find({
$and: [{ 'metadata.category': category }, { $text: { $search: question } }],
})
.toArray()
}

Make matching less strict by searching each word separately, which helps when the exact phrase is not present:

async function fuzzySearch(question) {
const words = question.split(' ')
return db
.collection('documents')
.find({ $or: words.map((word) => ({ $text: { $search: word } })) })
.toArray()
}

Limitations

MongoDB text search matches words, not meaning. A question that uses different words from the document ("car" vs "automobile") can miss, because there is no semantic understanding. If your retrieval needs to match on meaning, or you are searching across millions of documents, a vector database is the better fit.