Skip to content

[Feature] Hybrid Retrieval(BM25 + Semantic統合検索) #64

@Kewton

Description

@Kewton

概要

BM25(全文検索)とSemantic(意味検索)のスコアを統合し、両方の強みを活かしたハイブリッド検索を実装する。

背景・動機

BM25はキーワード完全一致に強く、Semanticは意味的類似に強い。両者を組み合わせることで検索精度を向上させる。

提案する解決策

CLIオプション設計

既存の query--semantic の相互排他制約を以下のように変更する:

  • query 引数による検索: embedding存在時は自動でハイブリッド検索、不在時はBM25のみ
  • --semantic オプション: 従来通り単独セマンティック検索として維持
  • --no-semantic オプション(新規): ハイブリッドを無効化しBM25のみで検索
  • --no-semanticquery モード専用。--semantic / --symbol / --relatedconflicts_with 設定
# ハイブリッド検索(デフォルト:embeddingが存在すれば自動でハイブリッド)
commandindex search "認証の流れ"

# BM25のみ(従来の挙動)
commandindex search "認証の流れ" --no-semantic

# query + --heading 時はBM25のみで動作(ハイブリッド化しない)
commandindex search "認証の流れ" --heading "セクション名"

# 単独セマンティック検索(従来の --semantic と同じ)
commandindex search --semantic "認証の流れ"

注意: --semanticqueryconflicts_with_all 制約は維持する。ハイブリッド検索は query 引数使用時の内部動作として実装する。

--heading との組み合わせ: query + --heading 指定時はBM25のみで動作する(ハイブリッド化しない)。理由: --heading フィルタはtantivyのBM25検索固有の機能であり、セマンティック検索側では対応するフィルタが存在しないため。

スコア統合方式

Reciprocal Rank Fusion (RRF) を採用:

RRF_score(d) = Σ 1 / (k + rank_i(d))
  • k = 60(定数、業界標準値)
  • rank_i(d) = i番目の検索手法でのドキュメントdの順位
  • BM25とSemanticそれぞれのランキングからRRFスコアを算出

候補取得深さ(Oversampling): RRF前にBM25・Semanticともに limit * 3 件を取得し、RRF統合後に上位 limit 件に絞り込む。

片方のランキングにのみ存在するドキュメントの扱い:

  • BM25のみにヒットした場合: Semanticランクを limit * 3 + 1 として扱う
  • Semanticのみにヒットした場合: BM25ランクを limit * 3 + 1 として扱う

同点時の安定ソート: RRFスコアが同点の場合は (path, heading) の辞書順でソートする。

ドキュメント識別キー

RRF統合時のドキュメント一致判定には (path, heading) のペアを使用する。

  • BM25: SearchResult.path + SearchResult.heading
  • Semantic: EmbeddingSimilarityResult.file_path + EmbeddingSimilarityResult.section_heading

制約: 同一ファイル内の同名見出しはembeddingストレージ側で INSERT OR REPLACE により1件に統合される(既存動作)。そのため (path, heading) ペアはユニークとなる。この制約は既存のインデックス処理パイプラインが保証する。

動作モード

embeddingの有無 --no-semantic --heading 挙動
あり なし なし ハイブリッド検索(BM25 + Semantic)
あり あり - BM25のみ
あり なし あり BM25のみ(--heading指定時はハイブリッド化しない)
なし なし - BM25のみ(従来互換、stderrに情報メッセージ出力)
なし あり - BM25のみ
  • embeddingが存在する場合、通常のsearchコマンドは自動的にハイブリッドモードになる
  • --no-semantic で明示的にBM25のみに切り替え可能
  • --heading 指定時はBM25のみで動作
  • embeddingが存在しない場合はstderrに No embeddings found, using BM25 only. Run 'commandindex embed' to enable hybrid search. を出力

フォールバック条件

query 引数による検索(自動ハイブリッド)では、後方互換性を維持するため、セマンティック検索側の障害時はBM25結果のみにフォールバックする。

条件 query 引数時の挙動 --semantic 明示時の挙動
count_embeddings() == 0 BM25のみ(stderrに情報メッセージ) エラー(従来通り)
embeddings.db が存在しない BM25のみ(stderrに情報メッセージ) エラー(従来通り)
embedding provider初期化失敗 BM25のみ(stderrに警告メッセージ) エラー
embedding API/ネットワーク失敗 BM25のみ(stderrに警告メッセージ) エラー

