Skip to content

[Feature] Reranking(検索結果の再順位付け) #65

@Kewton

Description

@Kewton

概要

ハイブリッド検索の結果に対して、クエリとドキュメントのペアワイズ類似度を再計算し、より精度の高い順位付けを行うReranking機能を実装する。

背景・動機

初回検索(BM25 + Semantic)は高速だが粗い。Rerankingで上位候補をより精密に再評価することで、最終的な検索精度を向上させる。

提案する解決策

Reranking方式

Cross-Encoder方式を採用:

  • クエリとドキュメントのペアを入力し、関連度スコアを直接出力
  • Bi-Encoder(embedding類似度)より高精度だがコストが高い
  • 上位N件のみにRerankingを適用(デフォルト: top-20 → rerank → top-k出力)

対応モデル・APIエンドポイント

モデル 方式 エンドポイント 備考
Ollama(プロンプト方式) ローカル POST /api/generate プライマリ。汎用生成モデル(例: llama3)ベースのプロンプトスコアリング
Cohere Rerank API API POST /v1/rerank 初期実装ではトレイト(RerankProviderType enum)定義のみ。実装は将来Issueに分離

Cohere実装スコープ(M4): 初期実装はOllamaプロバイダーのみ。Cohereは RerankProviderType::Cohere バリアントの定義と、cohere.rs に空のスケルトン(未実装エラー RerankError::ProviderNotImplemented を返す)を配置するに留める。Cohere APIの実際の呼び出し実装は将来Issueとして分離する。

Ollama プロンプト方式の詳細

Ollama の /api/generate エンドポイントを使用し、汎用生成モデル(例: llama3)によるプロンプトベースでクエリとドキュメントの関連度スコアを算出する。bge-reranker-v2-m3 等の専用Rerankモデルも対応モデルの一例として利用可能だが、プライマリは汎用生成モデルとする。

リクエスト形式:

{
  "model": "llama3",
  "prompt": "Given the query: \"<query>\"\n\nRate the relevance of the following document on a scale of 0 to 10:\n\n<document_text>\n\nRelevance score (0-10):",
  "stream": false,
  "options": {
    "temperature": 0.0,
    "num_predict": 10
  }
}

レスポンス形式:

{
  "model": "llama3",
  "response": "8",
  "done": true
}
  • レスポンスから数値を抽出し、0-10の範囲にクランプしてスコアとする
  • パース失敗時はスコア0として扱い、警告をログ出力する

将来対応: Ollama が /api/rerank エンドポイントを正式サポートした場合、ネイティブRerank APIへの切り替えを検討する。その際は RerankProvider トレイトの実装を追加するだけで対応可能な設計とする。

document_text の組み立て規則

Reranking時にモデルへ送信する document_text は、以下の規則で組み立てる:

  • heading + "\n" + body を連結して構成する
  • 最大 4096文字 で truncate する(4096文字を超える場合は先頭4096文字を使用)
  • heading が空の場合は body のみを使用する

変更(S4): 最大長を8192文字から4096文字に縮小。Ollamaへの逐次リクエストにおけるパフォーマンスを考慮し、候補あたりの入力サイズを抑制する。

適用対象の検索モード

Rerankingは SearchResult ベースの経路 に適用する。BM25フォールバック時もrerankは適用される(rerank対象はSearchResult型であればモードに関わらず適用)。

検索モード --rerank 指定時の動作
ハイブリッド検索(デフォルト) Reranking適用
--no-semantic (BM25単体) Reranking適用
BM25フォールバック(embedding未設定時) Reranking適用(SearchResult型を返す経路のため)
--semantic (Semantic Search単体) 初期実装ではスコープ外。SemanticSearchResult型を使用するため、将来Issueで対応予定
--symbol (シンボル検索) 使用不可--rerank と排他。異なる結果型を返すため)
--related (関連ファイル検索) 使用不可--rerank と排他。異なる結果型を返すため)

変更(S2): BM25フォールバック経路もReranking適用対象として明記。SearchResult型を返す経路であれば検索モードに関わらずrerankを適用する。

  • --rerank--symbol--related、および --semanticconflicts_with に設定する
  • --rerank はテキスト検索モード(ハイブリッド / --no-semantic / BM25フォールバック)の後段処理として適用される
  • Rerankingは検索結果の順序のみを変更し、検索対象の絞り込みロジックには影響しない
  • --semantic 経路のReranking対応は将来Issueとする(SemanticSearchResult → RerankCandidate への変換処理が必要)

CLIオプション

# Reranking有効(embeddingとrankモデルが利用可能な場合)
commandindex search "認証の流れ" --rerank

# Reranking無効(デフォルト)
commandindex search "認証の流れ"

# リランク候補数を指定
commandindex search "認証の流れ" --rerank --rerank-top 30

# --no-semantic との組み合わせ
commandindex search "認証の流れ" --no-semantic --rerank

# 以下はエラーになる(conflicts_with)
# commandindex search "認証の流れ" --symbol --rerank   ← エラー
# commandindex search "認証の流れ" --related --rerank   ← エラー
# commandindex search "認証の流れ" --semantic --rerank  ← エラー(初期実装ではスコープ外)

引数の優先順位: CLI引数 > config.toml > デフォルト値

  • --rerank-top 30 が指定された場合、config.toml の top_candidates より優先される
  • config.toml に [rerank] セクションがない場合、デフォルト値を使用する
  • --rerank-toprequires = "rerank" とし、--rerank なしで --rerank-top のみ指定した場合はclapエラーにする

設定

# .commandindex/config.toml
[rerank]
provider = "ollama"          # "ollama" | "cohere"
model = "llama3"             # 使用するモデル名(デフォルト: llama3)
top_candidates = 20          # リランク対象の候補数(デフォルト: 20)
endpoint = "http://localhost:11434"  # Ollamaエンドポイント(オプション)

Config構造体の変更:

pub struct Config {
    // ... 既存フィールド
    pub rerank: Option<RerankConfig>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum RerankProviderType {
    Ollama,
    Cohere,  // enum定義のみ。実装は将来Issue
}

pub struct RerankConfig {
    pub provider: RerankProviderType,  // プロバイダー種別(enum)
    pub model: String,                 // モデル名(デフォルト: "llama3")
    pub top_candidates: usize,         // リランク対象数(デフォルト: 20)
    pub endpoint: Option<String>,      // エンドポイントURL(オプション)
    pub api_key: Option<String>,       // APIキー(Cohere等で使用、オプション)
    pub timeout_secs: u64,             // 全体タイムアウト秒数(デフォルト: 30)
}
  • rerank フィールドは Option<RerankConfig> とし、設定がない場合は None
  • --rerank 指定時に RerankConfigNone の場合、デフォルト値(provider=Ollama, model="llama3", top_candidates=20, timeout_secs=30)を使用する
  • provider フィールドは String ではなく RerankProviderType enum を使用し、型安全性を確保する

実装ガイド

検索フローの変更箇所

Reranking処理は src/cli/search.rs の検索フロー末尾に挿入される。具体的な変更箇所:

