MongoDBText Indexes Deep Dive

Text Indexes

A text index enables full-text search over string content in a collection. Instead of matching exact values or using slow regex scans, a text index tokenizes strings into words, applies language-specific stemming (so running matches run), removes stop words (like the and a), and lets you search with the $text query operator.

Creating a Text Index

Use the special "text" index type instead of 1 or -1:

Single-field text index

JS
// Index the title field for full-text search
db.articles.createIndex({ title: "text" })

// Now search it
db.articles.find({ $text: { $search: "mongodb indexes" } })
Indexing Multiple Fields

A text index can cover several fields at once. A search then matches against all indexed fields:

Multi-field text index

JS
db.articles.createIndex({
  title: "text",
  body: "text",
  tags: "text"
})

// You can even index every string field in the document:
db.articles.createIndex({ "$**": "text" })  // wildcard text index
Warning
The wildcard text index ($**) is convenient but indexes every string in every document, which can make the index very large and slow down writes. Prefer listing specific fields.
Field Weights

By default every indexed field contributes equally to the relevance score. Use weights to make matches in some fields count more. Here a match in title is worth 10x a match in body:

Weighted text index

JS
db.articles.createIndex(
  { title: "text", body: "text", tags: "text" },
  {
    weights: { title: 10, tags: 5, body: 1 },
    name: "ArticleTextIndex"
  }
)
Querying with $text and $search

$text query syntax

JS
// Match documents containing "mongodb" OR "atlas" (space = OR)
db.articles.find({ $text: { $search: "mongodb atlas" } })

// Exact phrase — wrap in escaped double quotes
db.articles.find({ $text: { $search: "\"replica set\"" } })

// Exclude a term with a leading minus
db.articles.find({ $text: { $search: "mongodb -mysql" } })

// Combine with regular filters
db.articles.find({
  $text: { $search: "aggregation" },
  status: "published",
  views: { $gte: 100 }
})
Note
By default, terms separated by spaces are combined with OR, not AND. To require all terms, quote them as phrases or filter further in your application. This surprises almost everyone the first time.
Sorting by Relevance with textScore

Every $text match gets a relevance score. Project it with $meta: "textScore" and sort on it to return the best matches first:

Relevance-sorted search

JS
db.articles
  .find(
    { $text: { $search: "mongodb performance" } },
    { title: 1, score: { $meta: "textScore" } }
  )
  .sort({ score: { $meta: "textScore" } })
  .limit(5)
[
  { _id: ObjectId('...'), title: 'MongoDB Performance Tuning', score: 3.2 },
  { _id: ObjectId('...'), title: 'Improving MongoDB Query Performance', score: 2.8 },
  { _id: ObjectId('...'), title: 'Performance Basics', score: 1.1 }
]
Language Support

Text indexes support stemming and stop words for roughly 15 languages (English is the default). Set the language at index creation, or per document with a language field:

Language options

JS
// Index-wide default language
db.articles.createIndex(
  { body: "text" },
  { default_language: "german" }
)

// Per-document language override — MongoDB reads the "language" field
db.articles.insertOne({
  title: "Einführung in MongoDB",
  body: "Dokumente werden in Sammlungen gespeichert...",
  language: "german"
})

// Disable stemming and stop words entirely with "none"
db.logs.createIndex({ message: "text" }, { default_language: "none" })
Limitations
  • One text index per collection. You cannot create a second one — combine all searchable fields into a single (optionally weighted) text index.

  • A query can include only one $text expression, and it cannot appear inside $elemMatch or in every position within $or.

  • $text does no fuzzy matching — a typo like mongdb matches nothing.

  • No partial-word (prefix/infix) matching: data will not match database.

  • Sorting on regular fields cannot use the text index; that sort runs in memory.

  • Text indexes can grow large (every unique stemmed word becomes an entry) and add write overhead.

Text Index vs. Atlas Search

Capability

Text Index ($text)

Atlas Search ($search)

Engine

Built-in text index

Apache Lucene (managed)

Availability

Any MongoDB deployment

MongoDB Atlas only

Fuzzy matching / typo tolerance

No

Yes

Autocomplete

No

Yes

Relevance tuning

Field weights only

Rich scoring, boosting, functions

Facets and highlighting

No

Yes

Language analysis

Basic stemming, ~15 languages

40+ analyzers, custom analyzers

Tip
If you are on Atlas and building user-facing search, use Atlas Search — it handles typos, autocomplete, and relevance far better. Reach for $text when you self-host or only need simple keyword matching without extra infrastructure.
Complete Worked Example

Blog search end to end

JS
db.posts.insertMany([
  { title: "Indexing Strategies", body: "Compound indexes follow the ESR rule...", tags: ["indexes"] },
  { title: "Aggregation Pipeline", body: "Stages transform documents step by step...", tags: ["aggregation"] },
  { title: "Index Internals", body: "B-trees keep entries sorted for fast range scans...", tags: ["indexes", "internals"] }
])

db.posts.createIndex(
  { title: "text", body: "text", tags: "text" },
  { weights: { title: 10, tags: 5, body: 1 }, name: "PostSearch" }
)

db.posts
  .find(
    { $text: { $search: "index" } },
    { title: 1, _id: 0, score: { $meta: "textScore" } }
  )
  .sort({ score: { $meta: "textScore" } })
[
  { title: 'Indexing Strategies', score: 10.5 },
  { title: 'Index Internals', score: 8.75 }
]

Note how stemming made index, indexes, and indexing all match the single search term index, and how the title weight pushed "Indexing Strategies" to the top.

Note
To inspect or drop the text index later, use db.posts.getIndexes() and db.posts.dropIndex("PostSearch"). Naming your text index at creation time makes this much easier.