ハイブリッド検索とランクフュージョン(セマンティック+キーワード)

概要

Fess の ハイブリッド検索 は、従来のキーワード検索(BM25)と セマンティック(ベクトル)検索 を組み合わせ、両方の検索結果を Rank Fusion で統合することで、より精度が高く関連性の高いランキングを実現します。Rank Fusionは、複数のサーチャーの検索結果を単一の最適化されたランキングに統合します。

Fess 15.9 では、セマンティック検索(コンテンツチャンク+ベクトル検索)はコア機能として提供されます。 有効化すると、セマンティックサーチャーは自動的にRank Fusionに登録されます。 設定方法は セマンティック検索(コンテンツチャンク+ベクトル検索) を参照してください。

Fess のRank Fusion機能は、複数の検索結果を統合して より精度の高い検索結果を提供します。

Rank Fusionとは

Rank Fusionは、複数の検索アルゴリズムやスコアリング手法(例:キーワード/BM25検索とセマンティック/ベクトル検索)の結果を 組み合わせて、単一の最適化されたランキングを生成する技術です。

主な利点:

  • 異なるアルゴリズムの長所を組み合わせる

  • 検索精度の向上

  • 多様な検索結果の提供

対応アルゴリズム

Fess ではRRF(Reciprocal Rank Fusion)アルゴリズムをサポートしています。

RRF (Reciprocal Rank Fusion)

RRFは、各検索結果におけるドキュメントの順位の逆数を合計してスコアを計算します。 複数のサーチャーで取得されたドキュメントは、それぞれのスコアが加算されます。

計算式:

score(d) = Σ 1 / (k + rank(d))
  • k: 順位の影響度を調整する定数パラメーター(デフォルト: 20)

  • rank(d): 各検索結果におけるドキュメントdの順位(0始まり)

  • Σ: ドキュメントdが出現したすべてのサーチャーにわたる合計

注釈

Fess 側で融合する場合(既定)、融合アルゴリズムはRRF固定で、サーチャーごとの重み付けも サポートしていません。各サーチャーの寄与は同じ重みで合計され、ランキングの傾向を調整できる のは rank.fusion.rank_constant のみです。検索エンジン側で融合する場合は、他の結合方式や サーチャーごとの重みを指定できます(検索エンジン側でのランクフュージョン を参照)。

設定

fess_config.properties

基本設定:

# ウィンドウサイズ(融合対象の結果数)
# 注: paging.search.page.max.size × 2 以上である必要があります。
# 設定値がこの最小値を下回る場合、最小値が自動的に使用されます。
rank.fusion.window_size=200

# RRFのrank_constant(kパラメーター)
rank.fusion.rank_constant=20

# 並列処理のスレッド数(0以下の場合、利用可能なCPUコア数 × 3 ÷ 2 + 1 が使用されます)
rank.fusion.threads=-1

# スコアフィールド名(融合後のスコアを格納するフィールド)
rank.fusion.score_field=rf_score
プロパティ デフォルト 説明
rank.fusion.window_size 200 融合対象として各サーチャーから取得する結果の最大件数。paging.search.page.max.size × 2(デフォルトでは 200)以上である必要があり、下回る場合はこの最小値に自動的に引き上げられます(起動時にWARNログを出力)。
rank.fusion.rank_constant 20 RRF計算式における定数 k。値を大きくすると、上位と下位の順位の差によるスコア差が小さくなります。
rank.fusion.threads -1 複数サーチャーを並列実行する固定スレッドプールのスレッド数。0 以下を指定すると 利用可能なCPUコア数 × 3 ÷ 2 + 1 が自動的に使用されます(整数演算のため小数点以下は切り捨て。例: 4コア→7、5コア→8)。
rank.fusion.score_field rf_score 融合後のスコアを格納する結果ドキュメントのフィールド名。

注釈

設定の反映タイミング

上記4つの設定は、いずれも変更の反映に Fess の再起動が必要です。fess_config.properties から読み込まれた値はJVM内にキャッシュされるため、稼働中にファイルを書き換えても反映されません。

なお rank.fusion.window_size は起動時に一度だけ、rank.fusion.threads は スレッドプールを作成する時点で読み込まれます。スレッドプールは default 以外のサーチャー (セマンティックサーチャーなど)が登録されたときに作成されるため、セマンティック検索が 無効な場合はスレッドプール自体が作成されません。

JVMシステムプロパティ

使用するサーチャーは、JVMシステムプロパティで指定します。 fess.in.sh に以下のように記述します:

FESS_JAVA_OPTS="$FESS_JAVA_OPTS -Drank.fusion.searchers=default,semantic_chunk"

fess.in.bat の場合は次のように記述します:

set FESS_JAVA_OPTS=%FESS_JAVA_OPTS% -Drank.fusion.searchers=default,semantic_chunk

このプロパティの動作は以下のとおりです:

  • fess_config.properties ではなく、JVMオプションとして設定します。キー名は rank.fusion.searchers そのものを指定してください。他の設定でよく使われる -Dfess.config. や -Dfess.system. を付けた形式(-Dfess.config.rank.fusion.searchers など)は認識されません。

  • JVMオプションの代わりに、管理画面の「システム > 全般」にある「システムプロパティ」欄へ rank.fusion.searchers=default,semantic_chunk のように1行で記述することもできます。 ただしこの欄の値は、同名のシステムプロパティがまだ設定されていない場合にのみ適用されます。 そのため -D で指定済みの場合はJVMオプションが優先され、いったん適用された値を変更する 場合は Fess の再起動が必要です。

  • default は標準のキーワード検索を行うサーチャーで、常に利用できます。

  • サーチャー名は、実装クラス名から Searcher を取り除き、スネークケースの小文字に変換した ものです(SemanticChunkSearcher → semantic_chunk)。コア内蔵のセマンティック サーチャー(セマンティック検索(コンテンツチャンク+ベクトル検索))は semantic_chunk という名前で登録されます。

  • このプロパティを指定しない場合は、登録されているすべてのサーチャーが使用されます。指定した名前がいずれのサーチャーにも一致しない場合は、default サーチャーのみが使用されます。コア内蔵のセマンティックサーチャー(セマンティック検索(コンテンツチャンク+ベクトル検索))を利用する場合は、通常このプロパティの指定自体が不要です。

  • Rank Fusionによる結果の融合は、利用可能なサーチャーが2つ以上ある場合に実行されます。サーチャーが1つだけの場合は、融合は行われず通常の検索結果が返されます。

警告

Fess 15.7 以前の fess-webapp-semantic-search プラグインを使っていた場合、このプロパティに -Drank.fusion.searchers=default,semantic を指定するよう案内されていました。プラグインが 登録していたサーチャー名は semantic で、15.9 でコアに統合されたセマンティックサーチャーの 名前 semantic_chunk とは 別物 です。この設定を 15.9 でも引き継いだままにすると、 allowlist に semantic_chunk が含まれないため、コア内蔵のセマンティック検索(コンテンツ チャンク+ベクトル検索)が 一切動作しないまま 通常のキーワード検索結果だけが返り続けます (起動時に警告ログは出力されますが、個々の検索リクエストでの除外自体は DEBUG ログにしか 現れません)。default,semantic を指定している場合は、この設定を削除するか semantic_chunk を追加してください。詳細は セマンティック検索(コンテンツチャンク+ベクトル検索) の 「15.7 以前からアップグレードする場合の移行」を参照してください。

検索エンジン側でのランクフュージョン

rank.fusion.engine.enabled=true を設定すると、融合を Fess ではなく OpenSearch が行います。 各サーチャーのクエリを1つの検索リクエスト(hybrid クエリ)にまとめ、OpenSearch の検索 パイプラインでスコアを正規化・結合します。1回のリクエストで融合するため、総ヒット件数と ファセットの件数も融合後の結果集合を表します。

この機能には OpenSearch の neural-search プラグインが必要です。標準配布の OpenSearch と ghcr.io/codelibs/fess-opensearch イメージには含まれていますが、minimal 配布には含まれません。

設定は fess_config.properties に記述します(変更の反映には Fess の再起動が必要です):

rank.fusion.engine.enabled=true
rank.fusion.combination.technique=rrf
rank.fusion.normalization.technique=min_max
rank.fusion.combination.weights=
rank.fusion.pagination_depth=1000
プロパティ デフォルト 説明
rank.fusion.engine.enabled false true にすると検索エンジン側で融合します。
rank.fusion.combination.technique rrf スコアの結合方式。rrf、arithmetic_mean、geometric_mean、harmonic_mean のいずれか。rrf の定数 k には rank.fusion.rank_constant が使われ、Fess 側で融合する場合と同じ計算式になります。
rank.fusion.normalization.technique min_max 結合前のスコアの正規化方式。min_max、l2、z_score のいずれか。rrf では使われません。
rank.fusion.combination.weights (空) サーチャーごとの重みを 名前:重み のカンマ区切りで指定します(例: default:0.7,semantic_chunk:0.3)。重みは 0.0〜1.0 で合計が 1.0 になる必要があり、融合に参加するすべてのサーチャーを指定する必要があります。空の場合は均等に扱われます。条件を満たさない場合はERRORログを出力し、その検索は Fess 側で融合されます。
rank.fusion.pagination_depth 1000 各サーチャーがシャードごとに融合へ提供する結果の件数。ページングできる深さと、融合の対象になるドキュメントの範囲を決めます。検索エンジン側で融合した検索は、最大でこの件数までページングできます。検索エンジンに送る値は indexer.max.result.window.size を超えません。

