Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 036 / 100

Full-text search in MongoDB: what developers should know

From text indexes to Atlas Search.

Expona Admin7 min read

Full-text search in MongoDB is often the first feature developers reach for when they need to let users find records by keywords—but it's also where teams discover that basic indexes aren't enough. From simple text queries to semantic search with embeddings, MongoDB offers a toolkit that scales from side projects to production SaaS platforms, and knowing when to reach for each tool is what separates a search experience users love from one they abandon.

Text indexes — Basic keyword search built in

Text indexes are the foundation of full-text search in MongoDB. They let you search for individual words or phrases across one or more fields without writing complex regex patterns. When you create a text index, MongoDB tokenizes the content, removes common stop words (the, a, is), and stems words so that running, runs, and run all match the same query.

Creating a text index is straightforward. Let's say we have a collection of blog posts:

javascript
db.posts.createIndex({ title: "text", body: "text" });

Now we can query it:

javascript
db.posts.find({ $text: { $search: "mongodb performance" } });

MongoDB will return any post where the title or body contains mongodb or performance. You can refine this further by using quoted phrases for exact matches or the minus operator to exclude terms:

javascript
db.posts.find({ $text: { $search: "\"query optimization\" -slow" } });

Text indexes also calculate a relevance score. Posts where the search terms appear in the title or appear closer together will rank higher. You retrieve this score using $meta: "textScore" in your projection:

