Introduction
Vector search is no longer a feature exclusive to specialized search engines—it now lives where most of your data already does. MongoDB Atlas Vector Search: what developers should know is essential knowledge if you're building semantic search, recommendation systems, or any feature that needs to understand meaning rather than just keywords. In this article, we'll walk through storing embeddings, building indexes, querying with $vectorSearch, and filtering by metadata, so you can ship semantic search without a separate infrastructure burden.
Store embeddings as arrays
Before you can search by similarity, you need embeddings. An embedding is a numerical representation of text, an image, or other content—typically a vector of 768, 1536, or more dimensions. You'll generate these embeddings from an external API (OpenAI, Cohere, Hugging Face) and store them directly in your MongoDB documents as arrays.
Here's a realistic example: you're building a product search feature for an e-commerce platform. Each product document needs a dense vector representation of its description and attributes.
db.products.insertOne({
_id: ObjectId(),
name: "Lightweight Running Shoe",
description: "Professional-grade running shoe with responsive cushioning",
price: 129.99,
category: "Footwear",
embedding: [
0.023, -0.041, 0.15, 0.089, ..., -0.012
// 1536 dimensions from OpenAI's text-embedding-3-small
],
inStock: true,
createdAt: new Date()
})
A few practical points:
- Dimension count matters. Smaller models (384–768 dims) are faster and cheaper; larger ones (1536+ dims) are more accurate. Start with a smaller model and upgrade if relevance suffers.
- Generate embeddings before insertion. Ideally in a pre-processing step or via a serverless function before the document enters MongoDB. Don't embed on read.
- Store metadata alongside embeddings. Price, category, stock status, timestamps—these let you filter results. Your embedding is useless if you can't narrow the result set.
- Index the embedding field. This is required for vector search and discussed in the next section.
In production, you're likely generating embeddings in bulk when you ingest a CSV, sync from an upstream system, or listen to a Kafka topic. A batch job in Node.js or Python pulls 100 products at a time, calls OpenAI, and bulk-writes them to MongoDB. This keeps latency out of the request path.
Create a vector search index
MongoDB Atlas Vector Search requires an explicit index definition. Unlike traditional database indexes, a vector index is built on the Atlas cluster and uses an approximate nearest-neighbor algorithm (HNSW) to make searches fast. You create it through the Atlas UI or via the API.
Here's what the index definition looks like in JSON form:
{
"name": "vector_index",
"type": "vectorSearch",
"fields": [
{
"type": "vector",
"path": "embedding",
"similarity": "cosine",
"dimensions": 1536
}
]
}
Break this down:
path: the field name in your documents that holds the embedding array.similarity: typically "cosine" for text embeddings. Also supports "euclidean" and "dotProduct" depending on your embedding model.dimensions: must match the actual embedding size. Mismatch = indexing failure.
If you're using the MongoDB Atlas UI, navigate to your cluster, go to Search, and create a new search index. Paste the JSON above, select your collection, and hit create. Index building takes a few minutes for large collections; smaller ones (under 100k docs) are near-instant.
One gotcha: if you add the index to an existing collection with documents that don't have the embedding field, the index will silently skip those documents. If your collection is sparse, that's fine. If you need all documents searchable, backfill embeddings before indexing.
$vectorSearch in aggregation
Once indexed, you query with $vectorSearch in the aggregation pipeline. This is MongoDB's equivalent of a semantic search operator. You provide a query vector and a limit, and MongoDB returns the closest matches.
Here's a practical example: a user types "fast shoes for running" into your product search. Your backend generates an embedding for that query, then searches the products collection:
const searchEmbedding = await generateEmbedding("fast shoes for running");
const results = await db.products.aggregate([
{
$vectorSearch: {
queryVector: searchEmbedding,
path: "embedding",
k: 20,
exact: false
}
},
{
$project: {
_id: 1,
name: 1,
price: 1,
score: { $meta: "vectorSearchScore" }
}
}
]).toArray();
Key parameters:
queryVector: an array of numbers matching the dimensions of your indexed embedding.path: the field name you indexed.k: how many results to return. Start with 20–50; you'll refine and filter later.exact: false uses the approximate index (fast); true does exact search (slow, but perfect recall). Leave it false unless you're debugging.
$meta: "vectorSearchScore" gives you the similarity score (0 to 1 for cosine). Higher is closer. Use this score in your UI to show confidence or rank results.
A real production pattern combines multiple aggregation stages:
const results = await db.products.aggregate([
{
$vectorSearch: {
queryVector: searchEmbedding,
path: "embedding",
k: 100
}
},
{
$match: { inStock: true, price: { $lte: 200 } }
},
{
$sort: { score: -1 }
},
{
$limit: 10
},
{
$project: {
name: 1,
price: 1,
similarity: { $meta: "vectorSearchScore" }
}
}
]).toArray();
This retrieves 100 vector neighbors, filters to in-stock items under $200, sorts by similarity, and returns the top 10. The filtering happens after vector search, which is important: vector search is fast at finding semantic neighbors, but you're combining that with business logic.
Filter by metadata alongside similarity
Real search is never just similarity. You need to combine semantic relevance with domain constraints: price range, availability, category, freshness, rating. MongoDB Atlas lets you do this in the same query.
There are two approaches:
Approach 1: Filter after vector search (simpler, slightly less efficient).
Get the 100 nearest neighbors, then apply $match to narrow it down. Works well if your filters eliminate a small percentage of results.
{
$vectorSearch: { queryVector, path, k: 100 }
},
{
$match: {
category: "Footwear",
averageRating: { $gte: 4.0 },
inStock: true
}
}
Approach 2: Filter inside $vectorSearch (newer feature, more efficient).
If you're on a recent Atlas cluster, you can pass a filter directly to $vectorSearch. This pre-filters documents before returning the k nearest neighbors, reducing wasted computation:
{
$vectorSearch: {
queryVector: searchEmbedding,
path: "embedding",
k: 50,
filter: {
$and: [
{ inStock: true },
{ price: { $lte: 200 } },
{ category: "Footwear" }
]
}
}
}
Prefer approach 2 when possible—it's faster. But for complex business logic or many or-branches, approach 1 is clearer.
A typical e-commerce scenario combines both:
const results = await db.products.aggregate([
{
$vectorSearch: {
queryVector: searchEmbedding,
path: "embedding",
k: 100,
filter: { inStock: true }
}
},
{
$match: {
price: { $gte: minPrice, $lte: maxPrice },
category: { $in: selectedCategories }
}
},
{
$addFields: {
relevance: { $meta: "vectorSearchScore" }
}
},
{
$sort: { relevance: -1 }
},
{
$limit: 20
},
{
$project: {
name: 1,
price: 1,
category: 1,
relevance: 1
}
}
]).toArray();
This scales to thousands of products because vector search is efficient and the filtering is pushed down to MongoDB itself.
Common mistakes
- Forgetting to embed the query. Your user's search query needs the same embedding model as your documents. If documents are embedded with OpenAI's text-embedding-3-small, so must the query. Mismatch = bad results.
- Mismatched dimensions or similarity metric. If your index says 1536 dimensions but you send 384, the query fails. If your embeddings are cosine-normalized but you index for euclidean, results drift.
- Building the index before data exists. An empty index won't hurt, but if your collection already has 50k documents, add the index to the JSON before creating it. Rebuilding is slower.
- Treating vector search as a replacement for full-text search. Vector search shines for semantic meaning (synonyms, intent), but full-text search is still better for exact phrase matching and typo tolerance. Consider both.
- Ignoring latency of embedding generation. If you generate embeddings synchronously in your API handler, you'll slow down every search. Pre-compute embeddings or call the embedding API asynchronously.
- Setting k too low. If you return only 5 results and then filter by metadata, you might end up with 0 matches. Start with k=100 or higher and filter afterward.
Key takeaways
- Store embeddings as arrays in your documents alongside metadata; generate them before insertion, not on query.
- Create a vector index specifying the field path, similarity metric (usually cosine), and dimensions to match your embedding model.
$vectorSearchis an aggregation stage that returns the k nearest neighbors by similarity; combine it with$matchto filter by business logic.- Use
$meta: "vectorSearchScore"to access similarity scores and rank results. - Filter inside
$vectorSearchwhen possible for efficiency; filter after for clarity and flexibility. - Ensure your query embedding uses the same model and normalization as your indexed documents.
- Vector search is fast for semantic relevance; combine it with traditional filtering to meet user expectations on price, availability, and category.






Comments
Loading comments…