Elasticsearch でハイブリッド検索を構築する方法
はじめに
検索システムを構築する際、従来のキーワード検索(BM25)だけでは、ユーザーの意図を十分に汲み取れないケースがあります。一方、セマンティック検索だけでは完全一致が必要な場面で精度が落ちることもあります。
そこで注目されているのがハイブリッド検索です。キーワード検索とセマンティック検索を組み合わせることで、両方の長所を活かした高精度な検索体験を実現できます。
この記事では、Elastic の公式コーディングセッション(発表者:Ugo Sangiorgi 氏 / Principal Product Marketing Engineer)の内容をもとに、Elasticsearch を使ったハイブリッド検索の構築方法を解説します。
この記事で扱う内容
- キーワード検索(BM25)の仕組みと特徴
- セマンティック検索(ELSER)の仕組みと特徴
- ハイブリッド検索の2つのアプローチ:RRF(Reciprocal Rank Fusion) と Linear(線形結合)
- 映画検索デモアプリを使った実装例
前提環境
- Elasticsearch(Elastic Cloud 推奨)
- ELSER(Elastic Learned Sparse EncodeR)モデルがデプロイ済み
- Node.js / React フロントエンド
-
@elastic/react-search-ui/@elastic/search-ui-elasticsearch-connector
デモでは TMDB(The Movie Database)の映画データを elastiflix-movies インデックスに格納し、映画検索アプリとして動作させています。
1. キーワード検索(BM25)
概要
キーワード検索は、Elasticsearch の標準的な全文検索機能で、BM25 アルゴリズムに基づいてスコアリングされます。テキストは アナライザー によってトークン(単語)に分割され、転置インデックスを使って高速に検索されます。
アナライザーの動作
Elasticsearch の Dev Tools で _analyze API を使うと、テキストがどのようにトークン化されるか確認できます。
Standard アナライザー:
GET _analyze
{
"analyzer": "standard",
"text": "the terminator"
}
standard アナライザーでは "the" と "terminator" の2トークンに分割されます。
English アナライザー:
GET _analyze
{
"analyzer": "english",
"text": "the terminator"
}
english アナライザーでは、ストップワード("the" など)が除去され、ステミングが適用されるため、"termin" のような語幹のみが残ります。
検索クエリの例
GET elastiflix-movies/_search
{
"_source": ["title"],
"query": {
"match": {
"title": "terminator"
}
}
}
キーワード検索の限界
「the terminator」のようにタイトルを直接入力すれば正確な結果が返りますが、「a movie about the resistance fighting at machines from the future」(未来からの機械と戦うレジスタンスの映画)のような自然言語のクエリでは、期待する結果を得にくい場合があります。
2. セマンティック検索(ELSER)
概要
セマンティック検索は、テキストの意味に基づいて検索する手法です。Elastic では ELSER(Elastic Learned Sparse EncodeR) という学習済みモデルを使用します。ELSER はテキストをスパースベクトルに変換し、単語の表面的な一致ではなく意味的な類似度に基づいてマッチングします。
ELSER による推論
Elasticsearch の _inference API を使って、テキストがどのようなスパースベクトルに変換されるか確認できます。
POST /_ml/trained_models/elser_infer
{
"docs": [{"text_field": "the terminator"}]
}
レスポンスには predicted_value として各トークンとそのスコアが返されます。「terminator」に対して、関連する概念(例:「movie」「war」「fight」など)も含まれたスパースベクトルが生成されます。
同様に「mafia」をテキストとして推論すると、「mafia」に直接関連しない単語を含む映画(例:The Godfather)もセマンティックに関連するものとして検出できます。
コネクタの実装(SemanticConnector.js)
import ElasticsearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
const connector = new ElasticsearchAPIConnector({
host: process.env.ES_HOST,
index: process.env.ES_INDEX,
apiKey: process.env.ES_API_KEY
}, (requestBody, requestState, queryConfig) => {
if (!requestState.searchTerm) return requestBody;
const body = {
query: {
semantic: {
field: "plot_elser",
query: requestState.searchTerm
}
}
};
requestBody.query = body.query;
delete requestBody.sort;
return requestBody;
});
export default connector;
ポイントは semantic クエリで plot_elser フィールド(ELSER で推論済みのフィールド)を指定していることです。
セマンティック検索の強み
- 「
mafia」で検索 → The Godfather、The Clan、Gotti などが正しくヒット - 「
mobsters」で検索 → 「mafia」という単語を含まない映画も意味的にマッチ
キーワード検索では「mafia」で検索してもプロットに「mafia」を含まない映画はヒットしませんが、セマンティック検索なら意味的に関連する映画を見つけられます。
3. ハイブリッド検索:RRF(Reciprocal Rank Fusion)
概要
RRF は、複数の検索結果のランキングを統合するアルゴリズムです。各検索結果の**順位(ランク)**に基づいてスコアを再計算し、最終的なランキングを生成します。スコアの絶対値ではなく順位を使うため、異なるスコアリング方式の結果を自然に統合できます。
RRF の計算式
RRF_score = Σ 1 / (rank_constant + rank_i)
rank_constant はデフォルトで 1 です(チューニング可能)。
コネクタの実装(HybridConnectorRRF.js)
const body = {
retriever: {
rrf: {
retrievers: [
{
standard: {
query: requestBody.query
}
},
{
standard: {
query: {
semantic: {
field: "plot_elser",
query: requestState.searchTerm
}
}
}
}
],
rank_window_size: 50,
rank_constant: 1
}
}
};
ポイント
-
retrieverの中にrrfを指定し、retrievers配列に複数のサブリトリーバーを定義 - 1つ目:キーワード検索(
requestBody.query) - 2つ目:セマンティック検索(
semanticクエリ) -
rank_window_size: 各リトリーバーから取得する上位結果の数(ここでは50件) -
rank_constant: RRF のスコア計算に使うパラメータ
4. ハイブリッド検索:Linear(線形結合)
概要
Linear リトリーバーは、各検索結果のスコアを重み付けして線形結合する方式です。RRF が順位ベースなのに対し、Linear はスコアベースで統合します。各リトリーバーに weight を設定することで、キーワード検索とセマンティック検索のバランスを調整できます。
コネクタの実装(HybridConnectorLinear.js)
const body = {
retriever: {
linear: {
retrievers: [
{
retriever: {
standard: {
query: requestBody.query
}
},
weight: 1
},
{
retriever: {
standard: {
query: {
semantic: {
field: "plot_elser",
query: requestState.searchTerm
}
}
}
},
weight: 3
}
],
rank_window_size: 100
}
}
};
ポイント
- キーワード検索の
weight: 1に対し、セマンティック検索のweight: 3としている - セマンティック検索のスコアを3倍重視する設定
-
rank_window_sizeは RRF よりも大きい値(100)を設定
RRF と Linear の使い分け
| 項目 | RRF | Linear |
|---|---|---|
| スコアリング | 順位ベース | スコアベース |
| チューニング | rank_constant |
各リトリーバーの weight
|
| 特徴 | スコアの分布に依存しない | スコアの意味を活かせる |
| 適用場面 | 異なるスコア尺度の統合 | スコアの比率で調整したい場合 |
5. フロントエンドの構成
デモアプリは React + @elastic/react-search-ui で構成されており、コネクタを切り替えるだけで検索方式を変更できます。
SearchPage.js
import Results from "./Results";
import { SearchProvider, SearchBox } from "@elastic/react-search-ui";
import { EuiIcon } from "@elastic/eui";
import connector from "../services/SearchConnectorHybridLinear";
function SearchPage() {
const config = {
apiConnector: connector,
trackUrlState: true,
alwaysSearchOnInitialLoad: false,
searchQuery: {
search_fields: {
title: { weight: 2 },
overview: {},
plot: {}
},
},
resultsPerPage: 10,
result_fields: {
title: { raw: { size: 100 } },
poster_path: { raw: {} },
release_date: { raw: {} },
overview: { raw: { size: 500 } },
cast: { raw: {} },
_score: { raw: {} }
}
};
// ...
}
コネクタのインポート先を変更するだけで、4つの検索方式を切り替えられます。
-
SearchConnector.js→ キーワード検索 -
SearchConnectorSemantic.js→ セマンティック検索 -
SearchConnectorHybridRRF.js→ ハイブリッド(RRF) -
SearchConnectorHybridLinear.js→ ハイブリッド(Linear)
6. 検索結果の比較
「mafia」で検索した場合の各方式の結果の違い:
| 検索方式 | 上位結果 |
|---|---|
| キーワード検索 | プロットに「mafia」を含む映画のみ |
| セマンティック検索 | The Godfather、The Clan、Gotti など意味的に関連する映画 |
| ハイブリッド(RRF) | キーワード一致 + 意味的関連を順位ベースで統合 |
| ハイブリッド(Linear) | The Godfather が最上位(セマンティック weight:3 の効果) |
「mobsters」のような類義語で検索した場合、キーワード検索ではほとんどヒットしませんが、ハイブリッド検索ではセマンティック側が補完して関連映画を返します。
よくある質問
ハイブリッド検索は3つ以上のリトリーバーで動作する?
はい、retrievers 配列に3つ以上のリトリーバーを指定できます。例えば、キーワード検索 + ELSER + kNN(密ベクトル)の組み合わせも可能です。
ハイブリッド検索に必要なライセンスは?
Elasticsearch のライセンスについては Elastic のサブスクリプションページ を確認してください。ELSER の利用には一定のサブスクリプションが必要です。
まとめ
Elasticsearch のハイブリッド検索は、retriever API を使うことで非常にシンプルに実装できます。
- キーワード検索(BM25)で完全一致・部分一致を担保
- セマンティック検索(ELSER)で意味的な検索を実現
- RRF または Linear で両者のランキングを統合
ユースケースに応じて RRF と Linear を使い分け、weight や rank_constant をチューニングすることで、最適な検索体験を構築できます。
参考リソース
- Elastic Search Labs - チュートリアル、サンプルコード、ブログ記事
- elasticsearch-labs(GitHub) - サンプルコードリポジトリ
- Elasticsearch Retriever API ドキュメント
- ELSER ドキュメント