概要
BM25(全文検索)とSemantic(意味検索)のスコアを統合し、両方の強みを活かしたハイブリッド検索を実装する。
背景・動機
BM25はキーワード完全一致に強く、Semanticは意味的類似に強い。両者を組み合わせることで検索精度を向上させる。
提案する解決策
CLIオプション設計
既存の query と --semantic の相互排他制約を以下のように変更する:
query 引数による検索: embedding存在時は自動でハイブリッド検索、不在時はBM25のみ
--semantic オプション: 従来通り単独セマンティック検索として維持
--no-semantic オプション(新規): ハイブリッドを無効化しBM25のみで検索
--no-semantic は query モード専用。--semantic / --symbol / --related と conflicts_with 設定
# ハイブリッド検索(デフォルト:embeddingが存在すれば自動でハイブリッド)
commandindex search "認証の流れ"
# BM25のみ(従来の挙動)
commandindex search "認証の流れ" --no-semantic
# query + --heading 時はBM25のみで動作(ハイブリッド化しない)
commandindex search "認証の流れ" --heading "セクション名"
# 単独セマンティック検索(従来の --semantic と同じ)
commandindex search --semantic "認証の流れ"
注意: --semantic と query の conflicts_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引数時はセマンティック側の障害をグレースフルに処理する。
検索フロー
- クエリテキストでBM25検索(tantivy、
limit * 3 件取得) → ランキングA
- クエリテキストをベクトル化 → コサイン類似度検索(
limit * 3 件取得) → ランキングB
- RRFでランキングA・Bを統合 → 最終ランキング
- 上位
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テスト追加が推奨
テスト観点
受け入れ基準
設計上の決定事項
- 重みオプション非実装:
--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
概要
BM25(全文検索)とSemantic(意味検索)のスコアを統合し、両方の強みを活かしたハイブリッド検索を実装する。
背景・動機
BM25はキーワード完全一致に強く、Semanticは意味的類似に強い。両者を組み合わせることで検索精度を向上させる。
提案する解決策
CLIオプション設計
既存の
queryと--semanticの相互排他制約を以下のように変更する:query引数による検索: embedding存在時は自動でハイブリッド検索、不在時はBM25のみ--semanticオプション: 従来通り単独セマンティック検索として維持--no-semanticオプション(新規): ハイブリッドを無効化しBM25のみで検索--no-semanticはqueryモード専用。--semantic/--symbol/--relatedとconflicts_with設定注意:
--semanticとqueryのconflicts_with_all制約は維持する。ハイブリッド検索はquery引数使用時の内部動作として実装する。--headingとの組み合わせ:query+--heading指定時はBM25のみで動作する(ハイブリッド化しない)。理由:--headingフィルタはtantivyのBM25検索固有の機能であり、セマンティック検索側では対応するフィルタが存在しないため。スコア統合方式
Reciprocal Rank Fusion (RRF) を採用:
候補取得深さ(Oversampling): RRF前にBM25・Semanticともに
limit * 3件を取得し、RRF統合後に上位limit件に絞り込む。片方のランキングにのみ存在するドキュメントの扱い:
limit * 3 + 1として扱うlimit * 3 + 1として扱う同点時の安定ソート: RRFスコアが同点の場合は
(path, heading)の辞書順でソートする。ドキュメント識別キー
RRF統合時のドキュメント一致判定には
(path, heading)のペアを使用する。SearchResult.path+SearchResult.headingEmbeddingSimilarityResult.file_path+EmbeddingSimilarityResult.section_heading制約: 同一ファイル内の同名見出しはembeddingストレージ側で
INSERT OR REPLACEにより1件に統合される(既存動作)。そのため(path, heading)ペアはユニークとなる。この制約は既存のインデックス処理パイプラインが保証する。動作モード
--no-semantic--heading--no-semanticで明示的にBM25のみに切り替え可能--heading指定時はBM25のみで動作No embeddings found, using BM25 only. Run 'commandindex embed' to enable hybrid search.を出力フォールバック条件
query引数による検索(自動ハイブリッド)では、後方互換性を維持するため、セマンティック検索側の障害時はBM25結果のみにフォールバックする。query引数時の挙動--semantic明示時の挙動count_embeddings() == 0設計根拠: 通常の
search <query>は従来BM25のみで完結しており、ネットワークやembedding providerに依存しなかった。自動ハイブリッド化でこのデフォルト挙動が失敗するようになるのは後方互換性破壊となるため、query引数時はセマンティック側の障害をグレースフルに処理する。検索フロー
limit * 3件取得) → ランキングAlimit * 3件取得) → ランキングBlimit件を出力注意: ステップ2でエラーが発生した場合(
query引数時)、BM25結果のみを返す。出力結果型
ハイブリッド検索結果は既存の
SearchResult型を再利用し、scoreフィールドにRRFスコアを格納する。これにより既存のformat_resultsフォーマッタをそのまま利用できる。score= BM25スコア(従来通り)score= RRFスコア(JSON出力のscoreフィールドもRRFスコアとなる)影響範囲
変更対象ファイル
src/main.rs--no-semanticオプション追加(--semantic/--symbol/--relatedとconflict)、パターンマッチ更新src/cli/search.rsrun()にハイブリッド統合ロジック追加(embedding存在チェック→セマンティック実行→RRF統合→フォールバック)src/search/hybrid.rssrc/search/mod.rspub 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等 - 他サブコマンドは変更なしテスト影響
--no-semanticオプションのCLIパーステスト追加が必要テスト観点
query単体(embedding存在時→ハイブリッド)query+--no-semantic(→BM25のみ)--semantic単体(従来通り動作)query+--heading(→BM25のみ)受け入れ基準
query引数での検索が自動的にハイブリッドモードになる--no-semanticでBM25のみに切り替えできる(queryモード専用)--semanticオプションは従来通り単独セマンティック検索として動作するquery+--heading指定時はBM25のみで動作する(path, heading)で正しくマッチングされるquery引数時はprovider/API障害でもBM25にフォールバック(後方互換性維持)--semantic明示時はprovider/API障害でエラー(従来通り)scoreはRRFスコア)設計上の決定事項
--bm25-weight/--semantic-weightは本Issueでは実装しない。標準RRFはランク順位ベースで重みの概念がないため、まず均等重みRRFで実装し、重み調整は将来のIssueで対応する。SearchResultを再利用。scoreフィールドの意味は検索モードにより異なる(BM25スコアまたはRRFスコア)。(path, heading)の辞書順で安定ソート。limit * 3件を取得。query引数時のセマンティック側障害はBM25フォールバック。--semantic明示時のみエラー。依存 Issue