Vector search and RAG with MongoDB
Store embeddings next to your documents, index them for approximate nearest neighbour search, combine semantic similarity with filters, and build retrieval-augmented generation.
- Explain embeddings, similarity functions and ANN indexes
- Create a vector search index with filter fields
- Query with $vectorSearch: numCandidates, limit, filter, score
- Design a RAG pipeline: chunking, embedding, retrieval, prompting
conceptThe concept
An embedding is a vector of numbers (for example 768 or 1536 dimensions) produced by a model so that semantically similar texts or images have nearby vectors. Similarity is measured with cosine, dot product (for normalized vectors) or euclidean distance.
db.docs.createSearchIndex({ name: "vec", type: "vectorSearch", definition: { fields: [
{ type: "vector", path: "embedding", numDimensions: 1536, similarity: "cosine" },
{ type: "filter", path: "category" } ] } })
db.docs.aggregate([
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: [...], numCandidates: 200, limit: 5, filter: { category: "sports" } } },
{ $project: { title: 1, score: { $meta: "vectorSearchScore" } } }
])
- Vector indexes use HNSW graphs for approximate nearest neighbour (ANN) search;
numCandidatescontrols how many neighbours are explored (recall vs latency; a common start is 10-20xlimit).exact: trueperforms exhaustive search. - Pre-filtering requires the field to be declared as
type: "filter"in the index. - Quantization (scalar or binary) reduces memory for large vector sets at some cost in recall.
Open the interactive lesson to use this diagram.
whyWhy it matters
Keeping vectors with the operational data avoids a separate vector database and the sync problems that come with it: filters on business fields, access control and freshness work on the same documents. RAG grounds LLM answers in your own data, which is the most common enterprise generative-AI pattern.
Use caseReal-world scenario
A support assistant: product manuals are split into ~500-token chunks, each stored with its embedding, product id and version. A question is embedded, $vectorSearch retrieves the 5 most similar chunks filtered by the customer's product and version, and the LLM answers using only those chunks, citing them.
Try itExamples
Run and edit these in the interactive lesson.
db.lab_kb.drop()
// dimensions: [electronics, fitness, kitchen, reading]
db.lab_kb.insertMany([
{ title: "Pairing wireless earbuds with a phone", category: "electronics", embedding: [0.9, 0.2, 0.0, 0.1] },
{ title: "Choosing running shoes for long distances", category: "sports", embedding: [0.1, 0.95, 0.0, 0.1] },
{ title: "Heart-rate zones with a smart watch", category: "electronics", embedding: [0.7, 0.7, 0.0, 0.1] },
{ title: "Cleaning a coffee grinder", category: "home", embedding: [0.1, 0.0, 0.95, 0.1] },
{ title: "Yoga mat thickness guide", category: "sports", embedding: [0.0, 0.9, 0.1, 0.2] },
{ title: "Best books on databases", category: "books", embedding: [0.2, 0.0, 0.0, 0.95] }
])
db.lab_kb.createSearchIndex({ name: "vec", type: "vectorSearch", definition: { fields: [
{ type: "vector", path: "embedding", numDimensions: 4, similarity: "cosine" },
{ type: "filter", path: "category" } ] } })db.lab_kb.aggregate([
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: [0.2, 0.9, 0.0, 0.1], numCandidates: 20, limit: 3 } },
{ $project: { _id: 0, title: 1, score: { $meta: "vectorSearchScore" } } }
])db.lab_kb.aggregate([
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: [0.2, 0.9, 0.0, 0.1], numCandidates: 20, limit: 3, filter: { category: "electronics" } } },
{ $project: { _id: 0, title: 1, category: 1, score: { $meta: "vectorSearchScore" } } }
])async function answer(question, productId) {
const queryVector = await embed(question); // same model used for the chunks
const chunks = await db.collection("manual_chunks").aggregate([
{ $vectorSearch: { index: "vec", path: "embedding", queryVector, numCandidates: 150, limit: 5, filter: { productId } } },
{ $project: { text: 1, source: 1, score: { $meta: "vectorSearchScore" } } }
]).toArray();
const context = chunks.map((c, i) => "[" + (i + 1) + "] " + c.text).join("\n\n");
return llm.complete({
system: "Answer only from the numbered context. Cite sources like [2]. Say you do not know if the context is insufficient.",
user: "Context:\n" + context + "\n\nQuestion: " + question
});
}internalsUnder the hood
The vector index is maintained by mongot alongside the database and updated asynchronously from change streams, so newly written vectors become searchable shortly after the write. Memory is the main cost: 1 million 1536-dimension float32 vectors are about 6 GB before quantization, plus graph overhead. Hybrid search combines vector results with keyword ($search) results, for example with reciprocal rank fusion, to handle exact terms such as product codes that embeddings handle poorly.
Avoid / preferDo this, not that
// documents embedded with model A (1536 dimensions) // queries embedded with model B (768 dimensions)
// documents and queries embedded with the same model and version // numDimensions in the index matches that model
Vectors from different models live in different spaces; similarity between them is meaningless. Re-embed everything when you change the model.
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: q, numCandidates: 3, limit: 3 } }{ $vectorSearch: { index: "vec", path: "embedding", queryVector: q, numCandidates: 50, limit: 3 } }Approximate search explores numCandidates neighbours before keeping the best limit. Setting them equal lowers recall; 10 to 20 times the limit is a common starting point.
PitfallsCommon mistakes
- Embedding queries and documents with different models or versions.
- Chunks that are too large (diluted meaning) or too small (no context); no overlap between chunks.
- Filtering after
$vectorSearchwith$matchinstead of a pre-filter, returning fewer thanlimitresults. numCandidatesequal tolimit, giving poor recall.- Ignoring re-embedding cost and versioning when the model changes.
LimitsWhen not to use it
Vector search is not exact lookup. For IDs, SKUs and exact phrases use regular or text indexes; hybrid search often works best. A few thousand vectors can be compared by brute force in the application. And RAG quality depends on chunking and evaluation as much as on the index.
SecuritySecurity notes
Retrieved chunks do not inherit permissions. Filter by tenant or access list inside $vectorSearch (the field must be declared as a filter field), otherwise one customer's question can surface another customer's documents. Treat retrieved text as untrusted input to the model: it can contain prompt injection.
ChallengeTry it yourself
Create kb_lab with a few embedded articles and a vector index with a tenant filter field. Return the 2 closest articles to a fitness-leaning query vector for tenant acme only.
Show one solution
db.kb_lab.drop()
db.kb_lab.insertMany([
{ title: "Smart watch heart-rate zones", tenant: "acme", embedding: [0.7, 0.7, 0.0, 0.1] },
{ title: "Running shoe sizing", tenant: "acme", embedding: [0.1, 0.95, 0.0, 0.1] },
{ title: "Coffee grinder cleaning", tenant: "acme", embedding: [0.1, 0.0, 0.95, 0.1] },
{ title: "Marathon training plan", tenant: "globex", embedding: [0.0, 0.99, 0.0, 0.1] }
])
db.kb_lab.createSearchIndex({ name: "vec", type: "vectorSearch", definition: { fields: [
{ type: "vector", path: "embedding", numDimensions: 4, similarity: "cosine" },
{ type: "filter", path: "tenant" } ] } })
db.kb_lab.aggregate([
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: [0.2, 0.9, 0.0, 0.1], numCandidates: 20, limit: 2, filter: { tenant: "acme" } } },
{ $project: { _id: 0, title: 1, score: { $meta: "vectorSearchScore" } } }
])InterviewQuestions you should be able to answer
How does vector search work in MongoDB?
Embeddings are stored as arrays in documents. A vector search index (HNSW graph, maintained by mongot) is defined on the vector path with dimensions and similarity, plus optional filter fields. $vectorSearch takes a query vector, explores numCandidates nearest neighbours approximately, applies pre-filters, and returns the top limit documents with a similarity score available via $meta: "vectorSearchScore". Results can flow into further pipeline stages.
Describe a RAG architecture on MongoDB.
Ingest: split documents into chunks, embed each, store text, embedding and metadata (source, version, ACL) in a collection with a vector index. Query: embed the question, run $vectorSearch with metadata filters (tenant, permissions), optionally combine with keyword search, re-rank, assemble a prompt with the top chunks and instructions to cite and abstain, call the LLM. Operate: keep embeddings in sync on content changes (change streams), evaluate retrieval quality, monitor latency and cost.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.