All skills
Skillintermediate

Lexical Search - Querying

This guide covers query patterns and optimization techniques for MongoDB Atlas Search.

Claude Code Knowledge Pack7/10/2026

Overview

Lexical Search - Querying

This guide covers query patterns and optimization techniques for MongoDB Atlas Search.

Table of Contents


$search vs $searchMeta

Both stages must be the first stage in an aggregation pipeline.

StageUse When
$searchYou need matching documents, with or without metadata
$searchMetaYou only need metadata (count, facets) — no documents returned

$searchMeta shares the following fields with $search: index, all operator names (e.g. text, range, compound), concurrent (parallelizes search across segments on dedicated search nodes only — ignored otherwise), and returnStoredSource.


Query Patterns

Operator Reference

OperatorDescription
autocompleteSearch-as-you-type from incomplete input
compoundCombines multiple operators into a single query
embeddedDocumentQueries fields inside arrays of objects
equalsExact match on boolean, date, number, objectId, token, uuid
existsTests for presence of a field
geoShapeQueries shapes by spatial relation (geo type, indexShapes: true)
geoWithinQueries points within a region (geo type)
hasAncestorQueries ancestor-level fields when using returnScope
hasRootQueries root-level fields when using returnScope
inQueries single values or arrays of values
moreLikeThisFinds documents similar to a given document
nearQueries values near a number, date, or geo point
phraseSearches for terms in a specific order
queryStringBoolean/field-specific query syntax
rangeQueries values within a numeric, date, string, or objectId range
regexRegular expression matching on string fields
textFull-text analyzed search on string fields
vectorSearchSemantic search with lexical pre-filters (vector type in search index)
wildcardWildcard pattern matching on string fields

Count Results

Use the count option in $searchMeta to count matching documents without fetching them. Also works in $search via the $SEARCH_META aggregation variable when you need both results and count.

// Count only (recommended)
db.movies.aggregate([
  {
    $searchMeta: {
      range: { path: "year", gte: 2010, lte: 2015 },
      count: { type: "lowerBound" }  // or "total" for exact count
    }
  }
])
// Returns: { count: { lowerBound: NumberLong(1001) } }
// Count alongside results using $SEARCH_META
db.movies.aggregate([
  {
    $search: {
      text: { path: "title", query: "<query>" },
      count: { type: "total" }
    }
  },
  { $project: { title: 1, meta: "$SEARCH_META" } },
  { $limit: 10 }
])
typeBehavior
lowerBoundApproximate. Exact up to threshold (default 1000), rough above it.
totalExact count. Slower on large result sets.

Note: Count affects performance — use only when needed (e.g., first page of paginated results).


Pagination with searchSequenceToken

Cursor-based pagination using tokens. More efficient than $skip alone for deep pagination.

Step 1 — Get tokens from the initial query:

db.movies.aggregate([
  {
    $search: {
      index: "<index-name>",
      text: { path: "title", query: "summer" },
      sort: { released: 1, _id: 1 }  // Sort on a unique field to prevent tie-ordering issues
    }
  },
  { $limit: 10 },
  {
    $project: {
      title: 1, released: 1,
      paginationToken: { $meta: "searchSequenceToken" }
    }
  }
])

Step 2 — Next page using searchAfter:

db.movies.aggregate([
  {
    $search: {
      index: "<index-name>",
      text: { path: "title", query: "summer" },
      searchAfter: "<token-from-last-document-on-previous-page>",
      sort: { released: 1, _id: 1 }  // maintain the same sort order
    }
  },
  { $limit: 10 },
  { $project: { title: 1, paginationToken: { $meta: "searchSequenceToken" } } }
])

Use searchBefore with the first document's token on the current page to go to the previous page — results are returned in reverse order. Combine searchAfter with $skip to jump pages.

Key constraint: Query semantics (operator, path, query value, sort) must be identical between the initial query and any searchAfter/searchBefore query.


Retrieve Arrays of Objects with returnScope

Return each element of an embedded document array as an individually scored document. Works in both $search and $searchMeta.

