Learn what embeddings are, how to create a vector search index, and how to find similar documents with the $vectorSearch stage for semantic search and RAG.

Bishal Biswas
Self Learner
Before you start: this lesson assumes you’ve read Atlas Search and the search Stage.
What you will learn
By the end of this lesson you will know:
- What a vector embedding is, in plain words
- How semantic search differs from keyword search
- How to create a vector search index and query it with
$vectorSearch - How to pre-filter results and read the similarity score
- How vector search fits into an AI app (RAG), and where you can run it
Note: Vector search runs on the same
mongotprocess as MongoDB Search. You can use it on MongoDB Atlas (including the free tier), in a local Atlas deployment (Atlas CLI or themongodb/mongodb-atlas-localDocker image), or on MongoDB Community 8.2+ withmongotinstalled. A plainmongodalone can’t run$vectorSearch.
Keyword search vs meaning search
Keyword search (last lesson) finds documents that contain the words you typed. That’s great for “USB cable”. It’s not great for:
“a film about a robot who falls in love”
A movie described as “an android discovers human feelings” matches the idea perfectly, but shares almost no words with the question. We need search by meaning. This is called semantic search.
What is an embedding?
An embedding is a list of numbers that describes the meaning of a piece of text (or an image, or audio). An AI model called an embedding model produces it.
// Text in, numbers out (real embeddings have hundreds or thousands of numbers)
"an android discovers human feelings" → [0.12, -0.03, 0.88, 0.41, ...]
A list of numbers like this is called a vector. The number of values is its dimensions. Popular models produce 512, 1024 or 1536 dimensions.
The magic is this: texts with similar meanings get vectors that point in similar directions. So “robot falls in love” and “android discovers feelings” end up close together.
Think of it like this: Imagine a giant map where every sentence is a pin. Sentences about love are in one neighbourhood, sentences about space in another, and “robot romance” sits on the border between them. Vector search is asking “which pins are nearest to my pin?”
MongoDB doesn’t create embeddings for you in this lesson’s examples. You call an embedding model (from providers such as Voyage AI, OpenAI, or an open-source model you run yourself) and store the resulting array in your documents.
A tiny example you can follow
Real embeddings are too long to read, so let’s use toy 3-dimensional vectors. Pretend the three numbers mean “how much action”, “how much romance” and “how much sci-fi”.
use shop
db.movies.insertMany([
{ title: "Space Rescue", genre: "sci-fi", year: 2021, plotEmbedding: [0.10, 0.00, 0.90] },
{ title: "Laser Duel", genre: "action", year: 2019, plotEmbedding: [0.80, 0.00, 0.60] },
{ title: "Monsoon Love", genre: "romance", year: 2023, plotEmbedding: [0.00, 0.95, 0.10] },
{ title: "Robot Heart", genre: "sci-fi", year: 2024, plotEmbedding: [0.10, 0.60, 0.70] },
{ title: "Street Chase", genre: "action", year: 2018, plotEmbedding: [0.95, 0.10, 0.00] }
])
In a real app, plotEmbedding would be the embedding of each movie’s plot, with something like 1024 numbers.
Creating a vector search index
A vector search index is a search index of type "vectorSearch". You tell it:
path: which field holds the vectornumDimensions: how many numbers each vector has (must match your model)similarity: how to measure closeness
db.movies.createSearchIndex(
"vector_index",
"vectorSearch",
{
fields: [
{ type: "vector", path: "plotEmbedding", numDimensions: 3, similarity: "cosine" },
{ type: "filter", path: "genre" },
{ type: "filter", path: "year" }
]
}
)
The two filter entries let us narrow results by genre and year later.
As with search indexes, wait until it’s ready:
db.movies.getSearchIndexes("vector_index")
// → [ { name: 'vector_index', type: 'vectorSearch', status: 'READY', queryable: true, ... } ]
Which similarity?
similarity |
Measures | Use when |
|---|---|---|
cosine |
The angle between vectors | Safe default for most text models |
dotProduct |
Angle and length | Your model’s docs say vectors are normalised and recommend it |
euclidean |
Straight-line distance | Your model’s docs recommend it, often for images |
Tip: Check your embedding model’s documentation. It usually tells you the dimensions and the best similarity function.
Your first $vectorSearch
Our question is “a romantic film about robots”. Pretend the embedding model turned it into [0.05, 0.70, 0.60] (lots of romance, quite a bit of sci-fi).
$vectorSearch must be the first stage of the pipeline:
db.movies.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plotEmbedding",
queryVector: [0.05, 0.70, 0.60],
numCandidates: 50,
limit: 3
}
},
{ $project: { _id: 0, title: 1, genre: 1, score: { $meta: "vectorSearchScore" } } }
])
[
{ title: 'Robot Heart', genre: 'sci-fi', score: 0.994 },
{ title: 'Monsoon Love', genre: 'romance', score: 0.911 },
{ title: 'Space Rescue', genre: 'sci-fi', score: 0.826 }
]
“Robot Heart” wins: it’s both romantic and sci-fi. Notice that no words matched at all. The search worked purely on meaning.
The score runs from 0 to 1. Higher means more similar.
What the fields mean
| Field | Meaning |
|---|---|
index |
Name of the vector search index |
path |
Field holding the vectors |
queryVector |
The embedding of the user’s question (same model, same dimensions!) |
numCandidates |
How many nearest neighbours to consider before picking the top results |
limit |
How many documents to return |
filter |
Optional pre-filter on fields indexed as filter |
exact |
Optional. true compares against every vector (slow, perfect). Default false |
ANN vs exact search
By default $vectorSearch runs an approximate nearest neighbour (ANN) search. It uses a clever graph structure (called HNSW) to find very close matches quickly, without comparing your query against every single vector.
numCandidatescontrols the trade-off: more candidates means better accuracy but a slower query. A good starting point is 10 to 20 times yourlimit.- For small collections, or to check accuracy, use exact nearest neighbour (ENN) with
exact: trueand nonumCandidates:
db.movies.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plotEmbedding",
queryVector: [0.05, 0.70, 0.60],
exact: true,
limit: 3
}
}
])
Pre-filtering
Often you want “similar movies, but only from 2020 onwards” or “only sci-fi”. Use filter. It narrows the search before finding neighbours, so you still get a full limit of results.
db.movies.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plotEmbedding",
queryVector: [0.05, 0.70, 0.60],
numCandidates: 50,
limit: 3,
filter: { $and: [ { genre: "sci-fi" }, { year: { $gte: 2020 } } ] }
}
},
{ $project: { _id: 0, title: 1, year: 1, score: { $meta: "vectorSearchScore" } } }
])
[
{ title: 'Robot Heart', year: 2024, score: 0.994 },
{ title: 'Space Rescue', year: 2021, score: 0.826 }
]
Filter fields must be declared as type: "filter" in the index, or the query errors. Filters support common operators like $eq, $in, $gt, $lt, $and and $or.
Note: You can also add a
$matchafter$vectorSearch(a post-filter). That runs on the results only, so you may end up with fewer thanlimitdocuments. Prefer the built-infilterwhen you can.
Vector search in an AI app (RAG)
The most common AI use is RAG, short for Retrieval-Augmented Generation. A chatbot answers questions using your documents instead of guessing.
1. User asks: "What is your return policy for headphones?"
2. Your app: turns the question into an embedding
3. MongoDB: $vectorSearch finds the 5 most relevant help articles
4. Your app: sends the question + those 5 articles to a language model
5. The model: writes an answer based on the articles
Here’s the retrieval part in Node.js. getEmbedding() stands for a call to your embedding provider’s API:
async function findRelevantArticles(db, question) {
const queryVector = await getEmbedding(question); // e.g. 1024 numbers
return db.collection("helpArticles").aggregate([
{
$vectorSearch: {
index: "articles_vector_index",
path: "embedding",
queryVector,
numCandidates: 100,
limit: 5
}
},
{ $project: { _id: 0, title: 1, body: 1, score: { $meta: "vectorSearchScore" } } }
]).toArray();
}
When you insert an article, you store its embedding alongside it:
const body = "Headphones can be returned within 30 days if unopened...";
await db.collection("helpArticles").insertOne({
title: "Returns: audio products",
body,
embedding: await getEmbedding(body)
});
Warning: Use the same embedding model for stored documents and for queries. Vectors from different models live on different “maps” and can’t be compared, even if the dimensions match.
Storing vectors efficiently
Embeddings are big. A million documents with 1536 dimensions is about 6 GB of vectors as plain doubles. Two features help:
- Quantization: set
quantization: "scalar"or"binary"on the vector field in the index. The index stores compressed vectors, using far less RAM, with a small accuracy cost. - BSON binary vectors: drivers can store vectors as a compact
BinDatavector subtype instead of an array of doubles, which shrinks documents on disk.
Start simple with arrays of numbers. Reach for these once your collection grows.
Vector search plus keyword search
Vector search is great at meaning, but can miss exact terms like product codes (“USB-C”) or names. Keyword search is the opposite. The best results often come from using both and merging the rankings. That’s called hybrid search, and it’s the topic of the next lesson.
Common mistakes
numDimensionsdoesn’t match the model. If the index says 1536 and your vectors have 1024 numbers, they aren’t indexed. Check your model’s output size.- Mixing embedding models. Query vectors must come from the same model as the stored vectors.
- Filtering on a field not in the index. Add it as
{ type: "filter", path: "..." }first. numCandidatessmaller thanlimit. It must be at leastlimit. Aim for 10 to 20 times higher for good recall.- Returning the huge vector field. Project only the fields you need, or your responses become megabytes of numbers.
Practice
- Write a vector search index definition for a
productscollection with 1024-dimension embeddings in a field calleddescEmbedding, using cosine similarity, and allowing filters oncategory. - Write a
$vectorSearchquery on that index that returns the 5 nearest products in theelectronicscategory, with their scores. - Using the toy
moviescollection, find the 2 movies closest to[0.9, 0.05, 0.1](pure action). Which do you expect to win?
Show a possible answer
// 1. Index
db.products.createSearchIndex("products_vector_index", "vectorSearch", {
fields: [
{ type: "vector", path: "descEmbedding", numDimensions: 1024, similarity: "cosine" },
{ type: "filter", path: "category" }
]
})
// 2. Query (queryVector would come from your embedding model)
db.products.aggregate([
{
$vectorSearch: {
index: "products_vector_index",
path: "descEmbedding",
queryVector: queryVector,
numCandidates: 100,
limit: 5,
filter: { category: "electronics" }
}
},
{ $project: { _id: 0, name: 1, score: { $meta: "vectorSearchScore" } } }
])
// 3. Pure action: expect "Street Chase" first, then "Laser Duel"
db.movies.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plotEmbedding",
queryVector: [0.9, 0.05, 0.1],
numCandidates: 20,
limit: 2
}
},
{ $project: { _id: 0, title: 1, score: { $meta: "vectorSearchScore" } } }
])
Key takeaways
- An embedding is a list of numbers describing meaning. Similar meanings give nearby vectors.
- Create a
vectorSearchindex with the vectorpath,numDimensionsandsimilarity, plus anyfilterfields. $vectorSearchmust be the first stage. SetqueryVector,numCandidatesandlimit, and read{ $meta: "vectorSearchScore" }.- Use
filterfor pre-filtering, andexact: truefor small collections or accuracy checks. - Vector search powers RAG chatbots and semantic search. It runs on Atlas, in a local Atlas deployment, or on Community 8.2+ with
mongot.
Next lesson: Hybrid Search with rankFusion and scoreFusion
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready