To run a full-text search in MongoDB, you have two options. For simple needs, create a text index (db.articles.createIndex({ title: "text", body: "text" })) and query it with the $text operator. For production-grade search with fuzzy matching, autocomplete, faceting, and relevance tuning, use MongoDB Atlas Search — a Lucene-powered engine queried through the $search aggregation stage. This guide covers both, with runnable mongosh and Python (PyMongo) examples, plus when to reach for an external engine like Elasticsearch.
Key takeaways
- Text indexes are built into every MongoDB deployment: one text index per collection, queried with
$text/$search, ranked with$meta: "textScore". Good for basic keyword search. - Atlas Search is the modern, recommended approach. It is built on Apache Lucene, runs on Atlas clusters, and adds fuzzy search, autocomplete, faceting, synonyms, custom analyzers, and much richer relevance scoring.
- The old
db.collection.runCommand("text", …)helper was removed in MongoDB 3.2 — use the$textquery operator instead. - Atlas Vector Search (
$vectorSearch) handles semantic/AI search and is the foundation for RAG pipelines. - Reach for Elasticsearch/OpenSearch only when you need search-specific infrastructure that lives outside your database.
Text indexes vs Atlas Search at a glance
Both let you search text inside documents, but they are very different engines. Use this table to pick the right one.
| Capability | Classic text index ($text) |
Atlas Search ($search) |
|---|---|---|
| Underlying engine | MongoDB's built-in text index | Apache Lucene |
| Where it runs | Any MongoDB deployment (self-managed or Atlas) | Atlas clusters only |
| Indexes per collection | One text index max | Many search indexes |
| Query API | $text query operator |
$search aggregation stage |
| Relevance score | $meta: "textScore" (basic TF) |
$meta: "searchScore" (full Lucene scoring, BM25) |
| Fuzzy / typo tolerance | No | Yes (fuzzy.maxEdits) |
| Autocomplete / type-ahead | No | Yes (autocomplete operator) |
| Faceting / counts | No | Yes ($searchMeta, facet collector) |
| Synonyms | No | Yes (synonym mappings) |
| Language analyzers | ~15 built-in languages, stemming, stop words | 40+ Lucene analyzers + custom analyzers |
| Phrase & negation | Yes (quotes, - prefix) |
Yes (phrase, compound.mustNot) |
| Best for | Simple keyword search, self-hosted clusters | App search, search bars, e-commerce, relevance tuning |
If you are self-hosting and only need keyword matching, the text index is enough. The moment you need typo tolerance, autocomplete, or tuned relevance, Atlas Search is the better tool.
Classic full-text search with text indexes
A text index tokenizes string fields, removes stop words, applies stemming, and stores the result so MongoDB can match search terms quickly. You create one with the text index type.
You can index a single field, several fields, or use the wildcard $** to index every string field in the collection. Remember the hard rule: a collection can have at most one text index, though that index may cover many fields.
// mongosh — sample data
db.articles.insertMany([
{ _id: 1, title: "Olivia Shakespear",
body: "Olivia Shakespear was a British novelist, playwright, and patron of the arts." },
{ _id: 2, title: "Linn-Kristin Riegelhuth Koren",
body: "A Norwegian handball player, commonly known as Linka, and a qualified nurse." }
]);
// Single-field text index
db.articles.createIndex({ title: "text" });
// Compound text index over several fields (drop the old one first —
// only ONE text index is allowed per collection)
db.articles.dropIndex("title_text");
db.articles.createIndex(
{ title: "text", body: "text" },
{ weights: { title: 10, body: 1 }, default_language: "english", name: "article_text" }
);
// Index EVERY string field with the wildcard specifier
db.articles.createIndex({ "$**": "text" });Querying with $text and ranking with textScore
You search a text index with the $text query operator. MongoDB ORs the search terms together by default, so a search for "olivia novelist" matches documents containing either word. Wrap a substring in double quotes for an exact phrase, and prefix a term with - to negate it.
To rank results by relevance, project the special $meta: "textScore" value and sort on it.
// Basic OR search
db.articles.find({ $text: { $search: "olivia novelist" } });
// Relevance: project the score and sort by it (highest first)
db.articles.find(
{ $text: { $search: "norwegian handball" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
// Exact phrase + negation: include "handball player" but exclude "nurse"
db.articles.find({ $text: { $search: "\"handball player\" -nurse" } });
// Inspect the text index that backs these queries
db.articles.getIndexes();Stemming, stop words, weights, and languages
Text indexes do real linguistic processing, which is what separates them from a plain regex scan:
- Stemming reduces words to their root, so searching
"novelists"matches"novelist". - Stop words (common words like the, and, is) are dropped from both the index and the query.
- Weights boost a field's contribution to the relevance score — a title hit can count 10x a body hit.
- Languages: text search supports ~15 languages (English, French, German, Spanish, Russian, Norwegian, and more). Set the index-wide
default_language, override it per document with alanguagefield, or pass$languageat query time. Set language to"none"to disable stemming and stop-word removal entirely.
// Per-query language (uses that language's stemmer + stop-word list)
db.articles.find({
$text: { $search: "romaner", $language: "norwegian" }
});
// Case- and diacritic-insensitive search (default in modern versions)
db.articles.find({
$text: { $search: "café", $caseSensitive: false, $diacriticSensitive: false }
});Constraints to know before you rely on $text
- One text index per collection — you cannot keep separate text indexes for different field sets.
- A query can use only one
$textexpression, and$textmust reference a field covered by the text index. - No partial-word / substring matching and no real typo tolerance —
$textmatches whole, stemmed tokens. - In a compound query,
$textcannot be combined with$orof non-text clauses in some cases, and sorting bytextScorerequires the score projection. - Relevance scoring is basic compared to Lucene's BM25.
When these limits start to hurt, that is the signal to move to Atlas Search.
Full-text search from Python (PyMongo)
The classic text-index path works identically from Python. Use the TEXT index direction constant to create the index, then query with $text and sort on the projected textScore. For the full CRUD foundation, see our guide to MongoDB CRUD operations with Python and PyMongo.
from pymongo import MongoClient, TEXT
client = MongoClient("mongodb://localhost:27017/")
db = client.test
# Create a weighted compound text index
db.articles.create_index(
[("title", TEXT), ("body", TEXT)],
weights={"title": 10, "body": 1},
default_language="english",
name="article_text",
)
# Keyword search ranked by relevance
cursor = (
db.articles.find(
{"$text": {"$search": "norwegian handball"}},
{"score": {"$meta": "textScore"}},
)
.sort([("score", {"$meta": "textScore"})])
.limit(10)
)
for doc in cursor:
print(doc["title"], "->", round(doc["score"], 3))MongoDB Atlas Search (the modern, recommended approach)
Atlas Search embeds an Apache Lucene index directly alongside your data on an Atlas cluster, kept in sync automatically through the change stream. There is no separate search server to operate, no ETL pipeline, and no second datastore to keep consistent — you query it with the same aggregation framework you already use.
Instead of a single rigid text index, you define one or more search indexes with a mapping that describes each field's type and analyzer. You can let Atlas index everything with dynamic mapping, or define static mappings for precise control. Queries run through the $search aggregation stage, which must be the first stage in the pipeline.
// 1) Search index definition (Atlas UI / API / mongosh createSearchIndex)
// Dynamic mapping indexes every supported field automatically.
{
"mappings": {
"dynamic": false,
"fields": {
"title": [
{ "type": "string", "analyzer": "lucene.english" },
{ "type": "autocomplete" }
],
"body": { "type": "string", "analyzer": "lucene.english" }
}
}
}
// 2) Query it with the $search aggregation stage (must be FIRST)
db.articles.aggregate([
{
$search: {
index: "default",
text: { query: "novelist playwright", path: ["title", "body"] }
}
},
{ $project: { title: 1, score: { $meta: "searchScore" } } },
{ $sort: { score: -1 } }
]);Fuzzy search, autocomplete, faceting, and synonyms
This is where Atlas Search leaves the classic text index far behind:
- Fuzzy matching tolerates typos via
fuzzy: { maxEdits: 1|2 }(Levenshtein edit distance), so"novlist"still finds"novelist". - Autocomplete powers type-ahead search bars with the
autocompleteoperator over anautocomplete-mapped field. - Faceting returns grouped counts (by category, brand, price bucket) via
$searchMetaor thefacetcollector — essential for e-commerce filters. - Synonyms let "laptop" and "notebook" match the same documents.
- Custom analyzers and the
compoundoperator (must,should,mustNot,filter) give Elasticsearch-level relevance control.
// Fuzzy text search (tolerates typos)
db.articles.aggregate([
{ $search: {
text: { query: "novlist", path: "body", fuzzy: { maxEdits: 2 } }
}}
]);
// Autocomplete for a search-as-you-type box
db.articles.aggregate([
{ $search: {
autocomplete: { query: "oli", path: "title" }
}},
{ $limit: 5 },
{ $project: { title: 1, _id: 0 } }
]);
// Faceted counts with $searchMeta
db.articles.aggregate([
{ $searchMeta: {
facet: {
operator: { text: { query: "novelist", path: "body" } },
facets: { langFacet: { type: "string", path: "language" } }
}
}}
]);Atlas Vector Search for semantic and AI search
Keyword search finds documents that share words; semantic search finds documents that share meaning. Atlas Vector Search stores embedding vectors next to your documents and retrieves the nearest neighbours with the $vectorSearch stage. This is the retrieval layer behind most RAG (retrieval-augmented generation) and AI assistant features. A common pattern is hybrid search — combining $search (keywords) with $vectorSearch (meaning) and fusing the scores.
// Semantic / nearest-neighbour search over an embedding field
db.articles.aggregate([
{
$vectorSearch: {
index: "vector_index",
path: "embedding", // field holding the document vectors
queryVector: [/* 1536-dim query embedding */],
numCandidates: 150,
limit: 10
}
},
{ $project: { title: 1, score: { $meta: "vectorSearchScore" } } }
]);Which one should you use?
- Use
$text(classic text index) when you self-host MongoDB or only need simple keyword matching, and you want zero extra infrastructure. It ships with every deployment. - Use Atlas Search for almost every real product search experience — search bars, e-commerce filters, fuzzy/autocomplete, and tuned relevance — without running a separate search cluster. This is the recommended default in 2026.
- Use Atlas Vector Search when you need semantic search, recommendations, or a RAG pipeline for an AI feature.
- Use Elasticsearch / OpenSearch only when you already operate a dedicated search platform, need cross-source aggregation, or have very specialized Lucene tuning that must live outside your database — at the cost of running and syncing a second system.
To go deeper on the surrounding query toolkit, see advanced queries in MongoDB and how the aggregation framework compares to $group and MapReduce — both pair naturally with $search pipelines.
Need help designing a search layer, modernizing a MongoDB app, or wiring Atlas Vector Search into a Python/Django backend? Our team builds these systems end to end — explore our Django and Python development services.
Frequently Asked Questions
How do I create a full-text search index in MongoDB?
Use db.collection.createIndex({ field: "text" }) for a single field, or pass multiple fields for a compound text index, for example db.articles.createIndex({ title: "text", body: "text" }). A collection can hold only one text index, but that index can span many fields and use per-field weights. Then query it with the $text operator.
What is the difference between $text and Atlas Search?
$text queries MongoDB's built-in text index and offers basic keyword matching with simple relevance scoring. Atlas Search is a separate, Lucene-powered engine on Atlas clusters, queried via the $search aggregation stage, and adds fuzzy matching, autocomplete, faceting, synonyms, custom analyzers, and BM25 relevance. Atlas Search is the modern recommended choice for real application search.
Does MongoDB full-text search support fuzzy matching and typo tolerance?
The classic $text index does not support fuzzy matching or substring search — it matches whole, stemmed tokens. Atlas Search does: add fuzzy: { maxEdits: 1 } or { maxEdits: 2 } to a text or autocomplete operator to tolerate typos based on Levenshtein edit distance.
How do I sort MongoDB search results by relevance?
For text indexes, project the relevance value with { score: { $meta: "textScore" } } and sort on it: .sort({ score: { $meta: "textScore" } }). For Atlas Search, results come back ordered by searchScore by default; you can also project { $meta: "searchScore" } and sort explicitly.
Can I run full-text search in MongoDB without Atlas?
Yes. Classic text indexes and the $text operator work on any self-managed MongoDB deployment, no Atlas required. Atlas Search and Atlas Vector Search, however, run only on Atlas clusters because they manage the embedded Lucene indexes for you.
When should I use Elasticsearch instead of MongoDB search?
Choose Elasticsearch or OpenSearch when you need a dedicated search platform that aggregates data from multiple sources, requires very specialized Lucene tuning, or must scale search independently of your database. For most applications already on MongoDB, Atlas Search delivers comparable relevance features without the cost of running and syncing a second system.