Requirements:

  • Array field indexed as embeddedDocuments type with storedSource defined on the fields to return
  • returnStoredSource: true in the query
  • All operator paths must be nested under returnScope.path (use hasAncestor or hasRoot to query outside it)

Index:

{
  "mappings": {
    "dynamic": false,
    "fields": {
      "funding_rounds": {
        "type": "embeddedDocuments",
        "dynamic": true,
        "storedSource": {
          "include": ["round_code", "raised_currency_code", "raised_amount"]
        }
      }
    }
  }
}

Query:

db.companies.aggregate([
  {
    $search: {
      range: { path: "funding_rounds.raised_amount", gte: 5000000, lte: 10000000 },
      returnStoredSource: true,
      returnScope: { path: "funding_rounds" }
    }
  },
  { $limit: 5 }
])

Only fields defined in storedSource within the embedded document are returned — root-level fields are excluded. When returnScope is specified, all query paths must start with returnScope.path.


Advanced Query Syntax (queryString)

Use case: Complex search with boolean operators, wildcards, and field-specific queries.

Fields configuration:

// Add to mappings.fields in your index:
{
  "title": { "type": "string" },
  "director": { "type": "string" },
  "year": { "type": "number" }
}

Query patterns:

// Boolean operators
db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      queryString: {
        defaultPath: "title",
        query: "detective AND (noir OR thriller) NOT comedy"
      }
    }
  }
])

// Field-specific searches
db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      queryString: {
        defaultPath: "title",
        query: "title:inception AND director:nolan"
      }
    }
  }
])

// Wildcards and ranges
db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      queryString: {
        defaultPath: "title",
        query: "star* AND year:[2010 TO 2020]"
      }
    }
  }
])

Supported syntax:

  • Boolean: AND, OR, NOT
  • Grouping: (term1 OR term2)
  • Wildcards: * (0+ chars), ? (single char)
  • Ranges: [min TO max] for numbers/dates
  • Field-specific: fieldName:value

Key considerations:

  • Great for building search UIs with advanced options
  • Users can construct complex queries without API changes
  • Validate/sanitize user input to prevent injection

Searching Nested Arrays (embeddedDocument)

Use case: Search within arrays of objects where element-wise comparisons are required (similar to $elemMatch), or each element must be scored independently.

Fields configuration:

// Add to mappings.fields in your index:
{
  "title": { "type": "string" },
  "reviews": {
    "type": "embeddedDocuments",  // Required for array search
    "fields": {
      "author": { "type": "string" },
      "text": { "type": "string" },
      "rating": { "type": "number" }
    }
  }
}

Query pattern:

db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      embeddedDocument: {
        path: "reviews",
        operator: {
          compound: {
            must: [
              { text: { query: "excellent", path: "reviews.text" } }
            ],
            filter: [
              { range: { path: "reviews.rating", gte: 4 } }
            ]
          }
        },
        score: { embedded: { aggregate: "maximum" } }  // or sum, minimum, mean
      }
    }
  }
])

Score aggregation options:

  • sum: Add scores from all matching array elements
  • maximum: Use highest score from array elements
  • minimum: Use lowest score from array elements
  • mean: Average scores from array elements

Key considerations:

  • Each array element is indexed as a separate document
  • Use embeddedDocuments field type, not regular document
  • Score aggregation controls how array matches affect overall document score
  • Performance can be degraded due to complexity of parent-child joins

Search Highlighting

Use case: Show users which parts of documents matched their query.

Fields configuration:

// Add to mappings.fields in your index:
{
  "title": { "type": "string" },
  "plot": { "type": "string" }
}

Query pattern:

db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      text: {
        query: "detective noir",
        path: "plot"
      },
      highlight: {
        path: "plot",
        maxCharsToExamine: 500000,  // Default
        maxNumPassages: 5            // Number of snippets
      }
    }
  },
  {
    $project: {
      title: 1,
      plot: 1,
      highlights: { $meta: "searchHighlights" },
      score: { $meta: "searchScore" }
    }
  }
])

Highlight result structure:

