Text Search

Add search to your app with MongoDB: create text indexes, search words and phrases with $text, exclude terms, sort by relevance score, weight important fields, compare with regex and collation, and know when to move to Atlas Search.

Lesson 17 of 43Intermediate17 min read

Almost every application has a search box. Users type "desk lamp" and expect to find products whose name or description mentions desks and lamps, with the best matches first, even if they typed "lamps" and the product says "lamp". A simple $regex query can't do this well: it matches exact character patterns, doesn't understand words, can't rank results, and on large collections it scans every document.

MongoDB's text indexes solve the basics. A text index breaks string fields into words, removes common words such as "the" and "and", and reduces words to their stem so "lamps" and "lamp" match. The $text query operator then searches the index, supports phrases and excluded words, and gives each result a relevance score you can sort by.

For more advanced needs, such as autocomplete, typo tolerance, synonyms and highlighting, MongoDB Atlas offers Atlas Search, built on Apache Lucene. In this lesson you'll create text indexes, write $text searches, sort by relevance, weight fields, compare with other techniques, and see when to step up to Atlas Search. The examples use this collection:

MongoDB Shell
db.articles.insertMany([
  { _id: 1, title: 'Choosing a desk lamp', body: 'Bright lamps help you work at your desk without eye strain.' },
  { _id: 2, title: 'Kitchen lighting ideas', body: 'Pendant lamps and spotlights for cooking areas.' },
  { _id: 3, title: 'Desk organisation tips', body: 'Keep cables tidy and your desk clear.' },
  { _id: 4, title: 'Reading in bed', body: 'A warm bedside lamp is easier on the eyes than a ceiling light.' }
])

Text indexes and $text

MongoDB Shell
db.articles.createIndex({ title: 'text', body: 'text' })
db.articles.find({ $text: { $search: 'desk lamp' } })
  • A collection can have one text index, but it can cover several fields. { '$**': 'text' } indexes every string field.
  • $search with several words matches documents containing any of them.
  • Searches are case-insensitive and use stemming: "lamps", "lamp" and "lamping" share a stem.
  • Stop words such as "a", "the" and "and" are ignored.

Phrases, exclusions and relevance

Search stringMeaning
'desk lamp'desk or lamp
'"desk lamp"'The exact phrase (with stemming), inside escaped quotes
'lamp -kitchen'lamp, but not kitchen

Each match gets a text score based on how often and where the words appear. Project it with { score: { $meta: 'textScore' } } and sort by it with .sort({ score: { $meta: 'textScore' } }). Weights make some fields count more: createIndex({ title: 'text', body: 'text' }, { weights: { title: 5, body: 1 } }).

Other options

  • Prefix search for autocomplete-style matches: an anchored, case-insensitive regex such as { name: { $regex: '^lam', $options: 'i' } } can use a normal index when combined with a case-insensitive collation index.
  • Exact matches ignoring case: create an index with collation: { locale: 'en', strength: 2 } and query with the same collation.
  • Atlas Search adds autocomplete, fuzzy matching for typos, synonyms, highlighting, facets and better relevance through the $search aggregation stage. It's the right choice for a search experience users expect from modern sites.

MongoDB Performance Syntax

Search syntax

db.c.createIndex({ title: 'text', body: 'text' }, { weights: { title: 5 }, default_language: 'english' })
db.c.find({ $text: { $search: 'desk lamp' } })
db.c.find({ $text: { $search: '"desk lamp"' } })
db.c.find({ $text: { $search: 'lamp -kitchen' } })
db.c.find({ $text: { $search: 'lamp' } }, { score: { $meta: 'textScore' } }).sort({ score: { $meta: 'textScore' } })
db.c.find({ name: 'lamp' }).collation({ locale: 'en', strength: 2 })
db.c.aggregate([{ $search: { autocomplete: { query: 'lam', path: 'name' } } }])   // Atlas Search

MongoDB Performance Summary

  • Text indexes break strings into words with stemming and stop words; a collection has at most one.
  • $text: { $search } matches any word; quoted phrases and -words refine the search.
  • Project and sort by { $meta: 'textScore' } to rank results by relevance.
  • Weights make some fields, such as titles, count more.
  • Use regex with an index for prefixes, collation for case-insensitive equality, and Atlas Search for advanced search.

MongoDB Performance Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Searching words, phrases and exclusions

MongoDB Shell
db.articles.createIndex({ title: 'text', body: 'text' })
db.articles.find({ $text: { $search: 'lamps' } }, { _id: 1 }).sort({ _id: 1 })
db.articles.find({ $text: { $search: '"desk lamp"' } }, { _id: 1 })
db.articles.find({ $text: { $search: 'lamp -kitchen' } }, { _id: 1 }).sort({ _id: 1 })
db.articles.find({ $text: { $search: 'the and' } }).toArray().length
Output
title_text_body_text
[ { _id: 1 }, { _id: 2 }, { _id: 4 } ]
[ { _id: 1 } ]
[ { _id: 1 }, { _id: 4 } ]
0
Line-by-line explanation
  1. Line 1One text index covers both title and body.
  2. Line 2Stemming lets "lamps" match "lamp" and "lamps" in any article.
  3. Line 3Escaped quotes search for the phrase "desk lamp".
  4. Line 4A minus sign excludes articles that mention kitchen.
  5. Line 5"the" and "and" are stop words, so they match nothing.
Example 2

Ranking by relevance with weights

MongoDB Shell
db.articles.createIndex({ title: 'text', body: 'text' }, { weights: { title: 5, body: 1 }, name: 'search' })
db.articles.find(
  { $text: { $search: 'desk lamp' } },
  { _id: 0, title: 1, score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } }).toArray().map(a => `${a.title} (${a.score.toFixed(2)})`)