注釈

z_score 正規化は arithmetic_mean とのみ組み合わせられます(neural-search プラグインの 制約です)。geometric_mean または harmonic_mean と組み合わせた場合、Fess はこの 組み合わせを使わず、両方の設定名を示すERRORログを出力したうえで、その検索を Fess 側で 融合します。

次の場合は検索エンジン側での融合は行われず、Fess 側での融合で処理されます。

  • 詳細検索のパラメーター(as.*)を含む検索

  • ソート、位置情報検索、類似ドキュメント検索を指定した検索。これらではセマンティック検索自体が スキップされるため、キーワード検索のみの結果になります(セマンティック検索(コンテンツチャンク+ベクトル検索) を参照)。

  • rank.fusion.combination.weights が条件を満たさない場合

  • z_score 正規化と geometric_mean または harmonic_mean を組み合わせた場合(両方の 設定名を示すERRORログを出力します)

  • OpenSearch が融合のリクエストをまったく実行できない場合(hybrid クエリや検索パイプライン のプロセッサーを認識しない場合。neural-search プラグインがない、または古い場合など)。 その検索ごとにWARNログを出力します。次の検索では再び OpenSearch に融合を依頼するため、 プラグインを導入または更新すれば Fess を再起動しなくても OpenSearch 側での融合に戻ります。

それ以外の理由で融合のリクエストが失敗した場合(不正なクエリ、クローズされたインデックス、 シャードの障害など)は、検索エンジン側で融合しない場合と同様にその検索に限って報告され、次の 検索は再び検索エンジン側で融合されます。

検索エンジン側で融合する場合の注意点は次のとおりです。

  • rank.fusion.timeout は適用されません。クエリの埋め込みは検索リクエストの送信前に同期的に 計算されるため、埋め込みプロバイダの応答が遅い場合や応答しない場合は、検索がプロバイダ側の タイムアウト(例: content_chunker.embedding.ollama.timeout)まで待たされます。プロバイダが 再試行する場合は、その回数分も加わります。たとえば内蔵の opensearch プロバイダは、 content_chunker.embedding.opensearch.timeout(既定 60000 ミリ秒)の待機を content_chunker.embedding.opensearch.retry.max(既定 3)回まで繰り返すため、 応答しない場合は既定で180秒に再試行の待機時間を加えた時間がかかることがあります。

  • rank.fusion.window_size と rank.fusion.threads は、Fess 側で融合する場合にのみ 使われます。

  • 検索エンジン側で融合した検索は、最大で rank.fusion.pagination_depth 件までページング できます。検索エンジンは各サーチャーのシャードごとの上位 rank.fusion.pagination_depth 件 だけを融合するためです。ページ数、次ページの有無、ページ番号はこの件数で打ち切られ、開始位置 (start)が rank.fusion.pagination_depth 以上のページを要求すると、ほかの検索で index.max_result_window を超えたページを要求した場合と同じエラー(検索API v2 では HTTP 400 の invalid_request)になります。この範囲内でも、融合結果の最後の件より後から 始まるページ(古いリンクなど)は空のページになります。検索エンジン側で融合しない検索は、 これまでどおり index.max_result_window までページングできます。

  • 総ヒット件数は rank.fusion.pagination_depth 未満であれば正確です。この値以上になると、 検索エンジンが実際に一致する件数より少なく数えることがあるため、下限値として扱われます (検索APIの record_count_relation は GREATER_THAN_OR_EQUAL_TO になります)。

  • rank.fusion.pagination_depth を大きくすると深いページまで参照できますが、融合するすべての 検索がサーチャーごと・シャードごとにその件数を収集するため、深いページに限らずすべての検索が 遅くなります。

  • セマンティックサーチャーの knn クエリの k(シャードごとの近傍数)は content_chunker.search.knn.k です。 ベクトル検索は類似度が低いドキュメントも近傍として返すため、content_chunker.search.min_score を設定していない場合、総ヒット件数にはその分のベクトル検索のヒットが加わり、小規模な インデックスでは閲覧できるドキュメントのほとんどが件数に含まれることがあります。件数を絞る には content_chunker.search.min_score を設定してください(セマンティック検索(コンテンツチャンク+ベクトル検索) を参照)。

  • 検索結果の searcher フィールドは同様に付与されますが、rf_score は付与されません。

ハイブリッド検索との連携

Rank Fusionは、キーワード検索とセマンティック検索を組み合わせた ハイブリッド検索で特に効果を発揮します。 セマンティック検索を利用するには、コンテンツチャンク機能を設定したうえで content_chunker.search.enabled=true を設定します。

警告

content_chunker.enabled や content_chunker.search.enabled などの content_chunker.* の設定は、fess_config.properties ではなく システムプロパティ です。conf/system.properties に記述するか、 -Dfess.system.content_chunker.search.enabled=true のようにJVMオプションとして 指定してください。fess_config.properties に記述しても反映されません。 また content_chunker.search.enabled は起動時にのみ評価されるため、 有効化後は Fess の再起動が必要です。

詳細は セマンティック検索(コンテンツチャンク+ベクトル検索) を参照してください。

融合結果の確認

Rank Fusionが実際に動作しているかは、検索結果に付与される以下の2つのフィールドで確認できます。

フィールド 内容
searcher そのドキュメントを取得したサーチャー名の配列(例: ["default", "semantic_chunk"])。両方が含まれていれば、キーワード検索とセマンティック検索の双方でヒットしたことを意味します。
rf_score RRFで算出した融合後のスコア。フィールド名は rank.fusion.score_field で変更できます。

どちらも検索時に動的に付与される値で、インデックスには保存されません。 また、既定では /api/v2/search のレスポンスに含まれないため、確認するには fess_config.properties に以下を設定して Fess を再起動してください:

query.additional.api.response.fields=rf_score,searcher

注釈

query.additional.api.response.fields は、v2検索APIのレスポンスに含めてよい フィールドの許可リストに項目を追加する設定です。role や virtual_host などの アクセス制御用フィールドを追加すると、アクセス制御情報が検索APIの応答に露出するため、 追加しないでください。

ヒット件数への影響

Fess 側でRank Fusionが実行されると、返される総ヒット件数はメインサーチャー(先頭に登録された default サーチャー)の件数そのままではなく、次のように補正されます:

総ヒット件数 = メインサーチャーの総ヒット件数 + 補正値

補正値は、融合後の上位 window_size ÷ 2 件のうち、メインサーチャーの上位 window_size ÷ 2 件に含まれていなかったドキュメントの件数です。つまり、セマンティック検索 だけが見つけたドキュメントの分だけ件数が増えます。 そのため、同じクエリでもハイブリッド検索の有効・無効でヒット件数が変わることがあります。

なお、メインサーチャーの総ヒット件数が概算値(下限値)として返される場合、この補正は行われません。 また、ファセット(ラベルなど)の件数はメインサーチャーの検索結果のものがそのまま使われます。

検索エンジン側で融合する場合(検索エンジン側でのランクフュージョン を参照)、総ヒット件数とファセットの件数は、 キーワード検索とセマンティック検索の両方のヒットの和集合を数えます。各サーチャーがシャードごとに 提供するのは最大で rank.fusion.pagination_depth 件で、セマンティックサーチャーはさらに content_chunker.search.knn.k 件が上限です。そのため、同じクエリでも rank.fusion.engine.enabled を切り替えると件数もファセットの件数も変わり、小規模なインデックスでは、セマンティック検索が加わる 検索エンジン側で融合した場合のほうが大きくなりやすくなります。

使用例

基本的なハイブリッド検索

  1. キーワード検索でBM25スコアを計算

  2. セマンティック検索でベクトル類似度を計算

  3. RRFで両方の結果を融合

  4. 最終的なランキングを生成

検索フロー:

User Query
    ↓
┌──────────────────┬──────────────────┐
│  Keyword Search  │ Semantic Search  │
│    (BM25)        │  (Vector)        │
└────────┬─────────┴────────┬─────────┘
         ↓                  ↓
     Rank List 1        Rank List 2
         └────────┬─────────┘
                  ↓
          Rank Fusion (RRF)
                  ↓
          Final Ranking

パフォーマンス考慮事項

メモリ使用量

  • 複数の検索結果を保持するため、メモリ使用量が増加します。

  • rank.fusion.window_size で融合対象の最大件数を制限できます。メインサーチャー(先頭の default サーチャー)は最大で window_size 件、その他のサーチャーはそれぞれ window_size ÷ サーチャー数 件を取得します(サーチャー数 はメインサーチャーを含む総数で、除算は切り捨てです)。

  • 例えばサーチャーが2つ(default と semantic_chunk)で window_size=200 の場合、メインサーチャーが200件、セマンティックサーチャーが100件を取得するため、保持されるドキュメントは最大300件になります。

# 融合対象のウィンドウサイズ
rank.fusion.window_size=200

警告

rank.fusion.window_size は paging.search.page.max.size × 2 を下回れません。 paging.search.page.max.size が既定の 100 の場合、下限は 200 となり、これは rank.fusion.window_size の既定値と同じです。つまり 既定の構成では window_size を 既定値より小さくできません。小さい値を設定しても起動時にWARNログが出力され、200 に引き上げられます。実際に小さくするには paging.search.page.max.size を先に下げる 必要がありますが、これは検索画面やAPIで1ページに要求できる最大件数も同時に下がります。

処理時間

  • 複数の検索を実行するため、レスポンス時間が増加します。

  • rank.fusion.threads で並列実行のスレッド数を設定します。

# 並列実行のスレッド数(0以下の場合、利用可能なCPUコア数 × 3 ÷ 2 + 1)
rank.fusion.threads=-1

注釈

Fess 側で融合する場合、メインサーチャー以外のサーチャーは rank.fusion.timeout(ミリ秒、 デフォルト 10000)まで待機されます。それまでに応答しないサーチャーはその検索から除外され、 検索結果は部分的な結果(タイムアウト)として扱われます。メインサーチャーは常に完了まで待機 されます。0 以下を指定すると無制限に待機します。検索エンジン側で融合する場合、この設定は 適用されません(検索エンジン側でのランクフュージョン を参照)。

サーチャー障害時の動作

いずれかのサーチャーが例外で失敗した場合、そのサーチャーの結果は空として扱われ、 WARNログを出力したうえで、残りのサーチャーの結果だけで融合が続行されます。 検索リクエスト自体はエラーになりません。

ただし、クエリ構文エラー(InvalidQueryException)とページング上限超過 (ResultOffsetExceededException)は例外で、これらはそのままエラーとして返されます。 また、融合が行われない深いページ(開始位置 × 2 が rank.fusion.window_size 以上に なる位置)では、メインサーチャーで発生した例外はそのまま検索リクエストのエラーになります。

セマンティックサーチャーは、埋め込みプロバイダに接続できない場合や埋め込み処理に失敗した場合、 結果を空として返します。この場合もエラーにはならず、キーワード検索のみの結果になります。

トラブルシューティング

検索結果が期待と異なる

症状: Rank Fusion後の結果が期待と異なる

確認事項:

  1. searcher フィールドを確認する(「融合結果の確認」を参照)。すべてのドキュメントが ["default"] のみの場合、セマンティックサーチャーが結果を返していません。

  2. セマンティック検索がスキップされていないか確認する。フレーズやワイルドカードなどの検索構文を 含むクエリのほか、ソート、位置情報検索、類似ドキュメント検索では、セマンティックサーチャーは 結果を返さずキーワード検索のみの結果になります。スキップ条件の詳細は セマンティック検索(コンテンツチャンク+ベクトル検索) を参照してください。

  3. 各検索タイプの結果を個別に確認

  4. rank.fusion.rank_constant の値を調整

  5. 深いページ(開始位置 × 2 が rank.fusion.window_size 以上になる位置。既定では 101件目以降)では融合が行われず、メインサーチャーのみで検索されます。より多くのページで 融合結果を利用したい場合は rank.fusion.window_size を大きくしてください。検索エンジン側で 融合する場合はこの境界はなく、最大 rank.fusion.pagination_depth 件までのすべてのページが 融合されます(検索エンジン側でのランクフュージョン を参照)。

検索が遅い

症状: Rank Fusion有効時に検索が遅くなる

解決方法:

  1. rank.fusion.threads を調整する:

    rank.fusion.threads=4
    
  2. rank.fusion.window_size を減らす。ただし下限(paging.search.page.max.size × 2) を下回れないため、既定の構成では次の2つをセットで設定します:

    paging.search.page.max.size=50
    rank.fusion.window_size=100
    

    1ページに要求できる最大件数も下がる点に注意してください。設定後は再起動が必要です。

  3. 検索エンジン側で融合する場合は rank.fusion.pagination_depth を減らす。融合結果を ページングできる深さも浅くなる点に注意してください。設定後は再起動が必要です。

メモリ不足

症状: OutOfMemoryError が発生する

解決方法:

  1. 「検索が遅い」と同じ手順で rank.fusion.window_size を減らす

  2. JVMヒープサイズを増やす

参考情報