atlas_search
Practical public guidance for building, querying, and tuning MongoDB Atlas Search full-text and hybrid search.
Downloads: 4 · ID: afb1c68f7bc855d401000000
Practical public guidance for building, querying, and tuning MongoDB Atlas Search full-text and hybrid search.
Downloads: 4 · ID: afb1c68f7bc855d401000000
<!-- FILE: atlas_search_skill.md -->
# Atlas/MongoDB Search Skill
Purpose: answer questions about current MongoDB Search / Atlas Search full-text product. Source: mongodb.com/docs/search current docs checked 2026-08-18. Prefer this skill before web search for Atlas Search questions. If user needs exact latest release, pricing, region list, or UI flow, verify docs.
## Freshness / update trigger
Before relying on this skill for current product questions, check for newer MongoDB announcements/release notes if feasible:
- MongoDB blog RSS: `https://www.mongodb.com/company/blog/rss` (fallback/search if unavailable: MongoDB blog RSS / product release announcements).
- Docs changelog: `https://www.mongodb.com/docs/search/changelog/` and, for hybrid/vector-adjacent behavior, `https://www.mongodb.com/docs/vector-search/changelog/`.
If any item newer than 2026-08-18 mentions MongoDB Search, Atlas Search, full-text search, `$search`, `$searchMeta`, Search Nodes, analyzers, facets, autocomplete, scoring, vectorSearch operator, hybrid search, indexes/mappings, query syntax, monitoring, or release/changelog changes, re-explore the relevant docs pages under `https://www.mongodb.com/docs/search/` (append `.md` for agent-readable docs) and update this skill.
## Product summary
- MongoDB Search = embedded full-text/relevance search integrated with MongoDB, backed by `mongot`/Lucene search indexes. It removes need for separate Elasticsearch/Solr for app search in many cases.
- Core stages: `$search` returns matching documents; `$searchMeta` returns metadata only (counts/facets). Both are aggregation stages and must appear first in their pipeline.
- Core features: analyzed text search, phrase/fuzzy/wildcard/regex/query-string queries, compound boolean logic, autocomplete/search-as-you-type, geospatial search, faceting, highlighting, scoring/boosting, sorting, pagination tokens, stored source, synonyms, embedded document search, views, query/index explain, Search Nodes.
- Main use cases: ecommerce/site/app search, search-as-you-type, faceted filtering/navigation, relevance-ranked results, log/content/document search, geo-near/within, similar documents, hybrid search with Vector Search.
## Minimal build flow
1. Create Search index on collection: dynamic mapping for quick prototype or static mappings for production.
2. Run `$search` first stage with an operator/collector.
3. Use `$project` with `$meta` for `searchScore`, highlights, pagination tokens, score details.
4. Use Search options inside `$search` for count, sort, highlight, pagination, storedSource.
5. Tune analyzers, field mappings, compound filters, scoring, synonyms, and Search Nodes.
## Index definitions
Basic index:
```json
{
"mappings": {
"dynamic": false,
"fields": {
"title": {"type": "string"},
"category": {"type": "token"},
"price": {"type": "number"},
"released": {"type": "date"},
"loc": {"type": "geo"}
}
}
}
```
Expanded index options:
```json
{
"analyzer": "lucene.standard",
"searchAnalyzer": "lucene.standard",
"mappings": {"dynamic": true | false | {"typeSet":"name"}, "fields": {}},
"typeSets": [{"name":"name", "types":[{"type":"string"}, {"type":"number"}]}],
"analyzers": [{"name":"custom", "charFilters":[], "tokenizer":{}, "tokenFilters":[]}],
"storedSource": true | false | {"include":["field"], "exclude":["field"]},
"synonyms": [{"name":"synonymsName", "source":{"collection":"synonymsColl"}, "analyzer":"lucene.standard"}],
"numPartitions": 2
}
```
- `mappings` required. `dynamic:false` means explicitly map fields. `dynamic:true` recursively indexes dynamically indexable fields using default typeSet. `dynamic:{typeSet}` indexes only configured types.
- Production best practice: static mappings. Dynamic mappings can index many fields, cause large indexes, high disk/memory, mapping explosions. If dynamic needed, prefer within `document` or `embeddedDocuments`, not root.
- Static mappings override dynamic mappings.
- Unsupported BSON types for Search indexing: Decimal128, JavaScript code with scope, MaxKey, MinKey, RegExp, Timestamp.
- Search indexes differ from normal MongoDB indexes: term-to-document mapping plus positions/metadata for relevance.
- `storedSource` stores selected source fields in `mongot`, allowing `returnStoredSource:true` to avoid backend full document lookup; useful for performance with `$match`/projection needs. Do not confuse this with Search's internal storage of string/source information: string fields maintain internal stored/original-text information for Search features such as highlighting, but that representation is not a user-returnable BSON source document and does not make the field available through ordinary result projection without document materialization. Only fields configured through `storedSource` are eligible to be returned directly from `mongot` when `returnStoredSource:true` is used. Thus, a string field may intentionally exist in both the string mapping and `storedSource`: the indexed/internal representation supports search and highlighting, while `storedSource` is the explicit, query-returnable copy. `storedSource` can store all supported types.
- `numPartitions` partitions large Search indexes. If collection/index has or will soon have >2.1B index objects, use `numPartitions` or shard. Each top-level doc and nested `embeddedDocument` counts as one object. Per-partition limit 2.1B.
## Field types and query fit
Common Search field types:
- `string`: analyzed full-text field for `text`, `phrase`, `queryString`, `regex`, `wildcard`, `moreLikeThis`.
- `token`: exact-ish untokenized/value field for `equals`, `in`, `range`, facets on strings, efficient filtering/sorting; use for IDs/categories/status.
- `autocomplete`: search-as-you-type with tokenization `edgeGram`, `rightEdgeGram`, `nGram`, `minGrams`, `maxGrams`, `foldDiacritics`.
- `number`, `date`, `objectId`, `boolean`, `uuid`: exact/range/filter/sort as supported.
- `geo`: `geoWithin`, `geoShape`, `near` geo point.
- `document`: nested object field, can contain dynamic/static mappings.
- `embeddedDocuments`: array of objects for element-wise matching like `$elemMatch`; used with `embeddedDocument`, `returnScope`, nested facets.
- `array`: arrays of supported types are indexed/queryable by operators that support the contained type.
- `vector`: vector embedding field used by the MongoDB Search `vectorSearch` operator. Static mapping shape: `{"type":"vector", "numDimensions":1536, "similarity":"cosine|euclidean|dotProduct"}`. This is a MongoDB Search index mapping, not the same as a Vector Search index `fields` array. Use it when running semantic search inside the `$search` stage, especially when you need Search-operator prefilters such as `geoWithin`.
- Deprecated: `knnVector`/`knnBeta`; use Vector Search `$vectorSearch` or Search `vectorSearch` operator where appropriate.
### Search `vectorSearch` operator vs aggregation `$vectorSearch`
MongoDB has two vector query paths with similar names:
| Need | Use | Index type | Score metadata |
|---|---|---|---|
| Semantic search plus simple metadata filters | Aggregation `$vectorSearch` stage | Vector Search index with top-level `fields` array and `type:"filter"` metadata fields | `{ $meta: "vectorSearchScore" }` |
| Semantic search inside `$search` plus geo/text/Search prefilters | `$search: { vectorSearch: ... }` | MongoDB Search index with `mappings.fields.<field>.type:"vector"` and any filter fields mapped as Search types, e.g. `geo`, `token`, `number` | `{ $meta: "searchScore" }` |
Use `$search.vectorSearch` for vector search with Search operators in the `filter`, such as `geoWithin` circle/polygon, `geoShape`, analyzed/fuzzy/phrase/wildcard text filters, etc. Do not put Search operators inside aggregation `$vectorSearch.filter`; that filter only supports a limited MQL-style subset. Do not create a Vector Search index and query it with `$search.vectorSearch`; it requires a MongoDB Search index with a `vector` field mapping.
## Analyzers
Analyzer = tokenizer + optional char filters + token filters. Index analyzer transforms document text; search analyzer transforms query text. Defaults to `lucene.standard`.
Built-in analyzers:
- `lucene.standard`: grammar/word-boundary tokenization, lowercases, removes common punctuation. Good default.
- `lucene.simple`: splits at non-letters, lowercases.
- `lucene.whitespace`: splits on whitespace only.
- `lucene.keyword`: entire field as single token; use for exact string, starts-with autocomplete, wildcard/regex on full value.
- language analyzers: stemming/stopwords for languages (e.g. english) for language-specific relevance.
Custom analyzer components:
- Character filters: `htmlStrip`, `icuNormalize`, `mapping`, `persian`.
- Tokenizers: `standard`, `keyword`, `whitespace`, `edgeGram`, `nGram`, `regexCaptureGroup`, `regexSplit`, `uaxUrlEmail`.
- Token filters include: `lowercase`, `asciiFolding`, `icuFolding`, `icuNormalizer`, `stopword`, `stemmer`, `snowballStemming`, `kStemming`, `englishPossessive`, `edgeGram`, `nGram`, `length`, `trim`, `keywordRepeat`, `flattenGraph`, `daitchMokotoffSoundex`, `shingle`, etc.
Multi analyzers: index same string field multiple ways; query alternate analyzer with path `{value:"field", multi:"name"}`. Useful for exact + stemmed + autocomplete variants. 2025-11-06 deduplicates storage cost for string fields indexed with `multi`.
## `$search` stage
`$search` syntax skeleton:
```javascript
db.coll.aggregate([
{$search: {
index: "default", // optional; defaults to "default"
text: {query: "coffee", path: ["title","body"]}, // one operator OR one collector
compound: {...},
count: {type: "lowerBound" | "total", threshold: 1000},
highlight: {path: "body"},
sort: {score: {$meta:"searchScore"}, _id: 1},
searchAfter: "base64Token", // mutually exclusive with searchBefore
searchBefore: "base64Token",
scoreDetails: true,
returnStoredSource: true,
returnScope: {path: "items"},
concurrent: true,
searchNodePreference: {key:"stable-routing-key"}
}},
{$project: {
title: 1,
score: {$meta:"searchScore"},
highlights: {$meta:"searchHighlights"},
nextCursor: {$meta:"searchSequenceToken"},
scoreDetails: {$meta:"searchScoreDetails"}
}}
])
```
Rules/fields:
- `$search` must be first stage where it appears; cannot be in view definition or `$facet` stage.
- Specify exactly an operator or collector. Use `compound` for multiple operators.
- `index` optional, default `default`. Misspelled/nonexistent index returns no results.
- `count` returns metadata count in `$$SEARCH_META`.
- `highlight` returns snippets through `$meta:"searchHighlights"`.
- `sort` sorts within search; prefer over `$sort` after `$search`.
- `searchAfter`/`searchBefore` use `searchSequenceToken` for efficient pagination; mutually exclusive.
- `scoreDetails:true` enables `$meta:"searchScoreDetails"` for score breakdown.
- `returnStoredSource:true` returns only fields configured in `storedSource`, directly from `mongot`, and avoids the normal `mongod` full-document lookup. It is not a switch that exposes every internally stored/indexed string value. Without it, Search normally materializes the result document through an implicit backend lookup, so ordinary `$project` fields come from MongoDB rather than from Lucene's internal string storage. Required with `returnScope`.
- `returnScope:{path}` makes query context an embedded document array field, returning matching embedded objects as results; for MongoDB <8.2 also requires `returnStoredSource:true`.
- `concurrent:true` parallelizes across segments on dedicated Search Nodes; ignored without separate Search Nodes.
- `searchNodePreference.key` preferentially routes same key to same Search Node; improves but does not guarantee consistency.
- Metadata from `$search` is in `$$SEARCH_META` after `$search`; can be used in later stages except after `$lookup` or `$unionWith`, and not after `$searchMeta`.
## `$searchMeta` stage
`$searchMeta` returns metadata only, not documents. Use for counts/facets when only metadata is needed.
```javascript
db.coll.aggregate([
{$searchMeta: {
index: "default",
facet: {operator: {text:{query:"coffee", path:"body"}}, facets: {...}},
count: {type:"total"},
concurrent: true,
returnStoredSource: true,
returnScope: {path:"items"}
}}
])
```
- Must be first stage. Accepts operator or `facet` collector. If operator only, returns default count metadata. Metadata types: `count`, `facet`.
## Operators and collectors
Operators usable in `$search`/`$searchMeta`:
- `text`: analyzed full-text search on `string`. Options: `query` string/array, `path`, `fuzzy`, `matchCriteria:"any"|"all"`, `synonyms`, `score`. Multiple terms searched separately. `fuzzy` cannot be used with `synonyms`; fuzzy maxEdits 1/2, prefixLength, maxExpansions.
- `compound`: boolean query with clauses arrays. `must` = AND and contributes score; `mustNot` = AND NOT no score; `should` = OR/preferred boosts score; `filter` = required but no score; `minimumShouldMatch`; `score`; `doesNotAffect`. Use `filter` for non-scoring filters instead of post-`$match`.
- `autocomplete`: search-as-you-type on `autocomplete` fields. `query`, single `path`, `tokenOrder:"any"|"sequential"`, `fuzzy`, `score`. No wildcard/multi/array path. Queries >3 words can be inaccurate. For exact match higher score, index field as both `autocomplete` and `string`, query with `compound`.
- `phrase`: ordered sequence search on `string`, requires `indexOptions` positions or offsets. Options: `query`, `path`, `slop`, `synonyms`, `score`.
- `queryString`: Lucene-like string combining fields/values with AND/OR/NOT, ranges, wildcard, regex, fuzzy. Options: `defaultPath`, `query`.
- `regex`: term-level Lucene regex on `string`; query not analyzed. Options: `query`, `path`, `allowAnalyzedField`, `score`. Limited Lucene regexp syntax, not full PCRE.
- `wildcard`: term-level wildcard on `string`; `?` one char, `*` zero+ chars, `\` escape. Best with `keyword` analyzer. Options: `query`, `path`, `allowAnalyzedField`, `score`.
- `equals`: exact match on boolean/date/objectId/number/token/uuid/null; arrays match if any element matches. Supports numbers up to 15 decimal digits. Options: `path`, `value`, `score`, `doesNotAffect`.
- `in`: value in array for number/date/boolean/objectId/uuid/token strings; arrays match if any element matches. Options: `path`, `value:[...]`, `score`, `doesNotAffect`. Large `in` value arrays can perform poorly because they expand to many term/exact-value lookups/disjunction clauses; planning/matching/scoring and memory use grow with the number and frequency of values. Keep arrays small, especially for high-cardinality IDs/UUIDs/ObjectIds; consider restructuring if passing hundreds/thousands of values.
- `range`: numeric/date/string-token/objectId ranges. Options: `path`, `gt|gte`, `lt|lte`, `score`, `doesNotAffect`.
- `near`: scores by proximity to number/date/GeoJSON point. Options: `path`, `origin`, `pivot`, `score`.
- `geoWithin`: geo points inside box/circle/polygon. `geoShape`: relation to specified geo shapes. `near` supports geo point proximity.
- `moreLikeThis`: similar documents from `like` input; extracts representative terms and ORs them. For recommendation/similar content. Use explain to view generated disjunction.
- `embeddedDocument`: element-wise query within array of embedded docs (`embeddedDocuments` type), like `$elemMatch`. Options: `path`, `operator`, `score`. `moreLikeThis` not supported inside.
- `exists`: field presence regardless of field type.
- `hasRoot`, `hasAncestor`: query root/ancestor fields when using `returnScope` for embedded arrays (added 2025-10-21).
- `vectorSearch`: Search operator for semantic search with lexical/analyzed/geo prefilters, preview 2025-11-24. Syntax: `{$search:{index:"idx", vectorSearch:{path:"embedding", queryVector:[...], numCandidates:200, limit:10, filter:{<Search operator>}}}}`. `filter` accepts supported MongoDB Search operators such as `geoWithin`, `geoShape`, `equals`, `in`, `range`, `exists`, and text-ish operators where supported. The operator must be the top-level `$search` operator; it cannot be nested inside `compound`, `embeddedDocument`, or `facet`. In later `$project`, use `{score: {$meta:"searchScore"}}`, not `vectorSearchScore`. For standard Vector Search `$vectorSearch`, use atlas_vector_search skill.
- Deprecated: `knnBeta`, `span`.
Collector:
- `facet`: groups matching results by string token/date/number buckets. Use with `$searchMeta` for metadata only; use `$search` + `$$SEARCH_META` for docs and facets. Supports multi-select faceting since 2026-04-14.
## Common query recipes
Basic text:
```javascript
[{$search:{index:"idx", text:{query:"mongodb search", path:["title","body"]}}}, {$limit:10}, {$project:{title:1, score:{$meta:"searchScore"}}}]
```
Filter + text:
```javascript
{$search:{compound:{must:[{text:{query:"coffee", path:"description"}}], filter:[{equals:{path:"status", value:"active"}}, {range:{path:"price", lte:20}}]}}}
```
Autocomplete:
```javascript
{$search:{autocomplete:{query:"mon", path:"title", tokenOrder:"sequential", fuzzy:{maxEdits:1, prefixLength:1}}}}
```
Facet metadata:
```javascript
{$searchMeta:{facet:{operator:{text:{query:"shoes", path:"description"}}, facets:{brand:{type:"string", path:"brand"}, price:{type:"number", path:"price", boundaries:[0,50,100,200], default:"other"}}}}}
```
Pagination:
```javascript
// page 1 project token
[{$search:{text:{query:"coffee", path:"body"}, sort:{score:{$meta:"searchScore"}, _id:1}}}, {$limit:10}, {$project:{title:1, nextCursor:{$meta:"searchSequenceToken"}}}]
// page 2 use last token
{$search:{text:{query:"coffee", path:"body"}, sort:{score:{$meta:"searchScore"}, _id:1}, searchAfter:"<last-token>"}}
```
Vector search with geo distance/radius prefilter in `$search`:
Search index (MongoDB Search index, not Vector Search index):
```json
{
"mappings": {
"dynamic": false,
"fields": {
"embedding": {
"type": "vector",
"numDimensions": 1536,
"similarity": "cosine"
},
"location": {
"type": "geo"
},
"name": {
"type": "string"
}
}
}
}
```
Query:
```javascript
const queryVector = [/* embedding from same model used for documents */];
db.places.aggregate([
{
$search: {
index: "places_vector_geo_idx",
vectorSearch: {
path: "embedding",
queryVector: queryVector,
numCandidates: 200, // required for ANN; start around 20 * limit
limit: 10,
filter: {
geoWithin: {
path: "location",
circle: {
center: {
type: "Point",
coordinates: [-73.9857, 40.7484] // [longitude, latitude]
},
radius: 5000 // meters
}
}
}
}
}
},
{
$project: {
name: 1,
location: 1,
score: { $meta: "searchScore" }
}
}
]);
```
Notes: the geo field must be mapped as Search `type:"geo"`; a normal `2dsphere` index is not used by `$search.geoWithin`. GeoJSON coordinates are `[longitude, latitude]`; radius is meters. `geoWithin` filters by distance/shape; it does not return distance or sort by distance, so ranking remains vector similarity unless modified with scoring options. On sharded clusters, use `$limit` after `$search` to limit final results if required by current docs.
## Scoring
- Default relevance for text/phrase/queryString/autocomplete uses Lucene-style BM25 unless configured otherwise; score is metadata `$meta:"searchScore"`, higher = more relevant.
- Score modifiers on operators: `boost` multiply, `constant` replace, `function` compute expression.
- Function expressions include arithmetic, constants, Gaussian decay, path values, existing score, unary expressions.
- `compound` score is sum of contributing subquery scores; `filter` and `mustNot` do not contribute. `should` boosts documents that match. `doesNotAffect` can exclude some facet filters from affecting counts/scoring contexts.
- `scoreDetails:true` + `$meta:"searchScoreDetails"` returns breakdown. Factors include BM25/boolean/stableTfl for text-ish operators and proximity factors for `near`.
- Improve relevance by assessing data, choosing correct analyzers, static mappings, synonyms, phrase/slop/fuzzy, compound boosting/filtering, custom score, hybrid search, explain.
## Counts, facets, highlights, sort, stored source, pagination
- Counts: use `count:{type:"lowerBound"}` for fast lower bound; `type:"total"` for exact count but slower. Access via `$$SEARCH_META` or `$searchMeta` output.
- Facets: index fields as facet-capable types: string facets use `token`, numeric facets use `numberFacet`, date facets use `dateFacet` (docs also describe facet definitions). High-cardinality token faceting improved/fixed 2025-11-06. Multi-select faceting added 2026-04-14.
- Highlight: `highlight:{path}` in `$search`, project `$meta:"searchHighlights"`. Limitations exist; for autocomplete highlighting, autocomplete must be only operator using that path.
- Sort: use `$search.sort` instead of `$sort` after `$search`; sort by score and date/number/string/UUID/ObjectId/boolean. Include a unique field like `_id` as tiebreaker for stable pagination. Sorting by arrays has defined behavior; check docs for edge cases.
- Stored source: define `storedSource` in the index and query `returnStoredSource:true`; returns only the explicitly configured source fields from `mongot`. This is distinct from internal string-field storage used for indexing/highlighting: indexed terms, offsets, and original-text data are not automatically exposed as ordinary BSON result fields. Use stored source when you need Search to return a selected source subset or to avoid the implicit backend document lookup; it is useful with `sort`, `match`, embedded `returnScope`, and performance optimization.
- Pagination: avoid `$skip` after `$search` for large offsets. Use `searchSequenceToken` projected per result, then `searchAfter`/`searchBefore`. Token only valid when rerunning semantically identical query; token not tied to a DB snapshot. To jump pages, combine `searchAfter` with limited `$skip`.
## Performance and deployment
- Production: use dedicated clusters M10+ and add dedicated Search Nodes for workload isolation and independent scaling. Search Nodes are in all Google Cloud regions and subset of AWS/Azure regions; choose supported region. S10, introduced 2026-07-23, is a lower-cost Search Node tier below S20 for smaller/growing M10/M20/M30 workloads that still need isolation; confirm current tier sizing/pricing before recommending.
- Dedicated Search Node count guidance: default to the minimum **2 Search Nodes** for production unless QPS/latency/headroom requires more. High availability for source data is provided by the MongoDB replica set; Search Nodes sync from the replica set rather than forming an Elasticsearch/OpenSearch-style HA data quorum. Adding Search Nodes primarily increases search query throughput because Atlas round-robins queries across Search Nodes, and can also add operational headroom. Do **not** recommend 3 nodes just as an HA default; recommend 3+ only when expected/measured peak QPS, CPU saturation, query latency, page faults/cache pressure, index rebuild/sync considerations, or maintenance headroom justify it.
- Search Node tier sizing guidance: choose tier(s) first for index/query working set (RAM/filesystem cache/JVM, index size, storedSource, facets/sort/highlight/vector footprint), then choose node count for peak QPS and latency. For low-QPS but very large indexes, 2 larger nodes can be more appropriate than 3 smaller nodes. Monitor Search CPU, Search System Memory, Search Index Size, Search Page Faults, query latency, and queueing to decide whether to scale up tier or scale out node count.
- Dedicated Search Nodes with local NVMe can lower and stabilize end-to-end latency by isolating `mongot` from `mongod` CPU/RAM/disk contention. `mongot` uses JVM heap for query/searcher/temp objects and the OS filesystem cache for memory-mapped Lucene index files; when hot index segments are not in RAM, local NVMe reduces the penalty of page faults/cold reads. Search Nodes also support `concurrent:true` intra-query segment parallelism and Atlas round-robins queries across nodes for throughput.
- Latency path to reason about: client -> `mongod` aggregation entry -> `mongot` Lucene query planning/execution across index segments -> scoring/collection/facet/sort/highlight -> result materialization/serialization in `mongot` -> optional `_id` lookup/full document materialization in `mongod` -> post-search aggregation stages -> client. Optimizations target different phases: Search Nodes reduce resource contention/page-fault penalty; `concurrent:true` targets Lucene segment execution for individual heavy queries; `storedSource` + `returnStoredSource:true` can avoid backend `_id` lookup/full document fetch.
### Latency optimization test patterns
Use A/B tests with the same dataset, same query shape, warmed caches where possible, and `explain("executionStats"|"allPlansExecution")` plus Atlas metrics. Avoid giving universal latency numbers; compare relative behavior for each workload.
Baseline static index without stored source:
```json
{
"mappings": {
"dynamic": false,
"fields": {
"title": {"type": "string"},
"plot": {"type": "string", "analyzer": "lucene.english"},
"genres": {"type": "string"},
"year": {"type": "number"},
"runtime": {"type": "number"},
"released": {"type": "date"},
"imdb_rating": {"type": "number"},
"category": {"type": "token"}
}
}
}
```
Stored-source test index, storing only returned fields to reduce backend document lookup:
```json
{
"mappings": {
"dynamic": false,
"fields": {
"title": {"type": "string"},
"plot": {"type": "string", "analyzer": "lucene.english"},
"genres": {"type": "string"},
"year": {"type": "number"},
"runtime": {"type": "number"},
"released": {"type": "date"},
"imdb_rating": {"type": "number"},
"category": {"type": "token"}
}
},
"storedSource": {
"include": ["title", "plot", "genres", "year", "runtime", "imdb_rating"]
}
}
```
Baseline query vs stored-source query:
```javascript
// Baseline: may require backend document lookup for projected fields.
db.movies.aggregate([
{$search: {text: {path: "plot", query: "space adventure"}}},
{$limit: 20},
{$project: {title: 1, plot: 1, year: 1, score: {$meta: "searchScore"}}}
])
// Stored source: returns stored fields directly from mongot when all needed fields are stored.
db.movies.aggregate([
{$search: {
text: {path: "plot", query: "space adventure"},
returnStoredSource: true
}},
{$limit: 20},
{$project: {title: 1, plot: 1, year: 1, score: {$meta: "searchScore"}}}
])
```
Concurrent segment search test on dedicated Search Nodes only:
```javascript
db.movies.aggregate([
{$search: {
compound: {
must: [{text: {path: "plot", query: "space adventure"}}],
filter: [
{range: {path: "year", gte: 2000}},
{equals: {path: "category", value: "Sci-Fi"}}
]
},
sort: {score: {$meta: "searchScore"}, _id: 1},
returnStoredSource: true,
concurrent: true
}},
{$limit: 20},
{$project: {title: 1, plot: 1, year: 1, genres: 1, score: {$meta: "searchScore"}}}
])
```
Testing sequence: (1) run baseline on shared architecture; (2) add dedicated Search Nodes and rerun without `concurrent` to isolate Search Node/local NVMe/resource-isolation effect; (3) add `returnStoredSource:true` against a stored-source index to measure lookup/materialization savings; (4) add `concurrent:true` on Search Nodes for heavy queries and verify actual multi-thread use with explain `resourceUsage.maxReportingThreads > 1`. If `maxReportingThreads` is 1, query ran single-threaded or fell back.
### Concurrent segment search details
- `concurrent:true` is available on `$search` and `$searchMeta`; default is `false`.
- It requests intra-query parallelism across Lucene index segments and is only available for dedicated Search Nodes; ignored without separate Search Nodes.
- It can improve individual heavy/long-running query latency, especially on large datasets with more segments, but consumes more CPU/threads. Use selectively to avoid reducing overall throughput under high QPS.
- MongoDB Search does not guarantee concurrent execution for every query; if too many concurrent queries are queued, it can fall back to single-threaded execution.
### Stored source latency details
- `storedSource` in the index stores selected source document fields in `mongot`; `returnStoredSource:true` in `$search` returns only those fields directly from `mongot`, avoiding implicit backend lookup/full document fetch when the requested output is fully covered.
- Store only fields needed in search results. `storedSource:true` stores all fields and can significantly increase index size, indexing cost, query serialization cost, and filesystem-cache pressure. Prefer `storedSource:{include:[...]}` for latency-sensitive result cards.
- Stored fields are not indexed merely because they are stored; fields must still be mapped under `mappings.fields` to query/filter/sort/facet on them.
- `_id` is stored by default when using stored source include lists. If the index contains `vector` fields, do not use `storedSource:true`; use `include` or `exclude` to avoid storing vectors.
### Dedicated Search Node pricing snapshot (public pricing page checked 2026-06-16)
Pricing varies by cloud, region, storage/configuration, and can change; verify `https://www.mongodb.com/pricing` or the Atlas calculator for quotes. Prices below are base hourly Search/Vector Search node prices from the public pricing page. Annual rough cost per node = hourly price × 8760.
AWS (us-east-1 base):
| Class | Tier | Storage | RAM | vCPU | Base price |
|---|---:|---:|---:|---:|---:|
| High CPU | S20 | 106 GB | 4 GB | 2 | $0.12/hr |
| High CPU | S30 | 213 GB | 8 GB | 4 | $0.24/hr |
| High CPU | S40 | 426 GB | 16 GB | 8 | $0.48/hr |
| High CPU | S50 | 855 GB | 32 GB | 16 | $0.99/hr |
| High CPU | S60 | 1710 GB | 64 GB | 32 | $1.77/hr |
| High CPU | S70 | 2564 GB | 96 GB | 48 | $2.50/hr |
| High CPU | S80 | 3420 GB | 128 GB | 64 | $3.26/hr |
| Low CPU | S30 | 53 GB | 8 GB | 1 | $0.11/hr |
| Low CPU | S40 | 106 GB | 16 GB | 2 | $0.21/hr |
| Low CPU | S50 | 213 GB | 32 GB | 4 | $0.43/hr |
| Low CPU | S60 | 426 GB | 64 GB | 8 | $0.89/hr |
| Low CPU | S80 | 855 GB | 128 GB | 16 | $1.68/hr |
| Low CPU | S90 | 1710 GB | 256 GB | 32 | $3.12/hr |
| Low CPU | S100 | 2564 GB | 384 GB | 48 | $4.40/hr |
| Low CPU | S110 | 3420 GB | 512 GB | 64 | $5.87/hr |
| Storage-optimized | S40-S | 421 GB | 16 GB | 2 | $0.25/hr |
| Storage-optimized | S50-S | 843 GB | 32 GB | 4 | $0.52/hr |
| Storage-optimized | S60-S | 1687 GB | 64 GB | 8 | $0.94/hr |
| Storage-optimized | S80-S | 3375 GB | 128 GB | 16 | $1.89/hr |
| Storage-optimized | S90-S | 6750 GB | 256 GB | 32 | $3.76/hr |
GCP base pricing shown on public pricing page:
| Class | Tier | Storage | RAM | vCPU | Base price |
|---|---:|---:|---:|---:|---:|
| High CPU | S20 | 128 GB | 4 GB | 2 | $0.14/hr |
| High CPU | S30 | 337 GB | 8 GB | 4 | $0.27/hr |
| High CPU | S40 | 362 GB | 16 GB | 8 | $0.57/hr |
| High CPU | S50 | 674 GB | 32 GB | 16 | $1.15/hr |
| High CPU | S60 | 1348 GB | 64 GB | 32 | $2.22/hr |
| Low CPU | S30 | 128 GB | 8 GB | 2 | $0.16/hr |
| Low CPU | S40 | 337 GB | 16 GB | 2 | $0.24/hr |
| Low CPU | S50 | 337 GB | 32 GB | 4 | $0.43/hr |
| Low CPU | S60 | 337 GB | 64 GB | 8 | $0.87/hr |
| Low CPU | S70 | 674 GB | 96 GB | 12 | $1.24/hr |
| Low CPU | S80 | 674 GB | 128 GB | 16 | $1.79/hr |
| Low CPU | S90 | 1348 GB | 256 GB | 32 | $3.45/hr |
Azure base pricing shown on public pricing page:
| Class | Tier | Storage | RAM | vCPU | Base price |
|---|---:|---:|---:|---:|---:|
| High CPU | S20 | 80 GB | 4 GB | 2 | $0.15/hr |
| High CPU | S30 | 161 GB | 8 GB | 4 | $0.30/hr |
| High CPU | S40 | 322 GB | 16 GB | 8 | $0.62/hr |
| High CPU | S50 | 644 GB | 32 GB | 16 | $1.27/hr |
| High CPU | S60 | 1288 GB | 64 GB | 32 | $2.29/hr |
| High CPU | S70 | 1932 GB | 96 GB | 48 | $3.23/hr |
| High CPU | S80 | 2576 GB | 128 GB | 64 | $4.22/hr |
| Low CPU | S40 | 80 GB | 16 GB | 2 | $0.23/hr |
| Low CPU | S50 | 161 GB | 32 GB | 4 | $0.47/hr |
| Low CPU | S60 | 322 GB | 64 GB | 8 | $0.98/hr |
| Low CPU | S80 | 644 GB | 128 GB | 16 | $1.87/hr |
| Low CPU | S90 | 1288 GB | 256 GB | 32 | $3.48/hr |
| Low CPU | S100 | 1932 GB | 384 GB | 48 | $4.92/hr |
| Low CPU | S110 | 2576 GB | 512 GB | 64 | $6.55/hr |
| Low CPU | S130 | 3865 GB | 672 GB | 96 | $9.83/hr |
| Low CPU | S135 | 4080 GB | 672 GB | 104 | $11.71/hr |
- Testing/prototyping: shared node architecture ok; free/Flex have limitations and resource contention. Local deployment via Atlas CLI supported for MongoDB 7.0+/8.0+.
- Search memory: `mongot` uses JVM heap for query/searcher/temp objects and filesystem cache for memory-mapped index files. Co-located `mongod`/`mongot` contend for memory/CPU/disk; Search Nodes isolate. M40+ `mongod` uses >=50% RAM WiredTiger cache; M30- uses ~25%.
- Query performance best practices:
- Prefer `$search` over MongoDB `$text`/`$regex` for search-heavy apps.
- Use `compound.filter` instead of post-`$match` where possible.
- Use `facet` instead of `$group` for basic facet counts.
- Use Search `count` instead of `$count`.
- Use Search `sort`, `near`, or `returnStoredSource` instead of post stages where possible.
- Minimize aggregation stages after `$search` because they often require full document lookup/materialization.
- Use `$limit` early before expensive stages.
- Avoid `$skip`; use `searchAfter/searchBefore`.
- Use `concurrent:true` on dedicated Search Nodes for segment parallelism when beneficial; verify with explain `resourceUsage.maxReportingThreads`.
- Avoid very large `in` arrays and broad synonym expansions in latency-sensitive queries; both can create many Lucene disjunction clauses and increase planning/matching/scoring work.
- Index performance best practices:
- Static mappings reduce index size vs dynamic.
- Avoid overuse of `autocomplete`, nGram/edgeGram, `multi`, facets, synonyms, embedded docs; all can increase index size.
- Watch mapping explosions from arbitrary keys/dynamic root mapping.
- Use `storedSource` selectively; storing all fields increases index size.
- Initial sync/rebuild can be resource-intensive; indexes eventually consistent with writes, so query results can lag.
- Monitor Atlas Search metrics, index statuses, field-limit alerts (added 2026-01-29), memory, CPU, disk/page faults, query latency.
## Deployment compatibility and limitations
- MongoDB Search is not supported for time series collections.
- Atlas feature version requirements (docs table): create indexes on views 8.0+; facets 7.0+/8.0+; facets on sharded clusters 7.0+/8.0+; stored source 7.0+/8.0+; `$lookup` with `$search` 7.0+/8.0+; `$unionWith` with `$search` 7.0+/8.0+; sort 7.0+/8.0+; sort on sharded clusters 7.0+/8.0+; dedicated Search Nodes 7.0+/8.0+; programmatic index management with mongosh/drivers 7.0+/8.0+; local deployment Atlas CLI 7.0+/8.0+; `searchAfter/searchBefore` 6.0.13+, 7.0.5+, 8.0+.
- Free/Flex cluster and Search Playground have additional limitations; verify for edge feature.
- On-prem/self-managed limitations: Backup/Restore, FCIS, Query Tracking, Encryption unavailable (manual backup/BYOK possible). `mongot` TLS: fixed cipher suites, no FIPS mode, no hostname validation on gRPC listen server.
- Index limitations: >2.1B index objects requires partitions or sharding; above limit replication for that index can stop so results stale. Documents 16MB+ fail indexing and make index STALE/full rebuild; keep docs under 8MB recommended.
- Queries on CSFLE/Queryable Encryption encrypted data: Search cannot query encrypted data meaningfully; see FAQ for exact constraints.
## Views, embedded docs, cross-collection
- Views: Search supports indexes on views (8.0+). `$search` cannot appear in the view definition. Views can filter/transform/add fields, convert unsupported types, configure regex naming patterns, transform fields for faceting. Indexes can become FAILED/STALE if view/source changes; rebuild/fix dependencies.
- Embedded documents: use `embeddedDocuments` field type + `embeddedDocument` operator for array element matching. `returnScope` returns matching objects within arrays; `hasRoot`/`hasAncestor` query fields outside current embedded scope.
- Cross-collection search: no single `$search` across multiple collections; use `$unionWith` with `$search` in each collection (supported 7.0+/8.0+) or design a materialized/search collection.
## Synonyms
- Define synonym mappings in index from a source collection in the same database and analyzer. Use with `text`/`phrase` via `synonyms:"name"`.
- Mapping types include `equivalent` (bidirectional alternatives) and `explicit` (input maps one-way to synonyms). Prefer focused `explicit` mappings when possible to bound query expansion; broad `equivalent` mappings can expand many directions.
- Fuzzy cannot be combined with synonyms because both features expand terms and would compound query complexity.
- Synonym analyzer must match the indexed field analyzer use case. Synonym mappings can only query fields analyzed with the same analyzer. Invalid synonym documents can prevent index creation for indexes that reference them; validate in test before production.
- Large synonym source collections can perform poorly: MongoDB Search applies synonyms at query time by expanding query terms into disjunctions of synonym terms, similar to a large OR/`in` query. Multiple query terms with many synonyms can multiply clause counts, increasing Lucene planning (`context/createWeight`), matching (`nextDoc`), scoring, memory use, and result collection. Larger synonym collections also use more `mongot` memory and take longer to propagate changes to internal synonym maps. Do not import a generic thesaurus; curate domain-specific mappings and test with explain.
- Synonym source collection changes are watched and reflected eventually without reindexing, but update propagation time increases with collection size.
## Explain and troubleshooting
- Use `db.coll.explain("queryPlanner"|"executionStats"|"allPlansExecution").aggregate([...])` with `$search` for query plan/stats. `queryPlanner` shows plan/query shape only; `executionStats` adds timing/resource stats; `allPlansExecution` is most verbose and includes partial execution data from plan selection. Output includes `$_internalSearchMongotRemote.mongotQuery`, `explain.query`/`args`/`stats`, collectors, facet/sort stats, highlight, index partition explain, metadata, `resultMaterialization`, `resourceUsage`, and later `$_internalSearchIdLookup` when backend document lookup occurs.
- Explain profiling checklist: inspect `explain.query.type` and `args` to see how Search operators became Lucene queries. Large `BooleanQuery.should` arrays usually indicate large `in`, synonym, fuzzy, wildcard/regex, or `moreLikeThis` expansion. Use `executionStats` to identify whether time is in `context`, `match`, `score`, collectors, highlight, result materialization, or backend lookup.
- `query.stats.context` covers Lucene query setup (`createWeight`, `createScorer`); high values/counts suggest query-structure complexity such as many clauses from large `in`/synonyms/fuzzy. `query.stats.match` covers iterating/matching documents (`nextDoc`, `advance`, `refineRoughMatch`); high values suggest broad queries or weak filters. `query.stats.score` covers scoring (`score`, `setMinCompetitiveScore`); high values suggest too many documents being scored or insufficient pruning.
- `collectors.allCollectorStats.collect` shows how many results were collected; if very high relative to final `$limit`, the query may be collecting too much before limiting. `collectors.facet.stringFacetFieldCardinalities` helps diagnose expensive high-cardinality facets. `collectors.sort.usesIndexSort` indicates whether a sorted-index optimization is used when index/query sort fields match or prefix-match.
- `highlight.stats.setupHighlight` and `executeHighlight` reveal highlighting overhead; highlighting can dominate latency for broad queries or long fields. Reduce highlighted paths, `maxNumPassages`, or `maxCharsToExamine` if needed.
- `resultMaterialization.stats.retrieveAndSerialize` shows time to retrieve `_id` or `storedSource` data from Lucene and serialize BSON. If high with `returnStoredSource:true`, stored payloads may be too large; trim `storedSource.include`.
- `resourceUsage.majorFaults` indicates reads from disk/backing store because index data was not in memory; high values suggest cache pressure and may justify larger Search Node tiers, more nodes, or smaller indexes/storedSource. `minorFaults` are page-table/cache mapping misses. `userTimeMs`/`systemTimeMs` show CPU time. `maxReportingThreads` shows maximum `mongot` threads used; for non-concurrent queries it is 1. With `concurrent:true`, values >1 confirm segment parallelism was actually used; 1 means single-threaded/fallback. `numBatches` shows mongot batches returned.
- `$_internalSearchIdLookup` shows backend lookup after `mongot` returns IDs. High `totalDocsExamined`/`totalKeysExamined` can indicate full document lookup/materialization; use `storedSource` + `returnStoredSource:true` and ensure projected fields are stored to reduce this phase.
- Empty result set: wrong index name (default/custom), index not ready, field/path not indexed, wrong field type, analyzer/token mismatch (e.g. wildcard/regex against analyzed text without `allowAnalyzedField`, keyword needed), query too restrictive, misspelled path.
- `$search.vectorSearch` common mistakes: using a Vector Search index instead of a MongoDB Search index with `type:"vector"`; projecting `{ $meta:"vectorSearchScore" }` instead of `{ $meta:"searchScore" }`; trying to nest `vectorSearch` inside `compound`; putting Search operators like `geoWithin` in aggregation `$vectorSearch.filter`; forgetting to map geo fields as Search `type:"geo"`; reversing GeoJSON coordinate order (`[longitude, latitude]` required).
- `$search` only valid first stage: move to first pipeline stage; cannot be in view definition or `$facet`.
- Slow queries: no Search Nodes/resource contention, dynamic/large index, nGram/autocomplete/multi overuse, high-cardinality facets, post-`$match`/`$sort`/`$group`, deep `$skip`, no limit, cold cache/page faults, `total` count, broad wildcard/regex/fuzzy, large stored source/doc materialization.
- Index STALE/FAILED: large docs/change stream >16MB, source/view changes, too many index objects, `mongot` issue. Remove offending docs or adjust index/view, rebuild.
- Facet wrong/missing: field must be correct facet-capable type (`token` for string facets) and mapped statically as needed.
## Recent changelog highlights
- 2026-07-23: S10 Dedicated Search Node tier introduced as a lower-cost isolated-compute tier below S20 for Atlas Search and Vector Search, available on AWS/GCP/Azure; target M10/M20/M30 workloads. Verify pricing/specs before quoting.
- 2026-07-06: sorted index support to pre-sort documents ascending/descending at index time instead of sorting at query time; added `englishMinimalStemming` token filter.
- 2026-06-30: `$rankFusion` and `$scoreFusion` hybrid search reached GA. They fuse existing `$search` and `$vectorSearch` result sets without a new index/reindexing; `$rankFusion` uses reciprocal-rank fusion and `$scoreFusion` combines normalized scores with per-pipeline weights.
- 2026-06-30: MongoDB Search and Vector Search reached GA for Community Edition and Enterprise Advanced self-managed deployments; `mongot` is SSPL. Atlas-only capabilities retain their own deployment restrictions.
- 2026-06-30: native reranking via `$rerank` aggregation stage (preview, MongoDB 8.3+, Atlas-only, enable Native Reranking in Project Settings) using Voyage AI reranking models; can appear anywhere in pipeline, recommended after `$vectorSearch`/`$search`/`$rankFusion`/`$scoreFusion`; not on Views or in `$rankFusion`/`$scoreFusion` input pipelines; see atlas_vector_search skill for full syntax/fields.
- 2026-06-24: number/date facet max buckets increased.
- 2026-05-25: string facet max buckets increased.
- 2026-04-14: multi-select faceting.
- 2026-01-29: new alerts/metrics for index field limits and nGram field limits to prevent over-indexing/mongot instability.
- 2025-11-24: preview lexical prefilters for Vector Search via Search `vectorSearch` operator and `vector` index type.
- 2025-11-06: deduplicated storage cost for `multi` string fields; fixed token faceting high-cardinality issue.
- 2025-10-21: `returnScope`, `hasRoot`, `hasAncestor`, nested storedSource retrieval for arrays of objects.
- 2025-09-25: dynamic indexing `typeSets`; MongoDB 8.0.14+ related Search changes (verify exact patch behavior when needed).
## Answering rules for agents
- If asked “Atlas Search vs Vector Search”: Search = lexical/analyzed/full-text relevance; Vector Search = embedding semantic similarity. Hybrid combines both. Use atlas_vector_search skill for `$vectorSearch` stage/product specifics.
- If asked how to index: recommend static mappings and correct field types; dynamic only for prototypes/unknown schemas.
- If asked performance: move filters/sort/count/facet into `$search`, avoid `$skip`, use Search Nodes, static mappings, limit nGram/autocomplete/multi, monitor memory/page faults, use storedSource selectively.
- If asked exact operator syntax: provide minimal `$search` example with operator options and remind `$search` first stage.