Output
search
[
  'Choosing a desk lamp (7.79)',
  'Desk organisation tips (3.93)',
  'Kitchen lighting ideas (0.60)',
  'Reading in bed (0.57)'
]
Line-by-line explanation
  1. Line 1Weights make words in the title count five times as much as words in the body.
  2. Lines 3 to 4$meta: 'textScore' adds the relevance score to each result.
  3. Line 4Sorting by the score puts the best match first.
  4. Line 4The exact scores depend on word counts and lengths; what matters is the order.
Example 3

Case-insensitive and prefix matching

MongoDB Shell
db.products.insertMany([{ name: 'Lamp' }, { name: 'lampshade' }, { name: 'LAMP' }, { name: 'Clamp' }])
db.products.createIndex({ name: 1 }, { collation: { locale: 'en', strength: 2 } })
db.products.find({ name: 'lamp' }, { _id: 0 }).collation({ locale: 'en', strength: 2 })
db.products.find({ name: { $regex: '^lamp', $options: 'i' } }, { _id: 0 })
db.products.find({ name: { $regex: 'lamp', $options: 'i' } }, { _id: 0 }).toArray().length
Output
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('...'),
    '1': ObjectId('...'),
    '2': ObjectId('...'),
    '3': ObjectId('...')
  }
}
name_1
[ { name: 'Lamp' }, { name: 'LAMP' } ]
[ { name: 'Lamp' }, { name: 'lampshade' }, { name: 'LAMP' } ]
4
Line-by-line explanation
  1. Line 2An index with a case-insensitive collation (strength 2 ignores case).
  2. Line 3Querying with the same collation matches lamp in any case, exactly.
  3. Line 4An anchored regex finds names starting with lamp, in any case.
  4. Line 5Without the ^ anchor, Clamp also matches, and every document must be checked.

Common MongoDB Performance Mistakes

Unanchored regex for search on big collections

Don't
db.products.find({ description: { $regex: 'lamp', $options: 'i' } })   // scans every document, no ranking
Do
db.products.createIndex({ name: 'text', description: 'text' })
db.products.find({ $text: { $search: 'lamp' } })

Creating a second text index

Don't
db.articles.createIndex({ title: 'text' })
db.articles.createIndex({ body: 'text' })        // error: only one text index per collection
Do
db.articles.createIndex({ title: 'text', body: 'text' })

Phrase search without escaped quotes

Don't
db.articles.find({ $text: { $search: 'desk lamp' } })       // any article with desk OR lamp
Do
db.articles.find({ $text: { $search: '"desk lamp"' } })     // the phrase

MongoDB Performance in Real Projects

Search in real applications

Internal tools and small sites use text indexes for article and product search, sorted by score and limited to the top 20 results. Shops and marketplaces on Atlas use Atlas Search for autocomplete as users type, typo-tolerant fuzzy matching, synonyms such as "sofa" and "couch", and facets that count results by brand and price. Larger systems sometimes sync data to Elasticsearch or OpenSearch for the same features.

MongoDB Performance FAQ

Why doesn't MongoDB $text search find partial words?

A text index stores whole words, reduced to their stems, so searching for 'lam' doesn't match "lamp" or "lampshade". For prefix matching, such as autocomplete, use an anchored regex like /^lam/i on a normal field, or Atlas Search's autocomplete feature. Text search is designed for finding documents that contain given words, not for matching fragments.

Can a MongoDB collection have more than one text index?

No, each collection can have only one text index. If you need to search several fields, put them all in that one index, optionally with weights to rank some fields higher. To change which fields are covered, drop the existing text index and create a new one.

How do I sort MongoDB text search results by relevance?

Project the score and sort on it: db.articles.find({ $text: { $search: 'lamp' } }, { score: { $meta: 'textScore' } }).sort({ score: { $meta: 'textScore' } }). Documents that mention the words more often, or in more heavily weighted fields, come first. Add a limit so you return only the best matches.

Does MongoDB text search support languages other than English?

Yes. Set default_language when creating the index, such as { default_language: 'spanish' }, so stemming and stop words follow that language's rules; then 'casa' also matches "casas". Documents can override the language with a language field, and 'none' turns off stemming and stop words entirely. Some languages, such as Chinese and Japanese, aren't supported by the built-in text index.

What is the difference between $text and $regex in MongoDB?

$text uses a text index to find documents containing whole words, with stemming, stop words and a relevance score, and it can search several fields at once. $regex matches a character pattern in one field, including partial words, but only an anchored prefix pattern can use an index efficiently. Use $text for word search and $regex for codes, prefixes and exact patterns.

MongoDB Performance Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the index definition so title becomes searchable with $text.

db.posts.createIndex({ title: '' })
Multiple choiceeasy

How many text indexes can a collection have?

Multiple choicemedium

What does $search: 'coffee -decaf' match?

Multiple choicemedium

How do you sort $text results by relevance?

Multiple choicehard

Why does a search for "running" find documents containing "runs"?

MongoDB Performance Quiz

5 questions to check what you learned.

  1. Which operator searches a text index?

    • $regex
    • $search in find
    • $text
    • $match
  2. What are stop words?

    • Words that end a search
    • Banned words
    • Words in quotes
    • Very common words such as "the" and "and" that text indexes ignore
  3. What do weights in a text index do?

    • Make matches in some fields count more towards the score
    • Limit results
    • Sort by date
    • Remove stop words
  4. Which regex can use a normal index efficiently?

    • /lamp/
    • An anchored prefix such as /^lam/
    • /.*lamp$/
    • Any regex
  5. Which feature needs Atlas Search rather than a basic text index?

    • Matching any of several words
    • Excluding words
    • Autocomplete as you type with typo tolerance
    • Case-insensitive search