  1. src/cli/search.rs: run() 関数(ハイブリッド検索経路およびBM25フォールバック経路)にRerankingを適用。run_semantic_search() は対象外(初期実装スコープ外)。run() の結果返却前にRerankingを呼び出す分岐を追加する。--rerank フラグが有効な場合、SearchResult のリストを RerankCandidate に変換し、Reranking処理を実行後、スコアを上書きして再ソートする
  2. src/cli/(searchサブコマンド): --rerank / --rerank-top 引数の追加、conflicts_with 設定(--symbol, --related, --semantic
  3. src/rerank/: Rerankingモジュール(新規作成)

新モジュール構成

src/
├── rerank/                  # Rerankingモジュール(新規)
│   ├── mod.rs               # RerankProviderトレイト、RerankCandidate/RerankResult型、オーケストレーション
│   ├── ollama.rs            # OllamaRerankProvider(/api/generate プロンプト方式)
│   └── cohere.rs            # CohereRerankProvider(空スケルトン。RerankError::ProviderNotImplemented を返す)
  • RerankProvider トレイトを rerank/mod.rs に定義
  • 検索モジュールからは RerankProvider トレイト経由で呼び出し、実装を差し替え可能にする

API呼び出し方式

  • Ollamaへのリクエストは候補ごとに 逐次実行(初期実装)
  • 各リクエストのタイムアウトは 10秒(個別候補単位)
  • 全体タイムアウト: 30秒(デフォルト。RerankConfig.timeout_secs で設定可能)。全体タイムアウトに達した場合、スコア取得済みの候補のみで再ソートし返す。未処理の候補は元スコアを維持する
  • 将来的にバッチ化や tokio::spawn による並列化を検討する

スコアの扱い

  • Reranking後のスコア(0-10の範囲)で SearchResult.score フィールドを上書きする
  • 上書き前のオリジナルスコア(RRFスコアやBM25スコア)は保持しない(初期実装)
  • JSON出力時にはrerank後のスコアが score フィールドに出力される

出力フォーマット

human / json / path 出力形式に対応。

注記(S1): --rerank 指定時のJSON出力における score フィールドの意味が変わる。通常時はRRFスコアやBM25スコアだが、rerank時は 0-10のCross-Encoderスコア になる。スコアの値域とスケールが異なるため、rerankの有無でスコアの比較はできない点に留意。

検索フロー

  1. ハイブリッド検索(または --no-semantic のBM25検索、またはBM25フォールバック)でtop-N件を取得。N = max(limit, rerank_top) とする(limit はCLIの --limit 値、rerank_top--rerank-top 値またはデフォルト20)
  2. 各候補について document_text(heading + "\n" + body、最大4096文字で truncate)を組み立てる
  3. 各候補について (query, document_text) ペアでCross-Encoderスコアを算出
  4. Cross-Encoderスコアで SearchResult.score を上書きし、再ソート
  5. top-k件(--limit で指定された件数)を出力

変更(M1): 候補取得数を max(limit, rerank_top) とする。例: --limit 30 --rerank --rerank-top 20 の場合、30件を検索エンジンから取得し、上位20件をrerankし、最終的に30件を出力する。--limit 5 --rerank --rerank-top 20 の場合、20件を取得し、20件をrerankし、上位5件を出力する。これにより --limit--rerank-top より大きい場合でも必要な件数が確保される。

パフォーマンス要件

  • Rerankingの追加レイテンシ 運用目安: top-20候補に対して 10秒程度(ローカルOllama環境)。これは受け入れ基準ではなく、実環境での目安値とする
  • Ollamaへのリクエストは候補ごとに逐次実行(初期実装)。将来的にバッチ化や並列化を検討する
  • top_candidates のデフォルト値(20)はレイテンシと精度のバランスを考慮した値
  • 全体タイムアウト: デフォルト30秒。タイムアウト超過時はスコア取得済みの候補のみで再ソートし返す。未処理の候補は元スコアを維持する

変更(M3): 「10秒以内」を受け入れ基準から運用目安に変更。途中タイムアウト時はスコア取得済みの候補のみで再ソートし返す方針を明確化。

Graceful Degradation(障害時の振る舞い)

Rerankモデルが利用不可またはエラーが発生した場合の動作:

状況 動作
Rerankモデルに接続不可 Reranking前の検索結果をそのまま返す + stderr に警告出力
全件のスコア算出に失敗 元の検索結果の順序をそのまま返す + stderr に警告出力
一部の候補のスコア算出に失敗 失敗した候補はスコア0として扱い、結果に含める。同スコア時のtie-breakは元の順序を維持する(安定ソート) + stderr に警告出力
レスポンスのパースに失敗 スコア0として扱い、結果に含める + stderr に警告出力
全体タイムアウト超過 スコア取得済みの候補のみで再ソートし返す。未処理候補は元スコアを維持 + stderr に警告出力
  • --rerank 指定時にRerankingが完全にスキップされた場合、stderr に Warning: Reranking skipped due to <reason>. Returning original search results. を出力する
  • 検索自体は失敗させず、常に結果を返すことを保証する

テスト方針

トレイト設計

pub trait RerankProvider: Send + Sync {
    fn rerank(
        &self,
        query: &str,
        documents: Vec<RerankCandidate>,
    ) -> Result<Vec<RerankResult>, RerankError>;
}

テスト戦略

テスト種類 対象 方法
ユニットテスト スコアリングロジック MockRerankProvider を使用
ユニットテスト Graceful Degradation MockRerankProvider でエラーを返すケース
ユニットテスト 全件失敗時の元順序維持 MockRerankProvider で全件エラー → 元順序で返却を検証
ユニットテスト CLI引数パース --rerank, --rerank-top のパーステスト
ユニットテスト CLI引数排他制約 --rerank--symbol / --related / --semanticconflicts_with テスト
ユニットテスト --rerank-top requires --rerank --rerank-top 単独指定時のclapエラーテスト
統合テスト 検索フロー全体 MockRerankProvider で検索→Rerank→出力の流れを検証

既存テストへの影響(M2): 既存テスト(tests/cli_args.rs, tests/output_format.rs)は --rerank なしでは一切影響を受けない。Reranking機能はオプトインであり、--rerank フラグが指定されない限り従来の検索パスがそのまま実行される。--rerank 指定時のみ score の意味がCross-Encoderスコア(0-10)に変わる点に注意。

struct MockRerankProvider {
    scores: Vec<f32>,  // 返すスコアを事前設定
}

impl RerankProvider for MockRerankProvider {
    fn rerank(&self, _query: &str, documents: Vec<RerankCandidate>) -> Result<Vec<RerankResult>, RerankError> {
        // テスト用の固定スコアを返す
    }
}

CLI引数パーステストの詳細

// 正常系
#[test] fn rerank_flag_is_parsed() { /* --rerank が正しくパースされる */ }
#[test] fn rerank_top_value_is_parsed() { /* --rerank-top 30 が正しくパースされる */ }
#[test] fn rerank_top_requires_rerank() { /* --rerank-top は --rerank なしでclapエラーになる */ }

// 排他制約テスト(conflicts_with)
#[test] fn rerank_conflicts_with_symbol() { /* --rerank --symbol は clap エラーになる */ }
#[test] fn rerank_conflicts_with_related() { /* --rerank --related は clap エラーになる */ }
#[test] fn rerank_conflicts_with_semantic() { /* --rerank --semantic は clap エラーになる(初期実装スコープ外のため) */ }

// 優先順位テスト
#[test] fn cli_rerank_top_overrides_config() { /* CLI引数がconfig.tomlより優先される */ }

受け入れ基準

  • --rerank でCross-Encoderによる再順位付けが実行される
  • --rerank なしでは従来の検索結果(Rerankingスキップ)
  • --rerank-top でリランク候補数を指定できる
  • --rerank-toprequires = "rerank" であり、--rerank なしで単独指定時はclapエラーになる
  • --rerank はハイブリッド検索、--no-semantic、およびBM25フォールバックで動作する(SearchResultベースの経路すべて)
  • --rerank--symbol--related、および --semantic と排他(conflicts_with)であり、同時指定時はclapエラーになる
  • --semantic 経路のReranking対応は将来Issueとする
  • rerank時の候補取得数は max(limit, rerank_top) とし、--limit--rerank-top より大きい場合でも必要な件数を確保する
  • Rerankモデルが利用不可の場合、Reranking前の結果をそのまま返し警告を出力する
  • 全件失敗時は元の検索結果の順序をそのまま返す
  • 部分失敗時の同スコアtie-breakは元順序を維持する(安定ソート)
  • 途中タイムアウト時はスコア取得済みの候補のみで再ソートし返す
  • CLI引数が config.toml の設定より優先される
  • Config構造体に rerank: Option<RerankConfig> が追加されている
  • RerankConfig.providerRerankProviderType enum を使用している
  • RerankConfigendpoint, api_key, timeout_secs フィールドが含まれている
  • 既存のConfig関連テスト(デシリアライズ等)が RerankConfig 追加後も全パスする
  • RerankProvider トレイトが定義され、MockRerankProvider でテスト可能
  • Reranking後のスコアで SearchResult.score が上書きされる
  • document_text は heading + "\n" + body を連結し、最大4096文字で truncate される
  • 全体タイムアウト(デフォルト30秒)が設定されている
  • Rerankingの実装箇所は src/cli/search.rsrun() 関数内
  • Ollama プロバイダーは汎用生成モデル(例: llama3)をデフォルトとする
  • 初期実装はOllamaプロバイダーのみ。Cohereは RerankProviderType enum定義と空スケルトン(RerankError::ProviderNotImplemented)のみ
  • cohere.rs は未実装エラーを返すスケルトンとして配置される
  • 既存テスト(tests/cli_args.rs, tests/output_format.rs)は --rerank なしでは一切影響を受けない
  • human / json / path 出力形式に対応
  • JSON出力の score フィールドは、rerank時は0-10のCross-Encoderスコアとなる
  • cargo test / clippy / fmt 全パス

依存 Issue

将来対応

  • --semantic 経路のReranking対応(SemanticSearchResult → RerankCandidate 変換)
  • Ollama /api/rerank ネイティブAPIサポート
  • バッチ化 / 並列化によるパフォーマンス改善
  • Cohere Rerank API 実装(cohere.rs スケルトンに実装を追加)

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