Free tool

Hybrid search scoring playground

Put a BM25 list and a vector list side by side, pick a fusion method, and see the fused ranking the way Elasticsearch or OpenSearch computes it, including how the fusion window changes the result. The companion to hybrid search scoring, explained.

Example data: a fictional outdoor-gear store. The scores are made up to show the mechanics, not measured; paste your own from explain output.

BM25 (keyword)

RankDocumentRaw score
  • 1
    normalized 1.0000
  • 2
    normalized 0.9179
  • 3
    normalized 0.7472
  • 4
    normalized 0.6264
  • 5
    normalized 0.5008
  • 6
    normalized 0.4122
  • 7
    normalized 0.2802
  • 8
    normalized 0.1433
  • 9
    normalized 0.0692
  • 10
    normalized 0.0010

Rank comes from the score. Documents are matched across lists by name.

Vector (kNN)

RankDocumentRaw score
  • 1
    normalized 1.0000
  • 2
    normalized 0.9583
  • 3
    normalized 0.8182
  • 4
    normalized 0.6629
  • 5
    normalized 0.5417
  • 6
    normalized 0.3939
  • 7
    normalized 0.3409
  • 8
    normalized 0.2159
  • 9
    normalized 0.1250
  • 10
    normalized 0.0010

Rank comes from the score. Documents are matched across lists by name.

Fusion settings

Engine
Method
Normalization
Combination

Fused ranking

BM25Vector

OpenSearch, linear, Min-max normalization, arithmetic mean, weights BM25 0.5 / vector 0.5. Window 10 per list; the first 5 are the page you get back.

  1. 1Storm waterproof running jacket0.9589
    BM25 #2↑1up 1Vector #1=unchangedBM25 0.4589 + Vector 0.5000
  2. 2Waterproof jacket care and re-proofing0.7050
    BM25 #3↑1up 1Vector #4↑2up 2BM25 0.3736 + Vector 0.3314
  3. 3Running jacket size guide0.6970
    BM25 #1↓2down 2Vector #6↑3up 3BM25 0.5000 + Vector 0.1970
  4. 4Featherlite packable rain shell0.5508
    BM25 #8↑4up 4Vector #2↓2down 2BM25 0.0717 + Vector 0.4792
  5. 5Trailhead insulated jacket0.5212
    BM25 #5=unchangedVector #5=unchangedBM25 0.2504 + Vector 0.2708
  6. Beyond the page (size 5)
    6Breathable hardshell for wet runs0.4091
    BM25: not retrievedVector #3↓3down 3BM25 0.0000 + Vector 0.4091
  7. 7Waterproof trail shoes0.3141
    BM25 #6↓1down 1Vector #8↑1up 1BM25 0.2061 + Vector 0.1080
  8. 8Running jacket returns policy0.3132
    BM25 #4↓4down 4Vector: not retrievedBM25 0.3132 + Vector 0.0000
  9. 9Reflective running vest0.3106
    BM25 #7↓2down 2Vector #7↓2down 2BM25 0.1401 + Vector 0.1705
  10. 10Windproof cycling gilet0.0625
    BM25: not retrievedVector #9↓1down 1BM25 0.0000 + Vector 0.0625
  11. 11Running socks 3-pack0.0346
    BM25 #9↓2down 2Vector: not retrievedBM25 0.0346 + Vector 0.0000
  12. 12Jacket repair kit0.0005
    BM25 #10↓2down 2Vector: not retrievedBM25 0.0005 + Vector 0.0000
  13. 13Compact travel umbrella0.0005
    BM25: not retrievedVector #10↓3down 3BM25 0.0000 + Vector 0.0005

Does page size change the ranking?

Same lists and settings. When the window follows the page size, min and max (or mean and spread) are taken over fewer hits, so the top results can change. A fixed window keeps them stable.

RequestTop results
size 2, window 2Storm waterproof running jacket (0.5005), Running jacket size guide (0.5000)
size 5, window 5Storm waterproof running jacket (0.9177), Running jacket size guide (0.5000), Featherlite packable rain shell (0.4545), Waterproof jacket care and re-proofing (0.3790), Breathable hardshell for wet runs (0.3017)
size 10, window 10Storm waterproof running jacket (0.9589), Waterproof jacket care and re-proofing (0.7050), Running jacket size guide (0.6970), Featherlite packable rain shell (0.5508), Trailhead insulated jacket (0.5212), Breathable hardshell for wet runs (0.4091), Waterproof trail shoes (0.3141), Running jacket returns policy (0.3132), Reflective running vest (0.3106), Windproof cycling gilet (0.0625)
size 2, window 10Storm waterproof running jacket (0.9589), Waterproof jacket care and re-proofing (0.7050)

OpenSearch search pipeline