javascript
db.posts.find(
  { $text: { $search: "mongodb performance" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });

The tradeoff is that text indexes are rigid. They use a fixed language (English by default), have limited relevance tuning, and don't handle fuzzy matching or typos. They're perfect for straightforward keyword search on small to medium datasets, but when you need flexibility or your users expect search features like autocomplete or misspelling tolerance, you'll outgrow them quickly.

Atlas Search — Lucene-based relevance, fuzzy matching and autocomplete

Atlas Search is MongoDB's managed search layer, powered by Apache Lucene. It's available in MongoDB Atlas (the cloud service) and gives you far more control over how search works. Instead of a simple text index, you define an Atlas Search index through a JSON configuration that lives in Atlas.

Atlas Search excels at relevance tuning and advanced matching strategies. Here's a basic index definition:

javascript
{
  "mappings": {
    "dynamic": false,
    "fields": {
      "title": {
        "type": "string",
        "analyzer": "lucene.standard"
      },
      "body": {
        "type": "string",
        "analyzer": "lucene.standard"
      },
      "category": {
        "type": "string"
      }
    }
  }
}

You query it using the aggregation pipeline with the $search stage:

javascript
db.posts.aggregate([
  {
    $search: {
      "text": {
        "query": "mongodb",
        "path": ["title", "body"]
      }
    }
  },
  { $limit: 10 }
]);

Now for the real power: fuzzy matching. If a user types "mongo" or "mongobd" (typo), Atlas Search can still find relevant results:

javascript
db.posts.aggregate([
  {
    $search: {
      "text": {
        "query": "mongobd",
        "path": "title",
        "fuzzy": {
          "maxEdits": 2
        }
      }
    }
  }
]);

For autocomplete, use the autocomplete operator instead:

javascript
db.posts.aggregate([
  {
    $search: {
      "autocomplete": {
        "query": "mon",
        "path": "title"
      }
    }
  },
  { $limit: 5 }
]);

Atlas Search also lets you boost certain fields or use boolean logic to combine queries. You can weight the title higher than the body, combine must-have terms with nice-to-have ones, and filter results before or after the search stage. For production search experiences—e-commerce sites, SaaS apps, content platforms—Atlas Search is often the sweet spot: managed infrastructure, powerful features, and pricing that scales with your data size rather than query volume.

Vector search — Semantic search with embeddings

Vector search takes a different approach. Instead of matching keywords, it matches meaning. You embed your text into a high-dimensional vector using a model like OpenAI's embeddings API or an open-source model, store that vector in MongoDB, and then search by vector similarity.

Say we embed each blog post title and body into a 1536-dimensional vector:

javascript
db.posts.insertOne({
  title: "MongoDB query optimization tips",
  body: "Learn how to write efficient queries...",
  title_embedding: [0.021, -0.102, 0.045, ...], // 1536 values
  body_embedding: [0.018, -0.095, 0.052, ...]
});

Now if a user searches for "how to speed up database queries", we embed that query and find similar vectors:

javascript
const queryEmbedding = await embedText("how to speed up database queries");

db.posts.aggregate([
  {
    $search: {
      "cosmosSearch": {
        "vector": queryEmbedding,
        "k": 10
      },
      "returnStoredSource": true
    }
  }
]);

Vector search is powerful for scenarios where keyword matching falls short: finding similar documents, content recommendation, or queries that ask conceptual questions. The tradeoff is cost (embedding APIs charge per token) and latency (you must embed the query at search time). Vector search also requires you to maintain embeddings as data changes, which adds operational complexity.

Hybrid — Combine keyword and vector results for best relevance

In production, you often want both. Keyword search is fast, precise, and catches exact matches. Vector search understands intent and handles synonym-like queries. Running both and blending the results gives users the best of both worlds.

A hybrid approach looks like this in code:

javascript
async function hybridSearch(query, limit = 10) {
  // Run keyword search
  const keywordResults = await db.posts.aggregate([
    {
      $search: {
        "text": {
          "query": query,
          "path": ["title", "body"],
          "fuzzy": { "maxEdits": 1 }
        }
      }
    },
    { $addFields: { score: { $meta: "searchScore" } } },
    { $limit: limit * 2 }
  ]).toArray();

  // Run vector search
  const queryEmbedding = await embedText(query);
  const vectorResults = await db.posts.aggregate([
    {
      $search: {
        "cosmosSearch": {
          "vector": queryEmbedding,
          "k": limit * 2
        }
      }
    },
    { $addFields: { score: { $meta: "searchScore" } } }
  ]).toArray();

  // Merge, deduplicate, and re-rank
  const merged = new Map();
  keywordResults.forEach(r => {
    merged.set(r._id.toString(), { ...r, keywordScore: r.score });
  });
  vectorResults.forEach(r => {
    const key = r._id.toString();
    if (merged.has(key)) {
      merged.get(key).vectorScore = r.score;
    } else {
      merged.set(key, { ...r, vectorScore: r.score });
    }
  });

  // Combine scores with weights
  const results = Array.from(merged.values())
    .map(r => ({
      ...r,
      finalScore: (r.keywordScore || 0) * 0.6 + (r.vectorScore || 0) * 0.4
    }))
    .sort((a, b) => b.finalScore - a.finalScore)
    .slice(0, limit);

  return results;
}

Notice we weight keyword results at 60% and vector results at 40%. You'll tune these weights based on your domain. For an e-commerce search where exact product names matter, bump keyword weight higher. For a recommendation engine, flip it toward vector scores.

Common mistakes

  • Over-indexing text fields: A text index slows writes. Only index fields users actually search. If you have 50 fields but only 3 are searchable, don't create a wildcard text index on all of them.
  • Ignoring language specifics: The default English analyzer removes stop words and stems aggressively. If your data is in French or contains technical jargon like "MongoDB," test your index with real queries before going live.
  • Not monitoring Atlas Search costs: Atlas Search charges based on index size and query volume. A large index with millions of documents can surprise you. Regularly review your Atlas Search usage and consider pagination to limit results processed.
  • Embedding everything without a strategy: Vector search is not a free lunch. Embedding 10 million documents costs money, and re-embedding after updates adds overhead. Use it only for fields where semantic similarity genuinely adds value.
  • Forgetting to normalize scores in hybrid search: Keyword and vector scores are on different scales. Normalizing them before combining prevents one signal from drowning out the other.

Key takeaways

  • Text indexes are built-in and free but rigid; use them for simple keyword search on smaller collections.
  • Atlas Search provides fuzzy matching, autocomplete, and advanced relevance tuning—it's the go-to for production search experiences in Atlas.
  • Vector search adds semantic understanding but requires embedding infrastructure and ongoing maintenance; use it when conceptual matching matters.
  • Hybrid search combines keyword and vector results for the best relevance; weight the signals based on your domain and measure the outcome.
  • Monitor costs and performance; search features scale with your data, not your revenue.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles