MongoDB Mastery
MongoDB Mastery lessons / Module 40: Vector Search & RAG

Vector search and RAG with MongoDB

Level 7: Search + Vector30 min

Store embeddings next to your documents, index them for approximate nearest neighbour search, combine semantic similarity with filters, and build retrieval-augmented generation.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • 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; numCandidates controls how many neighbours are explored (recall vs latency; a common start is 10-20x limit). exact: true performs 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.

Documents with tiny embeddings
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" } ] } })
Semantic query: "training for a marathon"
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" } } }
])
No keyword overlap with "marathon" is needed: the vectors carry the meaning.
Semantic query with a pre-filter
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" } } }
])
RAG retrieval step in application code
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.

Playground noteThis simulator computes exact similarity on small hand-made vectors so you can see the mechanics; a real deployment uses model-generated embeddings and an HNSW index in mongot.

Avoid / preferDo this, not that

Avoid
// documents embedded with model A (1536 dimensions)
// queries embedded with model B (768 dimensions)
Prefer
// 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.

Avoid
{ $vectorSearch: { index: "vec", path: "embedding", queryVector: q, numCandidates: 3, limit: 3 } }
Prefer
{ $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 $vectorSearch with $match instead of a pre-filter, returning fewer than limit results.
  • numCandidates equal to limit, 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
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.