Chapter 07 · MongoDB Search And AI
Subchapter 7.4
references/vector-search.mdMarkdown20 KBView on GitHub
This guide covers how to configure MongoDB Vector Search indexes and construct queries for semantic similarity search.
Scope: This guide covers pure vector search indexes. For hybrid search (combining lexical and vector search), see hybrid-search.md.
MongoDB Vector Search index definitions have the following structure:
{
"fields": [
{
"type": "vector",
"path": "<field-to-index>",
"numDimensions": <number-of-dimensions>,
"similarity": "euclidean | cosine | dotProduct",
"quantization": "none | scalar | binary", // Optional
"hnswOptions": { // Optional (Preview feature)
"maxEdges": <number-of-connected-neighbors>,
"numEdgeCandidates": <number-of-nearest-neighbors>
}
},
{
"type": "filter", // Optional: for pre-filtering
"path": "<field-to-index>"
}
]
}Note: The exact syntax for creating indexes varies by driver/interface. The above shows the core index definition structure that applies across all methods.
Most vector search indexes only need the vector field:
{
"fields": [
{
"type": "vector",
"path": "<embedding-field>",
"numDimensions": <number>,
"similarity": "<similarity-function>"
}
]
}Definition: Number of dimensions in your vector embeddings. MongoDB enforces this at both index-time and query-time.
Constraints:
How to Determine:
Example - Voyage AI Models:
Definition: The similarity function used to compare vectors and rank results.
Available Options:
| Similarity | Score Formula | Score Range | Best For | Requirements |
|---|---|---|---|---|
cosine | (1 + cosine(v1,v2)) / 2 | [0, 1] | Most embedding models, normalized vectors | Cannot use zero-magnitude vectors |
dotProduct | (1 + dotProduct(v1,v2)) / 2 | [0, 1] | Most efficient - angle + magnitude | Vectors MUST be normalized to unit length |
euclidean | 1 / (1 + euclidean(v1,v2)) | [0, 1] | Spatial/geometric similarity | REQUIRED for int1 (binary) quantized vectors |
Decision Process:
dotProduct (fastest)cosineeuclideandotProduct and normalize your vectorsNotes:
dotProduct is most efficient but requires normalized vectorsDefinition: Automatic vector compression to reduce storage and improve query speed at the cost of some accuracy.
Syntax:
{
"type": "vector",
"path": "<field>",
"numDimensions": <number>,
"similarity": "<function>",
"quantization": "none | scalar | binary"
}Options:
| Type | Compression | Accuracy | Storage | Use Case |
|---|---|---|---|---|
none | 1x (no compression) | Highest | Full size | Maximum accuracy needed, small datasets (less than 1M vectors) |
scalar | 4x | High | 4x smaller | Good balance for most cases (1M-10M+ vectors) |
binary | 4-8x | Good | Maximum compression | Large datasets (10M+ vectors), speed priority |
Important Rules:
none: Default if omitted. Use for pre-quantized vectors (int1, int8)scalar: Transforms float32/double values to 1-byte integersbinary: Transforms values to single bit. numDimensions MUST be multiple of 8euclidean similarityExample:
{
"type": "vector",
"path": "plot_embedding",
"numDimensions": 1536,
"similarity": "cosine",
"quantization": {
"type": "scalar"
}
}Definition: Parameters for the Hierarchical Navigable Small Worlds graph construction algorithm.
Warning: Modifying default values might negatively impact your index and queries. Use with caution.
Syntax:
{
"type": "vector",
"path": "<field>",
"numDimensions": <number>,
"similarity": "<function>",
"hnswOptions": {
"maxEdges": <16-64>, // Default: 16
"numEdgeCandidates": <100-3200> // Default: 100
}
}Parameters:
maxEdges (16-64, default: 16):
numEdgeCandidates (100-3200, default: 100):
Recommendation: Leave at defaults unless you have specific performance requirements and understand the trade-offs.
Definition: Additional fields indexed to enable pre-filtering before vector similarity computation. This narrows the search scope and improves performance.
Use Case: Filter by specific criteria (e.g., category, date range, user ID) BEFORE computing vector similarity.
Performance: Filtering before similarity computation is much faster than post-filtering with $match.
Supported Field Types: boolean, date, objectId, numeric (int32, int64, double), string, UUID, and arrays of these types.
{
"fields": [
{
"type": "vector",
"path": "embedding",
"numDimensions": 1024,
"similarity": "cosine"
},
{
"type": "filter",
"path": "category" // String field for filtering
},
{
"type": "filter",
"path": "year" // Numeric field for filtering
}
]
}Use filter fields when:
Use post-filtering ($match) when:
MongoDB Vector Search supports the following MQL operators in the filter option:
| Type | Operators |
|---|---|
| Equality | $eq, $ne |
| Range | $gt, $lt, $gte, $lte |
| In set | $in, $nin |
| Existence | $exists |
| Logical | $not, $nor, $and, $or |
Note: Other query operators, aggregation pipeline operators, and MongoDB Search operators are NOT supported in the filter option.
Index with filter fields:
{
"fields": [
{
"type": "vector",
"path": "plot_embedding",
"numDimensions": 2048,
"similarity": "dotProduct"
},
{
"type": "filter",
"path": "genres" // String or array of strings
},
{
"type": "filter",
"path": "year" // Numeric field
}
]
}Query with single filter:
{
$vectorSearch: {
queryVector: [<array-of-numbers>],
path: "plot_embedding",
filter: {
genres: { $eq: "Action" }
},
numCandidates: 150,
limit: 10
}
}Query with multiple filters using $and:
{
$vectorSearch: {
queryVector: [<array-of-numbers>],
path: "plot_embedding",
filter: {
$and: [
{ genres: "Action" },
{ year: { $gte: 2020 } }
]
},
numCandidates: 150,
limit: 10
}
}Short form of $eq (recommended):
{
$vectorSearch: {
queryVector: [<array-of-numbers>],
path: "plot_embedding",
filter: {
genres: "Action", // Equivalent to { genres: { $eq: "Action" } }
year: { $gte: 2020 }
},
numCandidates: 150,
limit: 10
}
}Pre-filtering does NOT affect scores: The vectorSearchScore returned for documents is based only on vector similarity, not on how well they matched the filter criteria.
Filter fields must be indexed: You must add fields as type “filter” in your index definition to use them in the filter option. Fields not indexed cannot be used for pre-filtering.
Arrays are supported: You can filter on fields that contain arrays. MongoDB automatically handles array matching.
Definition: The $vectorSearch stage performs semantic search for a query vector on indexed vector fields. It must be the first stage in an aggregation pipeline.
Requirements:
$vectorSearch MUST be the first stage in the pipeline{
"$vectorSearch": {
"index": "<index-name>",
"path": "<field-to-search>",
"queryVector": [<array-of-numbers>],
"numCandidates": <number-of-candidates>,
"limit": <number-of-results>,
"filter": {<filter-specification>}, // Optional
"exact": true | false // Optional
}
}index (String, Required):
path (String, Required):
queryVector (Array of Numbers, Required):
limit (Integer, Required):
numCandidates (Integer, Conditional):
exact is false or omittedlimitlimit value for good recallExample:
{
$vectorSearch: {
queryVector: [<array>],
path: "embedding",
numCandidates: 150, // 15x the limit
limit: 10
}
}filter (Object, Optional):
exact (Boolean, Optional):
true for ENN (Exact Nearest Neighbor) searchfalse or omit for ANN (Approximate Nearest Neighbor) searchENN vs ANN:
Basic ANN query:
db.collection.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plot_embedding",
queryVector: [<1536-dimension-array>],
numCandidates: 150,
limit: 10
}
},
{
$project: {
_id: 0,
title: 1,
plot: 1,
score: { $meta: "vectorSearchScore" }
}
}
])ANN query with pre-filtering:
db.collection.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plot_embedding",
queryVector: [<2048-dimension-array>],
filter: {
$and: [
{ year: { $gte: 1955 } },
{ year: { $lt: 1975 } }
]
},
numCandidates: 150,
limit: 10
}
},
{
$project: {
_id: 0,
title: 1,
year: 1,
score: { $meta: "vectorSearchScore" }
}
}
])ENN query (exact search):
db.collection.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plot_embedding",
queryVector: [<2048-dimension-array>],
exact: true,
limit: 10
}
},
{
$project: {
_id: 0,
title: 1,
score: { $meta: "vectorSearchScore" }
}
}
])Use $meta: "vectorSearchScore" in a $project stage to include similarity scores:
{
$project: {
title: 1,
score: { $meta: "vectorSearchScore" }
}
}Important:
vectorSearchScore after a $vectorSearch stageFor ad-hoc filters or complex logic not indexed as filter fields, use $match after $vectorSearch:
db.collection.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "plot_embedding",
queryVector: [<array>],
numCandidates: 150,
limit: 50 // Get more candidates for post-filtering
}
},
{
$match: {
category: "Electronics",
"reviews.rating": { $gte: 4.5 } // Complex nested field
}
},
{ $limit: 10 }
])Performance Note: Post-filtering is slower than pre-filtering because it computes similarity for all candidates first.
Definition: The numCandidates parameter controls the trade-off between recall (finding relevant results) and query performance in ANN searches.
Rule of Thumb: A good starting point is 20x your limit value. You can adjust between 10-20x (or higher) based on your recall and performance requirements.
Example:
{
$vectorSearch: {
queryVector: [<array>],
path: "embedding",
numCandidates: 200, // 20x the limit — good starting point; tune between 10-50x based on recall and latency requirements
limit: 10
}
}Increase when:
Decrease when:
Note on low limit values: A very low limit (e.g., 5) may need proportionally higher numCandidates (e.g., 40x) to maintain recall.
ANN (Approximate Nearest Neighbor):
numCandidates parameterUse ANN when:
ENN (Exact Nearest Neighbor):
exact: true in querynumCandidates parameterUse ENN when:
Pre-filtering (filter option):
Post-filtering ($match stage):
Recommendation: Use pre-filtering whenever possible for best performance. Reserve post-filtering for complex or ad-hoc queries.
MongoDB Vector Search parallelizes query execution across segments when running on dedicated search nodes, which can improve response time for queries on large datasets.
Notes:
If you see inconsistent results: Increase numCandidates to improve consistency.
Version requirements, supported stages, limitations, and troubleshooting are identical to Atlas Search on Views — see lexical-search-indexing.md. The difference is using a vectorSearch-type index and querying with $vectorSearch.
Example: partial index (exclude documents without embeddings)
db.createView("moviesWithEmbeddings", "embedded_movies", [
{
$match: {
$expr: { $ne: [{ $type: "$plot_embedding_voyage_3_large" }, "missing"] }
}
}
])
db.moviesWithEmbeddings.createSearchIndex(
"embeddingsIndex",
"vectorSearch",
{
"fields": [
{
"type": "vector",
"numDimensions": 2048,
"path": "plot_embedding_voyage_3_large",
"similarity": "cosine"
}
]
}
)
// 8.1+: query view directly; 8.0: query source collection using index name
db.moviesWithEmbeddings.aggregate([
{
$vectorSearch: {
index: "embeddingsIndex",
path: "plot_embedding_voyage_3_large",
queryVector: [<query-vector-2048-dimensions>],
numCandidates: 100,
limit: 10
}
}
])