PUT /_search/pipeline/hybrid-minmax-arithmetic
{
  "phase_results_processors": [
    {
      "normalization-processor": {
        "normalization": {
          "technique": "min_max"
        },
        "combination": {
          "technique": "arithmetic_mean",
          "parameters": {
            "weights": [
              0.5,
              0.5
            ]
          }
        }
      }
    }
  ]
}

POST /my-index/_search?search_pipeline=hybrid-minmax-arithmetic
{
  "size": 5,
  "query": {
    "hybrid": {
      "pagination_depth": 10,
      "queries": [
        {
          "multi_match": {
            "query": "waterproof running jacket",
            "fields": [
              "title",
              "body"
            ]
          }
        },
        {
          "knn": {
            "vector": {
              "vector": "REPLACE_WITH_QUERY_VECTOR",
              "k": 10
            }
          }
        }
      ]
    }
  }
}

Replace my-index, the field names and REPLACE_WITH_QUERY_VECTOR with your own. Set the pipeline as index.search.default_pipeline so no client forgets it. Syntax from normalization processor and hybrid query docs.

We tune hybrid relevance against labeled queries: see Elasticsearch consulting and OpenSearch consulting.

How this is calculated

Windows and ranks. Each list is sorted by raw score and cut to its top N hits (the window: Elasticsearch rank_window_size, OpenSearch pagination_depth, which defaults to size). Only hits inside a window take part in fusion. We assume one shard; OpenSearch applies pagination_depth per shard (hybrid query docs).

Reciprocal rank fusion (both engines). score(d) = Σ over lists containing d of 1 / (k + rank), rank starting at 1, k = rank_constant (default 60). A document missing from a list adds nothing for it (Elastic RRF; OpenSearch score ranker). Raw scores only decide the order within each list.

Min-max. n = (score − min) / (max − min) over the window. If all scores in the window are equal (or there is one hit), every hit gets 1. OpenSearch replaces a normalized 0 with 0.001; Elasticsearch keeps 0 (OpenSearch normalization processor; Elastic linear retriever, source).

L2. n = score / √(Σ score²) over the window (Elastic l2_norm, OpenSearch l2).

Z-score (OpenSearch only). n = (score − mean) / sd with the sample standard deviation; results ≤ 0 become 0.001. Only combinable with the arithmetic mean (docs).

Combination. Elasticsearch linear: score = Σ weight × n (a document missing from a list contributes 0). OpenSearch, with weights that must sum to 1: arithmetic mean = Σ w·n / Σ w, where a missing document contributes 0 but its weight still counts; geometric mean = exp(Σ w·ln n / Σ w) and harmonic mean = Σ w / Σ (w / n), both skipping lists where the document is missing, so a document found by only one retriever is not penalized (docs; weighted formulas from the neural-search source). With weights summing to 1, Elasticsearch's weighted sum equals OpenSearch's arithmetic mean apart from the 0.001 floor.

Bars and movement. For RRF and sums, each bar segment is that list's exact contribution. Geometric and harmonic means are not additive, so the bar splits the score in proportion to w·n. Arrows compare the fused rank with the document's rank in the full input list. Ties are broken arbitrarily here; engines break them by internal doc id.

Limits. Not modelled: OpenSearch lower_bounds/upper_bounds, per-retriever normalizers that differ, weighted RRF, more than two retrievers, and multi-shard effects. Fused scores rank documents within one query; they are not relevance or confidence scores. Which method works best depends on your queries: measure against labeled queries of each type you serve.

FAQ

Hybrid scoring questions

How does reciprocal rank fusion score a document?

+

It ignores raw scores and adds 1 / (k + rank) for every list the document appears in, with k = 60 by default on both Elasticsearch and OpenSearch. A document ranked 2nd by BM25 and 1st by vectors scores 1/62 + 1/61 = 0.0325.

Why does my hybrid ranking change with page size?

+

Because fusion only sees each retriever's top N hits, and min-max takes its min and max from that window. If the window follows the page size (OpenSearch without pagination_depth, or a small rank_window_size), a different size gives different results. Fix the window independently of size.

Why not just add BM25 and vector scores?

+

They live on different scales: BM25 is unbounded while vector similarity is usually between 0 and 1, so BM25 dominates the sum. RRF uses ranks instead, and linear fusion normalizes each list before weighting.

Which normalization and combination methods does each engine support?

+

Elasticsearch's linear retriever offers minmax and l2_norm normalizers with a weighted sum. OpenSearch's normalization processor offers min_max, l2 and z_score, combined by arithmetic, geometric or harmonic mean; z_score works only with the arithmetic mean.

Is one fusion method better than the others?

+

Not in general. Fusion is a compromise between its inputs, so the right method and weights depend on your queries. Measure each option against labeled queries of every type you serve, such as identifiers and paraphrases.

Hybrid search ranking the wrong things?

We tune BM25, vectors and fusion against your own labeled queries on Elasticsearch or OpenSearch, and show you the before and after.