Elasticsearch ハイブリッド検索

# はじめに

本記事は、2025年2月17日に公開されたElastic 社の公式ブログ 「Elasticsearch hybrid search」 を元にした日本語訳です。
元記事の著者: Valentin Crettaz

# Elasticsearch ハイブリッド検索

ハイブリッド検索について、Elasticsearch がサポートするハイブリッド検索クエリの種類と、その作成方法を学びます。

この記事は、ベクトル検索（別名セマンティック検索）の詳細と、Elasticsearch におけるその実装方法を探る全3部のシリーズの最終回です。

[最初の記事](https://www.elastic.co/search-labs/blog/introduction-to-vector-search) [(日本語訳)](https://qiita.com/nobuhikosekiya/items/3c3c7bb00195b9f276a2)では、埋め込み（ベクトル）の基本や、ベクトル検索が内部でどのように機能するかの概要を提供しました。

最初の記事で学んだベクトル検索の知識をもとに、[第2の記事](https://www.elastic.co/search-labs/blog/vector-search-set-up-elasticsearch) [(日本語訳)](https://qiita.com/nobuhikosekiya/items/dbc86009f7a49a08843e)では、Elasticsearch でのベクトル検索のセットアップ方法や k-NN 検索の実行方法について解説しました。

今回の第3部では、前2回の記事で学んだ知識を活用し、Elasticsearch で強力なハイブリッド検索クエリを作成する方法について詳しく掘り下げます。

## ハイブリッド検索の必要性

ハイブリッド検索の領域に入る前に、[最初の記事](https://www.elastic.co/search-labs/blog/introduction-to-vector-search)で学んだ、レキシカル検索とセマンティック検索の違いおよびそれらがどのように補完し合うかを簡単におさらいしましょう。

簡単にまとめると、レキシカル検索は、構造化データを管理でき、ユーザーが何を検索しているか明確な場合に非常に有効です。一方、セマンティック検索は、[非構造化データ](https://www.elastic.co/what-is/unstructured-data)を検索可能にし、ユーザーが正確に何を探しているか分からなくても効果を発揮します。両者の利点を最大限に引き出すため、これらを組み合わせることができれば素晴らしいでしょう。そこで登場するのがハイブリッド検索です。

ある意味で、ハイブリッド検索はレキシカル検索とセマンティック検索の「合算」ともいえます。しかし、適切に実装されれば、ハイブリッド検索は単なるその合算以上の効果を発揮し、レキシカル検索またはセマンティック検索単体では得られない優れた結果をもたらします。

ハイブリッド検索クエリは、通常、少なくとも1つのレキシカル検索クエリと1つのセマンティック検索クエリを組み合わせ、その結果を統合することで実現されます。レキシカル検索の結果は、[BM25](https://en.wikipedia.org/wiki/Okapi_BM25)や[TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)などの類似度アルゴリズムによってスコアリングされ、転置インデックスに記録された用語の数や頻度によりスコアの最大値が変動するため、通常はスコアが無制限です。これに対して、セマンティック検索の結果は、使用される類似度関数（例：コサイン類似度の場合は [0, 2]）によって、閉じた区間内でスコアリングされます。

ハイブリッド検索クエリにおいて、レキシカル検索結果とセマンティック検索結果を統合するには、取得されたドキュメントの相対的な関連性を維持する形で両者を融合する必要があり、これは複雑な課題です。幸い、既存の[いくつかの手法](https://arxiv.org/abs/2210.11934)が利用可能で、その中でも **Convex Combination (CC)** と **Reciprocal Rank Fusion (RRF)** が非常に一般的です。

基本的に、Convex Combination（線形結合とも呼ばれる）は、レキシカル検索結果とセマンティック検索結果の正規化済みスコアを、それぞれの重み（0 ≤ α, β ≤ 1）で組み合わせる方法です。具体的には、次のように表されます:

![](https://elastic.co/search-labs/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fme0ej585%2Fsearch-labs-import-testing%2F93cc63a6bcd759b2d217162b25c7ef389fbcce05-866x68.png&w=3840&q=75)

CC は、レキシカルスコアとセマンティックスコアの加重平均とみなすことができます。0から1の間の重みは関連クエリの影響を抑え、1より大きい重みはその影響を強化します。

一方、RRF はスコアのキャリブレーションや正規化を必要とせず、単に結果セット内での順位に基づいて、以下の数式により各ドキュメントにスコアを与えます。ここで k は、順位が低いドキュメントの重要度を調整するための任意の定数です:

![](https://elastic.co/search-labs/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fme0ej585%2Fsearch-labs-import-testing%2F2771584645207e423b16186f7314349f5f474062-656x78.png&w=3840&q=75)

CC と RRF にはそれぞれ長所と短所があり、以下の表1に示されています:

_表1: CC と RRF の長所と短所_

| | Convex Combination | Reciprocal Rank Fusion |
| -- | -- | -- |
| **長所** | 重みの適切なキャリブレーションにより、RRF より効果的にスコアリング可能 | キャリブレーション不要で完全に教師なし。最小/最大スコアを知る必要がない |
| **短所** | 適切な重みのキャリブレーションが必要で、最適な重みはデータセットごとに異なる | k の値の調整が容易ではなく、結果セットのサイズが大きいとランキング品質に影響する可能性がある |

なお、これらの長所と短所については、仮定やテストに用いたデータセットによって意見が分かれることもあります。まとめると、RRF は CC よりもわずかに精度が劣るものの、「プラグ・アンド・プレイ」という大きな利点があり、ラベル付きクエリセットを用いた重みの微調整が不要です。

Elastic は、[CC](https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html#_combine_approximate_knn_with_other_features) と [RRF](https://www.elastic.co/guide/en/elasticsearch/reference/8.8/release-highlights.html#add_support_for_reciprocal_rank_fusion_rrf_to_search_api) の両アプローチをサポートすることにしました。後ほど、その実装方法について詳しく説明します。もし、この選択の背景に興味がある場合は、Elastic ブログの素晴らしい[記事](https://www.elastic.co/blog/improving-information-retrieval-elastic-stack-hybrid)や、Haystack 2023 で Elastician の Philipp Krenn 氏が行った[RRF に関するトーク](https://haystackconf.com/eu2023/talk-2/)もご覧ください。

## タイムライン

2019年に dense ベクトルに対する brute-force kNN 検索を実装して以来、Elasticsearch は 2022年2月の 8.0 リリースで近似最近傍探索（ANN 検索）をサポートし、2022年8月の 8.4 リリースでハイブリッド検索のサポートが追加されました。下図の図1は、ハイブリッド検索が市場に投入されるまでの Elasticsearch のタイムラインを示しています:

![Elasticsearch のハイブリッド検索タイムライン](https://elastic.co/search-labs/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fme0ej585%2Fsearch-labs-import-testing%2Fb0a9917e83a62d305818855cb52741350c02415f-1018x469.png&w=3840&q=75)

_図1: Elasticsearch のハイブリッド検索タイムライン_

## Elasticsearch におけるハイブリッド検索の構造

前回の記事でも簡単に触れたように、Elasticsearch におけるベクトル検索のサポートは、主に **dense** ベクトルモデル（`dense_vector` フィールドタイプ）を利用して実現されています。これらのモデルは、通常ほぼ全ての値が非ゼロとなるベクトルを生成し、非構造化データの意味を多次元空間で表現します。

しかし、dense モデルだけがセマンティック検索の手法ではありません。Elasticsearch は、**sparse** ベクトルモデルを用いた別のアプローチも提供しています。Elastic は、微調整不要のドメイン非特化型 sparse ベクトルモデルである [**Elastic Learned Sparse EncodeR (ELSER)**](https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html) を開発しました。このモデルは約 [30,000語](https://ml-models.elastic.co/elser_model_1.vocab.json) の語彙で事前学習されており、sparse モデルであるため、ほとんどのベクトル値（99.9%以上）がゼロとなります。

その仕組みは非常にシンプルです。インデックス作成時に、`inference` インジェストプロセッサを用いて term/weight ペアを含む sparse ベクトルが生成され、`sparse_vector` フィールドタイプに格納されます。これは、`dense_vector` フィールドタイプの sparse 版といえます。クエリ実行時には、`sparse_vector` と呼ばれる特定の DSL クエリが、元の検索用語を ELSER モデルの語彙内の、重み付けにより最も類似している用語に置き換えます。

### sparseとdenseの違い

ハイブリッド検索クエリに入る前に、[sparse と dense モデルの違い](https://www.elastic.co/search-labs/lexical-ai-powered-search-elastic-vector-database#dense-versus-sparse-vector-retrieval)について簡単に説明します。下図の図2は、テキスト「the quick brown fox」が各モデルによってどのようにエンコードされるかを示しています。

スパースの場合、元の4語が、関連性の近い・遠い30の重み付き語に _拡張_ されます。拡張された語の重みが高いほど、元の語との関連性が高いことを示しています。ELSER の語彙は 30,000 語以上を含むため、「the quick brown fox」を表すベクトルは同数の次元を持ち、非ゼロ値は約 0.1%（約 30/30,000）のみに留まるため、これらのモデルは sparse と呼ばれます。

![スパースモデルとデンスモデルのエンコード比較](https://elastic.co/search-labs/_next/image?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2Fme0ej585%2Fsearch-labs-import-testing%2F0e4ae7327e20cae4429cd8b3eef1aed15c6f4ff9-1600x662.png&w=3840&q=75)

_図2: スパースモデルとデンスモデルのエンコード比較_

Denseの場合、「the quick brown fox」は、テキストの意味を捉える小規模な埋め込みベクトルにエンコードされます。384 個のベクトル要素はすべて非ゼロの値を持ち、それぞれの次元との類似度を表します。なお、ここで示している次元の名称（例：`is_mouse`、`is_brown` など）は架空のもので、値の説明を具体化するためのものです。

もう一つ重要な違いは、sparse ベクトルは _転置インデックス_ を通じてクエリされるのに対し、dense ベクトルはグラフベースまたはクラスターベースのデータ構造にインデックスされ、近似最近傍探索（ANN アルゴリズム）を用いて検索される点です。

ELSER の誕生の詳細には踏み込みませんが、このモデルがどのようにして生まれたのかに興味がある場合は、Elastic Search Labs の [この記事](https://www.elastic.co/search-labs/blog/introducing-elastic-learned-sparse-encoder-elser) をご覧ください。また、ELSER の評価にご興味がある場合は、通常の BM25 レキシカル検索と比較した Elastic の [リレバンスワークベンチ](https://www.elastic.co/demo-gallery/relevance-workbench) も参考になるでしょう。なお、本記事では ELSER モデルのダウンロードやデプロイのプロセスについては触れていませんが、[公式ドキュメント](https://www.elastic.co/guide/en/machine-learning/current/ml-nlp-elser.html#download-deploy-elser) に詳しい手順が記載されています。

### ハイブリッド検索のサポート

dense でも sparse でも、どちらの検索においても Elastic はハイブリッド検索のサポートを提供しています。第一のタイプは、`query` オプションで指定されるレキシカル検索クエリと、`knn` オプションで指定されるベクトル検索クエリ（またはその配列）を **組み合わせる** 方法です。第二のタイプは、8.14 で導入され、8.16 で GA となった新たな検索オプション **`retriever`** を使用し、レキシカル（例：`match`）やセマンティック（例：`sparse_vector`）の検索クエリの配列を指定する方法です。

これらが抽象的に感じられる場合でもご安心ください。すぐに、ハイブリッド検索が実際にどのように動作するのか、またその利点について具体例を通して解説します。

#### dense モデルを用いたハイブリッド検索

これは先に述べたハイブリッド検索タイプです。基本的には、`match` クエリなどのレキシカル検索クエリと、[近似 k-NN 検索](https://www.elastic.co/search-labs/blog/vector-search-set-up-elasticsearch#approximate-k-nn-search)を組み合わせ、関連性を向上させるものです。
```
POST my-index/_search
{
  "size": 10,
  "_source": false,
  "fields": [ "price" ],
  "retriever": {
    "standard": { 
      "retrievers": [ 
        {
          "standard": { 
            "query": {
              "match": {
                "text-field": "fox"
              }
            }
          }
        },
        {
          "knn": { 
            "field": "title_vector",
            "query_vector": [0.1, 3.2, 2.1],
            "k": 5,
            "num_candidates": 100
          }
        }
      ]
    }
  }
}
```
上記の例では、ハイブリッド検索クエリは、`standard` リトリーバーによるレキシカル検索クエリと、`knn` リトリーバーによるベクトル検索クエリの **組み合わせ** となっています。このクエリは、まずグローバルレベルで上位5件のベクトル一致を取得し、レキシカル一致と統合した上で、最も関連性の高い10件のヒットを返します。ベクトル一致とレキシカル一致の統合は、論理和（OR 条件）により行われ、各ドキュメントのスコアは前述の Convex Combination による、ベクトルスコアとレキシカルスコアの加重和で算出されます。

Elasticsearch は、同じハイブリッドクエリを RRF ランキングを用いて実行する方法も提供しており、これは単に `rrf` リトリーバーを使用するだけです。
```
POST my-index/_search
{
  "size": 10,
  "_source": false,
  "fields": [ "price" ],
  "retriever": {
    "rrf": { 
      "retrievers": [ 
        ...same as above...
      ],
      "rank_constant": 60, 
      "rank_window_size": 100
    }
  }
}
```
上記の例では、`window_size` 件（例：100件）のドキュメントが、ベクトルクエリとレキシカルクエリの両方から取得され、RRF によってランキングされ、最終的に上位 `size` 件（例：10件）が返されます。

なお、RRF ランキングは商用ライセンス（Enterprise）が必要ですが、ライセンスがなくても、CC スコアリングを用いたハイブリッド検索や、1ヶ月間のトライアルライセンスを利用してフル機能を試すことが可能です。

#### sparse モデルを用いたハイブリッド検索

sparse モデルを用いる第二のハイブリッド検索タイプは、dense ベクトルの場合と同様に動作します。
```
POST my-index/_search
{
  "_source": false,
  "fields": [ "text-field" ],
  "retriever": {
    "rrf": { 
      "retrievers": [ 
        {
          "standard": { 
            "query": {
              "match": {
                "text-field": "fox"
              }
            }
          }
        },
        {
          "standard": { 
            "query": {
              "sparse_vector": {
                "field": "ml.tokens",
                "inference_id": ".elser_model_1",
                "query": "a quick brown fox jumps over a lazy dog"
              }
            }
          }
        }
      ]
    }
  }
}
```
上記の例では、`retrievers` 配列に、レキシカルな `match` クエリと、先に紹介した ELSER sparse モデルを利用するセマンティックな `sparse_vector` クエリが含まれています。

#### dense と sparse モデルを組み合わせたハイブリッド検索

ここまで、dense または sparse のハイブリッド検索について見てきました。ここで、同一のインデックス内で dense と sparse の両方のデータを混在させることが可能かという疑問が生じるかもしれませんが、実際にそれは可能です。たとえば、画像の dense ベクトル空間と、その画像のテキスト説明の sparse ベクトル空間の両方を検索する必要がある場合、以下のように `standard` リトリーバーと `knn` リトリーバーを組み合わせたクエリで実現できます。

```
POST my-index/_search
{
  "_source": false,
  "fields": [ "text-field" ],
  "retriever": {
    "rrf": { 
      "retrievers": [ 
        {
          "knn": {
            "field": "image-vector",
            "query_vector": [0.1, 3.2, ..., 2.1],
            "k": 5,
            "num_candidates": 100
          }
        },
        {
          "standard": { 
            "query": {
              "sparse_vector": {
                "field": "ml.tokens",
                "inference_id": ".elser_model_1",
                "query": "a quick brown fox jumps over a lazy dog"
              }
            }
          }
        }
      ]
    }
  }
}
```

さらに、別の `standard` リトリーバーを用いて追加のレキシカル検索クエリを組み合わせることも可能です。
```
POST my-index/_search
{
  "_source": false,
  "fields": [ "text-field" ],
  "retriever": {
    "rrf": { 
      "retrievers": [ 
        {
          "knn": {
            "field": "image-vector",
            "query_vector": [0.1, 3.2, ..., 2.1],
            "k": 5,
            "num_candidates": 100
          }
        },
        {
          "standard": { 
            "query": {
              "match": {
                "text-field": "brown fox"
              }
            }
          }
        },
        {
          "standard": { 
            "query": {
              "sparse_vector": {
                "field": "ml.tokens",
                "inference_id": ".elser_model_1",
                "query": "a quick brown fox jumps over a lazy dog"
              }
            }
          }
        }
      ]
    }
  }
}
```
上記の例は、レキシカル検索クエリ、ベクトル検索クエリ、セマンティック検索クエリのすべてを含むハイブリッドクエリの指定方法のあらゆる可能性を示しています。

#### 制限事項

ELSER sparse モデルを評価する際の主な制限は、テキスト推論実行時に最大512トークンしかサポートしていない点です。したがって、もし長文のテキストを完全に検索可能にする必要がある場合は、  
a) より長いテキストをサポートする別のモデルを使用する、  
b) テキストをより小さなセグメントに分割する、または  
c) 8.15以降の場合、`semantic_text` フィールドタイプを利用して自動チャンク化を行う、  
という選択肢があります。

### 最適化

ベクトルは、使用する推論モデルにより、数十から数千の次元を持つことがあります。また、短い文章でも長い文章でも、生成される埋め込みベクトルは使用モデルで設定された次元数を必ず持ちます。その結果、これらのベクトルはドキュメント内やディスク上でかなりの容量を占める可能性があります。

この問題に対する最も明白な最適化策は、インデックスのマッピング設定で、ソースドキュメントからベクトルフィールド（`dense_vector` および `sparse_vector` の両方）を除外することです。こうすることで、ベクトルの値は引き続きインデックスされ検索可能な状態を保ちながら、ソースドキュメントのサイズが大幅に削減されます。以下は、ベクトルフィールドを `_source` から除外するマッピング設定の例です:

```json
PUT my-index
{
  "mappings": {
    "_source": {
      "excludes": [
        "text_embedding.predicted_value",
        "ml.tokens"
      ]
    },
    "properties": {
      "text": {
        "type": "text"
      },
      "ml": {
        "properties": {
          "tokens": {
            "type": "rank_features"
          }
        }
      },
      "text_embedding": {
        "properties": {
          "predicted_value": {
            "type": "dense_vector",
            "dims": 384,
            "index": true,
            "similarity": "cosine"
          }
        }
      }
    }
  }
}
```

具体例として、私たちは msmarco-passagetest2019-top1000 データセット（[Microsoft MARCO Passage Ranking](https://microsoft.github.io/MSMARCO-Passage-Ranking/) のサブセット、182,469 件のテキストパッセージを含む 60 MB の TSV ファイル）を用いて実験を行いました。

次に、[msmarco-MiniLM-L-12-v3](https://huggingface.co/sentence-transformers/msmarco-MiniLM-L-12-v3) モデルによって生成された dense 埋め込みベクトルと生テキストを含むインデックスを作成し、dense ベクトルをソースから除外するマッピング設定で同様の実験を行いました。

さらに、ELSER sparse モデルについても、`sparse_vector` フィールドをドキュメント内に保持した場合と除外した場合の両方でテストしました。下表2は、それぞれのインデックスのサイズ（MB 単位）を示しており、dense ベクトルフィールドをソースに含めた場合、インデックスサイズが約3分の1（rank 機能の場合は約3.5分の1）に削減されることが分かります。

| インデックス                                | サイズ (MB) |
| ------------------------------------------- | ----------- |
| index-with-dense-vector-in-source           | 376         |
| index-without-dense-vector-in-source        | 119         |
| index-with-sparse_vector-in-source          | 1,300       |
| index-without-sparse_vector-in-source       | 387         |

なお、これらの数値はあくまで一例であり、実際の値はデータの性質やサイズ、使用する dense または sparse モデルによって大きく異なります。

この最適化に関して最後に注意すべき点は、もしソースからベクトルを除外した場合、再インデックス時に元の埋め込みベクトルが利用できなくなるため、ソースインデックスとして他のインデックスに再インデックスすることができなくなる点です。しかし、インデックスには生テキストは保持されているため、`inference` プロセッサを使用して埋め込みベクトルを再生成することは可能です。

## 結論

今回、ベクトル検索に関するシリーズの最終記事として、Elasticsearch がサポートするさまざまなハイブリッド検索クエリのタイプについて紹介しました。1つは、レキシカル検索（例：`query`）とベクトル検索（例：`knn`）の組み合わせ、もう1つは新たに導入された `retriever` 検索オプションを用いて `sparse_vector` クエリを活用する方法です。

まず、レキシカル検索とセマンティック検索の結果を融合することで、検索精度を向上させる利点について再確認しました。その過程で、レキシカル検索結果とセマンティック検索結果の融合手法として、Convex Combination (CC) と Reciprocal Rank Fusion (RRF) のそれぞれの長所と短所についても検討しました。

次に、具体例を用いて、Elasticsearch が dense および sparse の両方のベクトル空間に対し、CC と RRF のスコアリング・ランキング手法を利用したハイブリッド検索をどのように実現しているかを示しました。また、Elastic Learned Sparse EncodeR (ELSER) モデルについても簡単に紹介し、30,000語の語彙に基づくドメイン非特化型 sparse モデルとしての取り組みであることを説明しました。

最後に、ELSER モデルの制限事項と、将来的なハイブリッド検索実装に向けた最適化手法について述べました。

もしこの記事がお気に召しましたら、以下のシリーズの他の記事もぜひご覧ください:

- [パート1: ベクトル検索の簡単な入門](https://www.elastic.co/search-labs/blog/introduction-to-vector-search)
- [パート2: Elasticsearch でのベクトル検索のセットアップ方法](https://www.elastic.co/search-labs/blog/vector-search-set-up-elasticsearch)

ベクトル検索を実際に試してみるには、この[セルフペースのハンズオン学習](https://www.elastic.co/demo-gallery/vector-search)をご利用ください。また、[無料クラウドトライアル](https://cloud.elastic.co/registration?onboarding_token=search&cta=cloudregistration&tech=trial&plcmt=cross%20module&pg=search-labs)を開始するか、[ローカルマシンで試す](https://github.com/elastic/start-local?cta=local-machine&tech=github&plcmt=cross%20module&pg=search-labs)ことも可能です。