{
  "highlights": [
    {
      "path": "plot",
      "texts": [
        { "value": "A ", "type": "text" },
        { "value": "detective", "type": "hit" },
        { "value": " investigates a murder in ", "type": "text" },
        { "value": "noir", "type": "hit" },
        { "value": " Los Angeles", "type": "text" }
      ],
      "score": 1.23
    }
  ]
}

Key considerations:

  • type: "hit" indicates matched terms
  • type: "text" is surrounding context
  • Multiple passages returned for long documents
  • Use in search results UI to show match context

Compound Queries

Compound queries combine multiple operators efficiently:

db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      compound: {
        must: [
          { text: { query: "detective", path: "plot" } }  // Required, affects score
        ],
        should: [
          { text: { query: "mystery", path: "genre" } }   // Optional, boosts score
        ],
        filter: [
          { range: { path: "year", gte: 2000 } }          // Required, no score impact
        ],
        mustNot: [
          { text: { query: "comedy", path: "genre" } }    // Excludes results
        ]
      }
    }
  }
])

Clause types:

  • must: Required matches that affect scoring
  • should: Optional matches that boost scores
  • filter: Required matches that don't affect scoring (faster)
  • mustNot: Exclusions

Performance tips:

  • Use filter instead of must for criteria that shouldn't affect scoring (faster)
  • Put most selective criteria in must or filter first
  • Limit should clauses to 3-5 for best performance

Query with Synonyms

When your index is configured with synonyms, specify the synonym mapping name in your query:

db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      text: {
        query: "car chase",
        path: "description",
        synonyms: "synonym-mapping-name"  // Reference the mapping from your index
      }
    }
  }
])

Note: When you specify a synonym mapping name, MongoDB Search automatically searches for the query terms AND all their synonyms (e.g., "car" also matches "automobile", "vehicle").


Using Multi Analyzers

Query specific analyzer variants of a field:

// Standard fuzzy search
db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      text: {
        query: "Action",
        path: "title"  // Uses default analyzer
      }
    }
  }
])

// Exact match using keyword analyzer
db.collection.aggregate([
  {
    $search: {
      index: "search_index",
      text: {
        query: "Action",
        path: "title.keywordAnalyzer"  // Uses alternate analyzer
      }
    }
  }
])

Use case: Support both fuzzy and exact matching on the same field without duplicating data.


Autocomplete

Search-as-you-type on fields indexed as autocomplete type (see lexical-search-indexing.md).

OptionDescription
queryString to search
pathField indexed as autocomplete
tokenOrderany (tokens in any order; sequential matches score higher) or sequential (tokens must be adjacent)
fuzzy`{ maxEdits: 1\

To score exact matches higher, index the field as both autocomplete and string types and query using compound.


Facet

Groups results into buckets by field values or ranges. Use with $searchMeta for metadata only, or with $search + $SEARCH_META variable for results and metadata.

{ "$searchMeta": { "facet": {
    "operator": { <operator> },
    "facets": {
      "<facet-name>": { "type": "string|number|date", "path": "<field>", ...options }
    }
} } }
Facet typeField index typeBucket definition
stringtokenTop N unique string values. numBuckets defaults to 10.
numbernumberNumeric ranges via boundaries array + optional default bucket
datedateDate ranges via boundaries array + optional default bucket

geoShape

Query shapes by spatial relation. Field must be indexed as geo type with indexShapes: true. Required fields: geometry (GeoJSON Polygon, MultiPolygon, or LineString), path, and relation:

relationMeaning
containsIndexed geometry contains the query geometry
disjointNo overlap between geometries
intersectsGeometries overlap
withinIndexed geometry is within the query geometry (not supported for LineString or Point)

geoWithin

Query geographic points within a region. Field must be indexed as geo type. Specify one of:

  • box: { bottomLeft: , topRight: }
  • circle: { center: , radius: <meters> }
  • geometry: GeoJSON Polygon or MultiPolygon

For both geo operators: longitude must be specified before latitude; longitude range [-180, 180], latitude range [-90, 90].


Query Optimization

Sorting Search Results

Use the sort option inside $search to sort at the mongot level (more efficient than a $sort stage after). Supports: boolean, date, number, objectId, uuid, and string (must be indexed as token type). Cannot sort on `embedde