4.5 Atlas Search: Search Index Definitions & $search Queries
Key Takeaways
- Exam objectives 2.18 and 2.19 sit inside the 51% CRUD domain and test MongoDB Search (Atlas Search), not the MongoDB Query Language.
- A search index is a Lucene inverted index maintained asynchronously by the mongot process; it is eventually consistent and is never used by the query planner for find().
- db.collection.createSearchIndex() names the index 'default' when the name argument is omitted and defaults the type to 'search'; with dynamic: false, a mappings.fields object is required.
- $search and $searchMeta must be the first stage of an aggregation pipeline, with the sole exception of the first stage inside a $lookup sub-pipeline in MongoDB 6.0 and later.
- Inside compound, must and should contribute to the relevance score, mustNot excludes documents, and filter constrains results without affecting the score exposed via $meta: 'searchScore'.
4.5 Atlas Search: Search Index Definitions & $search Queries
The official MongoDB Associate Developer exam guide places two objectives inside the 51% CRUD domain that are not about the MongoDB Query Language at all: objective 2.18 asks you to identify the correct command for defining a search index, and objective 2.19 asks you to identify the correct search query. Both refer to MongoDB Search (historically branded Atlas Search) — a relevance-based, full-text search engine that runs alongside the database. Because these two objectives sit in the heaviest-weighted domain, skipping them is one of the most common ways well-prepared candidates lose points.
1. A Search Index Is Not a Database Index
This is the single most important distinction in this section, and the exam tests it directly.
A database index (what db.collection.createIndex() builds) is a B-tree keyed on field values. It is updated synchronously, inside the same write operation that modifies the document, and it is consumed by the query planner to turn a COLLSCAN into an IXSCAN.
A search index is an inverted index — a mapping from terms to the documents containing those terms — built on Apache Lucene. It is maintained by a separate process called mongot, which follows the collection's change stream and updates the Lucene index asynchronously. Two consequences follow directly:
- Search indexes are eventually consistent. A document you just inserted may not appear in
$searchresults for a short interval. You cannot rely on read-your-own-write behavior. - Search indexes are invisible to the query planner. Creating a search index will never speed up an ordinary
find(), andexplain()on afind()will never show it.
| Database Index | Search Index | |
|---|---|---|
| Structure | B-tree | Lucene inverted index |
| Created with | createIndex() | createSearchIndex() |
| Queried with | find(), $match | $search, $searchMeta |
| Update timing | Synchronous with the write | Asynchronous via mongot |
| Used by query planner | Yes (IXSCAN) | No |
| Relevance scoring | None | { $meta: "searchScore" } |
2. Defining a Search Index (Objective 2.18)
In mongosh, search indexes are created with db.<collection>.createSearchIndex(), available from MongoDB 7.0 (and backported to 6.0.7). The full signature takes three arguments, two of which are optional:
db.<collection>.createSearchIndex(
<name>, // optional string
<type>, // optional: "search" (default) or "vectorSearch"
{ <definition> } // required
)
Two defaults are directly examinable:
- If you omit
name, the index is nameddefault. - If you omit
type, the index type issearch.
Dynamic Mappings
The simplest definition tells MongoDB Search to index every supported field type automatically:
db.movies.createSearchIndex({ mappings: { dynamic: true } })
// -> creates a search index literally named "default"
Static Mappings
Static mappings enumerate exactly which fields are indexed and how each is analyzed. This is the production-grade choice: smaller indexes, faster builds, predictable scoring.
db.movies.createSearchIndex(
"movieTitleIdx",
{
mappings: {
dynamic: false,
fields: {
title: { type: "string" },
plot: { type: "string" },
year: { type: "number" }
}
}
}
)
Exam trap: when
mappings.dynamicisfalse,mappings.fieldsis required. A definition of{ mappings: { dynamic: false } }with nofieldsobject indexes nothing, and every$searchquery against it returns zero results — with no error to tell you why.
Search indexes can also be created from the Atlas UI (Search tab → Create Search Index, using either the Visual Editor or the JSON Editor), the Atlas CLI, Compass, the Atlas Admin API, and the official drivers. The equivalent database command is createSearchIndexes. Companion helpers are $listSearchIndexes (an aggregation stage), db.<collection>.updateSearchIndex(), and db.<collection>.dropSearchIndex().
3. Writing a Search Query (Objective 2.19)
A search query is an aggregation pipeline stage, not a find() filter. MongoDB Search provides two stages:
$search— runs the full-text query and returns the matching documents, ordered by descending relevance.$searchMeta— returns only metadata about the results (facet buckets, total counts) without returning the documents themselves.
The First-Stage Rule
$search and $searchMeta must be the first stage of the aggregation pipeline. Placing any other stage ahead of them is an error, not a performance problem. This is the most frequently tested fact about search queries:
// INVALID — $match precedes $search
db.movies.aggregate([
{ $match: { year: { $gte: 2000 } } },
{ $search: { text: { query: "space", path: "plot" } } }
])
// VALID — $search first, then filter and shape the results
db.movies.aggregate([
{ $search: { text: { query: "space", path: "plot" } } },
{ $match: { year: { $gte: 2000 } } },
{ $project: { title: 1, year: 1, score: { $meta: "searchScore" } } },
{ $limit: 10 }
])
The one narrow exception, added in MongoDB 6.0: $search may appear as the first stage inside a $lookup sub-pipeline.
Selecting the Index
If the index option is omitted, $search queries the index named default. When you created a named index, you must name it in the query — this mismatch is a classic exam distractor:
db.movies.aggregate([
{ $search: { index: "movieTitleIdx", text: { query: "space", path: "title" } } }
])
Common Search Operators
| Operator | Purpose |
|---|---|
text | Analyzed full-text match; supports fuzzy |
phrase | Ordered term sequence, with optional slop |
autocomplete | Search-as-you-type against an autocomplete-typed field |
equals | Exact match on boolean, number, date, ObjectId, or string token |
range | Numeric, date, or string bounds (gt, gte, lt, lte) |
wildcard / regex | Pattern matching against indexed terms |
exists | Documents in which the indexed path is present |
compound | Combines the above via must, mustNot, should, filter |
Compound Queries and Scoring
compound is where relevance is actually shaped, and each clause behaves differently:
must— the clause is required and contributes to the score.should— optional; every match raises the score.minimumShouldMatchsets how manyshouldclauses must match.mustNot— excludes matching documents.filter— required, but does not affect the score. Use it for hard constraints such as a category or a date window so they cannot distort ranking.
db.movies.aggregate([
{ $search: {
index: "movieTitleIdx",
compound: {
must: [ { text: { query: "space", path: "title" } } ],
should: [ { text: { query: "odyssey", path: "plot" } } ],
filter: [ { range: { path: "year", gte: 1990 } } ]
}
} },
{ $project: { title: 1, score: { $meta: "searchScore" } } }
])
Relevance is exposed through the $meta: "searchScore" expression, mirroring the way $text exposes $meta: "textScore". Results already arrive sorted by descending score, so adding { $sort: { score: -1 } } is redundant unless you are combining scores with other criteria.
4. MongoDB Search vs. $text Full-Text Search
The guide's earlier coverage of text indexes (createIndex({ field: "text" }) queried with $text) is a different feature, and the exam will offer one as a distractor for the other.
$text + text index | MongoDB Search | |
|---|---|---|
| Created by | createIndex({ f: "text" }) | createSearchIndex(...) |
| Queried by | $text inside find() or $match | $search / $searchMeta |
| Engine | MongoDB's own text index | Apache Lucene via mongot |
| Text indexes per collection | One | Many search indexes allowed |
| Relevance metadata | { $meta: "textScore" } | { $meta: "searchScore" } |
| Fuzzy / autocomplete / highlighting | Not supported | Supported |
| Faceting | Not supported | $searchMeta with facets |
| Stage position | Anywhere a filter is valid | Must be first |
How to read these questions on exam day:
- If the stem says "define a search index", the answer uses
createSearchIndex()with amappingsobject — notcreateIndex(), and not a bare field list. - If the stem shows a pipeline, check stage order first.
$searchanywhere but position one is wrong before you even read the operator. - If the definition sets
dynamic: false, confirm afieldsobject is present. - If the query names an index, confirm that name was actually created; if no name was given at creation, the index is
default. filterclauses insidecompoundare the correct tool when a constraint must not influence relevance ranking.
A developer runs db.movies.aggregate([{ $match: { year: { $gte: 2000 } } }, { $search: { text: { query: 'space', path: 'plot' } } }]) and the pipeline fails with an error. What is the defect?
A developer creates a search index with db.products.createSearchIndex({ mappings: { dynamic: true } }), omitting the name argument. A later $search stage that specifies index: 'products_idx' errors because the index does not exist. Why?
A search index is defined as { mappings: { dynamic: false } } with no other keys. The index builds without error, but every $search query against it returns zero documents. What is wrong?
Which statement correctly distinguishes a MongoDB Search index from a standard database index?