---
title: "Hybrid Search"
description: "This guide covers hybrid search patterns in MongoDB Atlas: combining vector and lexical search using `$rankFusion` and `$scoreFusion`, and using lexical prefilters with the `vectorSearch` operator inside `$search`."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/hybrid-search
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:25:17.039Z
license: CC-BY-4.0
attribution: "Hybrid Search — Claudary (https://claudary.paisolsolutions.com/skills/hybrid-search)"
---

# Hybrid Search
This guide covers hybrid search patterns in MongoDB Atlas: combining vector and lexical search using `$rankFusion` and `$scoreFusion`, and using lexical prefilters with the `vectorSearch` operator inside `$search`.

## Overview

# Hybrid Search

This guide covers hybrid search patterns in MongoDB Atlas: combining vector and lexical search using `$rankFusion` and `$scoreFusion`, and using lexical prefilters with the `vectorSearch` operator inside `$search`.

**Scope**: This guide covers hybrid pipelines. For pure vector search indexes and `$vectorSearch` query construction, see vector-search.md. For lexical index definitions and query patterns, see lexical-search-indexing.md and lexical-search-querying.md.

## Table of Contents

- [Overview](#overview)
- [Choosing the Right Approach](#choosing-the-right-approach)
- [Indexing for Hybrid Search](#indexing-for-hybrid-search)
- [$rankFusion](#rankfusion)
- [$scoreFusion](#scorefusion)
- [Lexical Prefilters (vectorSearch Operator)](#lexical-prefilters-vectorsearch-operator)
- [Best Practices and Limitations](#best-practices-and-limitations)

---

## Overview

Hybrid search combines multiple search methods on the same collection and merges the results into a single ranked or scored list.

**Three patterns covered in this guide:**

| Pattern | Stage / Operator | Use When |
|---|---|---|
| Rank-based fusion | `$rankFusion` | Document position matters; use RRF algorithm |
| Score-based fusion | `$scoreFusion` | Score magnitude matters; need custom math or normalization |
| Lexical prefilter | `$search` + `vectorSearch` operator | Need fuzzy/phrase/wildcard/compound pre-filtering before vector search |

**$rankFusion vs $scoreFusion:**
- `$rankFusion` ranks by position in each input pipeline using the Reciprocal Rank Fusion (RRF) algorithm. A document ranked #1 in multiple pipelines scores much higher than one ranked #1 in only one. Weights influence how much each pipeline's rank contributes.
- `$scoreFusion` ranks by the actual score values from each pipeline. Supports normalization (sigmoid, minMaxScaler) and custom combination expressions. Use when score magnitude, not just ordering, matters.

---

## Choosing the Right Approach

| Scenario | Recommended Approach |
|---|---|
| Combine lexical + vector, rank by position | `$rankFusion` |
| Combine lexical + vector, control score math or normalization | `$scoreFusion` |
| Multiple query vectors or embedding models on same collection | `$rankFusion` with multiple `$vectorSearch` pipelines |
| Pre-filter vector search with fuzzy, phrase, wildcard, or compound | `$search` + `vectorSearch` operator |
| Pre-filter vector search with simple equality or range | `filter` fields in `$vectorSearch` (see vector-search.md) |
| Cross-collection hybrid search | `$unionWith` + `$vectorSearch` (not `$rankFusion`/`$scoreFusion`) |

**Version requirements**: `$rankFusion` requires MongoDB 8.0+. `$scoreFusion` requires MongoDB 8.2+. Only proceed with this guide if the use case is lexical prefilters, or if the cluster meets the version requirement for the fusion stage of interest. Otherwise do not proceed.

---

## Indexing for Hybrid Search

### For $rankFusion and $scoreFusion

You need two separate indexes on the collection:

**1. A vectorSearch-type index** for the `$vectorSearch` input pipeline:
```javascript
db.collection.createSearchIndex(
  "<vector-index-name>",
  "vectorSearch",
  {
    "fields": [
      {
        "type": "vector",
        "path": "<embedding-field>",
        "numDimensions": <number>,
        "similarity": "dotProduct"
      }
    ]
  }
)
```

**2. A search-type index** for the `$search` input pipeline:
```javascript
db.collection.createSearchIndex(
  "<search-index-name>",
  {
    "mappings": { "dynamic": true }
  }
)
```

---

### For Lexical Prefilters (vectorSearch Operator)

The `vectorSearch` operator runs inside `$search`, so you need a **single search-type index** that includes a `vector` field type. This is different from a vectorSearch-type index — you cannot use the `$vectorSearch` stage to query fields indexed this way.

```javascript
db.collection.createSearchIndex(
  "<search-index-name>",
  {
    "mappings": {
      "dynamic": true,
      "fields": {
        "<embedding-field>": {
          "type": "vector",
          "numDimensions": <number>,
          "similarity": "dotProduct",
          "quantization": "scalar"  // Optional
        }
      }
    }
  }
)
```

**Note**: `storedSource: true` is not supported on indexes that contain a `vector` field type. Use `include` or `exclude` to specify stored fields explicitly.

---

## Common Rules for Fusion Stages

The following rules apply to both `$rankFusion` and `$scoreFusion`.

**Pipeline naming restrictions**: Pipeline names must not be empty, start with `$`, contain the null character `\\0`, or contain `.`

**Not allowed inside input pipelines**: `$project` or `storedSource` fields. Apply modifications (`$project`, `$addFields`, `$set`) in stages after the fusion stage.

---

## $rankFusion

`$rankFusion` executes all input pipelines independently, de-duplicates results, and ranks them using the Reciprocal Rank Fusion (RRF) algorithm. Documents appearing highly ranked in multiple pipelines score highest.

### Syntax

```javascript
{
  $rankFusion: {
    input: {
      pipelines: {
        <pipelineName1>: [ <stages> ],
        <pipelineName2>: [ <stages> ],
        ...
      }
    },
    combination: {
      weights: {
        <pipelineName1>: <number>,
        <pipelineName2>: <number>
      }
    },
    scoreDetails: <boolean>  // Default: false
  }
}
```

### Fields

| Field | Type | Description |
|---|---|---|
| `input.pipelines` | Object | Map of pipeline names to aggregation stages. At least one required. |
| `combination.weights` | Object | Optional. Per-pipeline weights (non-negative numbers). Default weight is 1. |
| `scoreDetails` | Boolean | Optional. If true, populates `$meta: "scoreDetails"` per document. Default false. |

### RRF Formula

For each document, the RRF score is:

```
RRFscore(d) = sum over all pipelines of: weight * (1 / (60 + rank_of_d_in_pipeline))
```

The constant 60 is a sensitivity parameter set by MongoDB and cannot be changed. Documents not present in a pipeline do not contribute a term for that pipeline.

### Input Pipeline Restrictions

See [Common Rules](#common-rules-for-fusion-stages) for naming and modification restrictions. Allowed stages: `$search`, `$vectorSearch`, `$match`, `$geoNear`, `$sample`, `$sort`, `$skip`, `$limit`.

The ordering requirement is satisfied if the pipeline begins with `$search`, `$vectorSearch`, or `$geoNear`, or contains an explicit `$sort`.

---

### Example 1: Basic Hybrid (Vector + Lexical, Equal Weights)

```javascript
db.embedded_movies.aggregate([
  {
    $rankFusion: {
      input: {
        pipelines: {
          vectorPipeline: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "plot_embedding",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 100,
                limit: 20
              }
            }
          ],
          textPipeline: [
            {
              $search: {
                index: "<search-index-name>",
                text: {
                  query: "<query-term>",
                  path: "title"
                }
              }
            },
            { $limit: 20 }
          ]
        }
      }
    }
  },
  { $limit: 10 }
])
```

**Note**: `$search` does not auto-limit results — always add `$limit` inside the `$search` input pipeline.

---

### Example 2: Weighted Hybrid (Boosting One Pipeline)

Assign higher weight to the pipeline whose ranking should contribute more to the final score:

```javascript
db.embedded_movies.aggregate([
  {
    $rankFusion: {
      input: {
        pipelines: {
          vectorPipeline: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "plot_embedding",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 100,
                limit: 20
              }
            }
          ],
          textPipeline: [
            {
              $search: {
                index: "<search-index-name>",
                phrase: {
                  query: "<query-term>",
                  path: "title"
                }
              }
            },
            { $limit: 20 }
          ]
        }
      },
      combination: {
        weights: {
          vectorPipeline: 0.7,
          textPipeline: 0.3
        }
      }
    }
  },
  { $limit: 10 }
])
```

**Recommendation**: Set weights per-query based on which method is more appropriate for that query, rather than using static weights for all queries.

---

### Example 3: Multiple $vectorSearch Pipelines

Use multiple vector pipelines to search different fields, different query vectors, or different embedding models:

```javascript
db.embedded_movies.aggregate([
  {
    $rankFusion: {
      input: {
        pipelines: {
          plotPipeline: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "plot_embedding_voyage",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 200,
                limit: 50
              }
            }
          ],
          titlePipeline: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "title_embedding_voyage",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 200,
                limit: 50
              }
            }
          ]
        }
      },
      combination: {
        weights: {
          plotPipeline: 0.5,
          titlePipeline: 0.5
        }
      }
    }
  },
  { $limit: 20 }
])
```

---

### Surfacing scoreDetails

Set `scoreDetails: true` on the stage, then project via `$meta: "scoreDetails"`. The output includes a `value` (final RRF score), `description`, and a `details` array — one entry per input pipeline — containing `inputPipelineName`, `rank`, `weight`, and optionally `value` (raw pipeline score). See the `$scoreFusion` scoreDetails section below for a concrete structure example; `$rankFusion` follows the same pattern with `rank` instead of `inputPipelineRawScore`.

---

## $scoreFusion

`$scoreFusion` executes all input pipelines independently, de-duplicates results, and combines them using the actual score values from each pipeline. Supports normalization and custom combination expressions for fine-grained control over how scores are merged.

### Syntax

```javascript
{
  $scoreFusion: {
    input: {
      pipelines: {
        <pipelineName1>: [ <stages> ],
        <pipelineName2>: [ <stages> ],
        ...
      },
      normalization: "none | sigmoid | minMaxScaler"
    },
    combination: {
      weights: {
        <pipelineName1>: <number>,
        <pipelineName2>: <number>
      },
      method: "avg | expression",
      expression: <arithmetic-expression>
    },
    scoreDetails: <boolean>
  }
}
```

### Fields

| Field | Type | Description |
|---|---|---|
| `input.pipelines` | Object | Map of pipeline names to aggregation stages. At least one required. |
| `input.normalization` | String | Normalize scores before combining: `none` (no normalization), `sigmoid`, or `minMaxScaler`. |
| `combination.weights` | Object | Optional. Per-pipeline weights applied to normalized scores. Default is 1. Mutually exclusive with `combination.expression`. |
| `combination.method` | String | `avg` (default) or `expression`. |
| `combination.expression` | Expression | Custom arithmetic expression. Use pipeline names as variables representing each pipeline's score. Mutually exclusive with `combination.weights`. |
| `scoreDetails` | Boolean | Optional. If true, populates `$meta: "scoreDetails"` per document. Default false. |

### Normalization Options

| Option | Effect |
|---|---|
| `none` | No normalization — raw scores combined as-is |
| `sigmoid` | Applies the sigmoid expression, mapping scores to (0, 1) |
| `minMaxScaler` | Applies the minMaxScaler window operator, scaling scores to [0, 1] |

### Input Pipeline Restrictions

See [Common Rules](#common-rules-for-fusion-stages) for naming and modification restrictions. Allowed stages: `$search`, `$vectorSearch`, `$match`, `$geoNear`, `$sort`, `$skip`, `$limit`. Note: unlike `$rankFusion`, `$sample` is not permitted.

The scoring requirement is satisfied if the pipeline begins with `$search`, `$vectorSearch`, `$match` with legacy text search, or `$geoNear`. Otherwise, include an explicit `$score` stage.

---

### Example 1: avg Method with Weights

```javascript
db.embedded_movies.aggregate([
  {
    $scoreFusion: {
      input: {
        pipelines: {
          vectorPipeline: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "plot_embedding",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 100,
                limit: 20
              }
            }
          ],
          textPipeline: [
            {
              $search: {
                index: "<search-index-name>",
                text: {
                  query: "<query-term>",
                  path: "title"
                }
              }
            },
            { $limit: 20 }
          ]
        },
        normalization: "sigmoid"
      },
      combination: {
        method: "avg",
        weights: {
          vectorPipeline: 2,
          textPipeline: 1
        }
      }
    }
  },
  { $limit: 10 }
])
```

---

### Example 2: expression Method with Custom Score Math

Use `expression` when you need full control over how pipeline scores are combined. Reference pipeline names as variables in the expression:

```javascript
db.embedded_movies.aggregate([
  {
    $scoreFusion: {
      input: {
        pipelines: {
          searchOne: [
            {
              $vectorSearch: {
                index: "<vector-index-name>",
                path: "plot_embedding",
                queryVector: [<query-vector-2048-dimensions>],
                numCandidates: 100,
                limit: 20
              }
            }
          ],
          searchTwo: [
            {
              $search: {
                index: "<search-index-name>",
                text: {
                  query: "<query-term>",
                  path: "title"
                }
              }
            },
            { $limit: 20 }
          ]
        },
        normalization: "sigmoid"
      },
      combination: {
        method: "expression",
        expression: {
          $sum: [
            { $multiply: ["$searchOne", 10] },
            "$searchTwo"
          ]
        }
      },
      scoreDetails: true
    }
  },
  {
    $project: {
      _id: 1,
      title: 1,
      plot: 1,
      scoreDetails: { $meta: "scoreDetails" }
    }
  },
  { $limit: 10 }
])
```

**Note**: `combination.expression` and `combination.weights` are mutually exclusiv

---

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