設計根拠: 通常の search <query> は従来BM25のみで完結しており、ネットワークやembedding providerに依存しなかった。自動ハイブリッド化でこのデフォルト挙動が失敗するようになるのは後方互換性破壊となるため、query引数時はセマンティック側の障害をグレースフルに処理する。

検索フロー

  1. クエリテキストでBM25検索(tantivy、limit * 3 件取得) → ランキングA
  2. クエリテキストをベクトル化 → コサイン類似度検索(limit * 3 件取得) → ランキングB
  3. RRFでランキングA・Bを統合 → 最終ランキング
  4. 上位 limit 件を出力

注意: ステップ2でエラーが発生した場合(query 引数時)、BM25結果のみを返す。

出力結果型

ハイブリッド検索結果は既存の SearchResult 型を再利用し、score フィールドにRRFスコアを格納する。これにより既存の format_results フォーマッタをそのまま利用できる。

  • BM25のみ検索時: score = BM25スコア(従来通り)
  • ハイブリッド検索時: score = RRFスコア(JSON出力の score フィールドもRRFスコアとなる)

影響範囲

変更対象ファイル

ファイル 変更内容
src/main.rs --no-semantic オプション追加(--semantic/--symbol/--relatedとconflict)、パターンマッチ更新
src/cli/search.rs run() にハイブリッド統合ロジック追加(embedding存在チェック→セマンティック実行→RRF統合→フォールバック)
src/search/hybrid.rs 新規作成: RRFスコア統合アルゴリズム
src/search/mod.rs pub mod hybrid; 追加

影響を受けないファイル

  • src/indexer/reader.rs - BM25検索ロジックは変更なし
  • src/indexer/symbol_store.rs - セマンティック検索ロジックは変更なし
  • src/output/ - SearchResult型を再利用するためフォーマッタ変更なし
  • src/search/related.rs - 関連検索は変更なし
  • src/cli/embed.rs, src/cli/index.rs 等 - 他サブコマンドは変更なし

テスト影響

  • 既存テストは embedding 不在環境で動作するため破壊されない
  • --no-semantic オプションのCLIパーステスト追加が必要
  • ハイブリッド検索のRRFロジック単体テスト追加が必要
  • ハイブリッド検索のE2Eテスト追加が推奨

テスト観点

  • query 単体(embedding存在時→ハイブリッド)
  • query + --no-semantic(→BM25のみ)
  • --semantic 単体(従来通り動作)
  • embedding 0件時フォールバック(stderrメッセージ確認)
  • query + --heading(→BM25のみ)
  • 片側のみヒット時のRRF計算
  • path / json 出力の回帰テスト
  • RRFスコア同点時の安定ソート
  • provider/API失敗時のBM25フォールバック(query引数時)

受け入れ基準

  • embeddingが存在する場合、query 引数での検索が自動的にハイブリッドモードになる
  • --no-semantic でBM25のみに切り替えできる(query モード専用)
  • --semantic オプションは従来通り単独セマンティック検索として動作する
  • query + --heading 指定時はBM25のみで動作する
  • RRFによるスコア統合が正しく動作する(k=60、oversampling=limit*3)
  • 片方のランキングにのみ存在するドキュメントが正しく処理される
  • ドキュメント識別キー (path, heading) で正しくマッチングされる
  • embeddingが存在しない場合は従来のBM25のみ(エラーなし、stderrに情報メッセージ)
  • query 引数時はprovider/API障害でもBM25にフォールバック(後方互換性維持)
  • --semantic 明示時はprovider/API障害でエラー(従来通り)
  • human / json / path 出力形式に対応(ハイブリッド時のJSON score はRRFスコア)
  • cargo test / clippy / fmt 全パス

設計上の決定事項

  • 重みオプション非実装: --bm25-weight / --semantic-weight は本Issueでは実装しない。標準RRFはランク順位ベースで重みの概念がないため、まず均等重みRRFで実装し、重み調整は将来のIssueで対応する。
  • k定数固定: RRFのk=60は固定値とする。将来的に設定可能にする拡張ポイントとして設計する。
  • SearchResult型再利用: ハイブリッド結果は新しい型を作らず SearchResult を再利用。score フィールドの意味は検索モードにより異なる(BM25スコアまたはRRFスコア)。
  • RRFスコア同点時: (path, heading) の辞書順で安定ソート。
  • Oversampling: RRF前にBM25・Semanticともに limit * 3 件を取得。
  • 後方互換性: query 引数時のセマンティック側障害はBM25フォールバック。--semantic 明示時のみエラー。

依存 Issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions