---
title: "Lexical Search - Querying"
description: "This guide covers query patterns and optimization techniques for MongoDB Atlas Search."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/lexical-search-querying
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:30:36.220Z
license: CC-BY-4.0
attribution: "Lexical Search - Querying — Claudary (https://claudary.paisolsolutions.com/skills/lexical-search-querying)"
---

# Lexical Search - Querying
This guide covers query patterns and optimization techniques for MongoDB Atlas Search.

## Overview

# Lexical Search - Querying

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

## Table of Contents

- [$search vs $searchMeta](#search-vs-searchmeta)
- [Query Patterns](#query-patterns)
- [Query Optimization](#query-optimization)
- [Query Performance Analysis](#query-performance-analysis)

---

## $search vs $searchMeta

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

| Stage | Use When |
|---|---|
| `$search` | You need matching documents, with or without metadata |
| `$searchMeta` | You 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

| Operator | Description |
|---|---|
| `autocomplete` | Search-as-you-type from incomplete input |
| `compound` | Combines multiple operators into a single query |
| `embeddedDocument` | Queries fields inside arrays of objects |
| `equals` | Exact match on boolean, date, number, objectId, token, uuid |
| `exists` | Tests for presence of a field |
| `geoShape` | Queries shapes by spatial relation (geo type, indexShapes: true) |
| `geoWithin` | Queries points within a region (geo type) |
| `hasAncestor` | Queries ancestor-level fields when using `returnScope` |
| `hasRoot` | Queries root-level fields when using `returnScope` |
| `in` | Queries single values or arrays of values |
| `moreLikeThis` | Finds documents similar to a given document |
| `near` | Queries values near a number, date, or geo point |
| `phrase` | Searches for terms in a specific order |
| `queryString` | Boolean/field-specific query syntax |
| `range` | Queries values within a numeric, date, string, or objectId range |
| `regex` | Regular expression matching on string fields |
| `text` | Full-text analyzed search on string fields |
| `vectorSearch` | Semantic search with lexical pre-filters (vector type in search index) |
| `wildcard` | Wildcard 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.

```javascript
// 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) } }
```

```javascript
// 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 }
])
```

| type | Behavior |
|---|---|
| `lowerBound` | Approximate. Exact up to `threshold` (default 1000), rough above it. |
| `total` | Exact 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:**
```javascript
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:**
```javascript
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:**
```javascript
{
  "mappings": {
    "dynamic": false,
    "fields": {
      "funding_rounds": {
        "type": "embeddedDocuments",
        "dynamic": true,
        "storedSource": {
          "include": ["round_code", "raised_currency_code", "raised_amount"]
        }
      }
    }
  }
}
```

**Query:**
```javascript
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:**
```javascript
// Add to mappings.fields in your index:
{
  "title": { "type": "string" },
  "director": { "type": "string" },
  "year": { "type": "number" }
}
```

**Query patterns:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
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:**
```javascript
// Add to mappings.fields in your index:
{
  "title": { "type": "string" },
  "plot": { "type": "string" }
}
```

**Query pattern:**
```javascript
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:**
```javascript
{
  "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:

```javascript
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:

```javascript
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:

```javascript
// 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).

| Option | Description |
|---|---|
| `query` | String to search |
| `path` | Field indexed as `autocomplete` |
| `tokenOrder` | `any` (tokens in any order; sequential matches score higher) or `sequential` (tokens must be adjacent) |
| `fuzzy` | `{ maxEdits: 1\\|2, prefixLength: <n>, maxExpansions: <n> }` |

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.

```javascript
{ "$searchMeta": { "facet": {
    "operator": { <operator> },
    "facets": {
      "<facet-name>": { "type": "string|number|date", "path": "<field>", ...options }
    }
} } }
```

| Facet type | Field index type | Bucket definition |
|---|---|---|
| `string` | `token` | Top N unique string values. `numBuckets` defaults to 10. |
| `number` | `number` | Numeric ranges via `boundaries` array + optional `default` bucket |
| `date` | `date` | Date 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`:

| relation | Meaning |
|---|---|
| `contains` | Indexed geometry contains the query geometry |
| `disjoint` | No overlap between geometries |
| `intersects` | Geometries overlap |
| `within` | Indexed 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: <GeoJSON Point>, topRight: <GeoJSON Point> }`
- `circle`: `{ center: <GeoJSON Point>, 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

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/lexical-search-querying) · https://claudary.paisolsolutions.com
