Python AI Rerankクロスエンコーダー実践:RAG検索精度を40%向上させる5つのキーパターン

AI与大数据

なぜRAG検索はいつも「あと一歩」なのか?

3日かけて構築したRAGシステム、ユーザーが「返金方法」を聞いているのに「登録方法」のドキュメントが返ってくる。問題は大規模言語モデルではなく、検索層にある——RerankのないRAGは、審判のいない試合のようなものだ。初期検索が返すTop-K結果の中で、本当に関連性のあるものはわずか20%かもしれない。

2026年、RerankはRAGシステムの標準装備となっている。Cohere Rerank APIからオープンソースのクロスエンコーダー、ハイブリッド検索からカスタムファインチューニングまで、本記事では検索精度を40%向上させる5つのキーパターンを解説する。

コア概念クイックリファレンス

概念 英語 定義 代表的な応用
リランカー Reranker 初期検索結果を二次的に精密ランキングするモデル/コンポーネント RAG検索最適化、検索結果最適化
クロスエンコーダー Cross-Encoder queryとdocumentを結合して共同エンコードし、関連性スコアを出力 精密ランキング段階、QAマッチング
バイエンコーダー Bi-Encoder queryとdocumentをそれぞれ独立してエンコードし、ベクトル類似度でマッチング 一次選別段階、大規模リコール
レイトインタラクション Late Interaction queryとdocumentをそれぞれトークンレベルのベクトルにエンコードし、細粒度マッチングを実行 ColBERTモデル、高効率精密ランキング
ハイブリッド検索 Hybrid Retrieval 密検索(Dense)と疎検索(Sparse)を組み合わせた検索戦略 マルチモーダルリコール、セマンティック+キーワード
検索拡張生成 RAG 外部知識を検索して大規模言語モデルの生成を支援する技術パラダイム 企業ナレッジベース、インテリジェントカスタマーサービス
逆数ランク融合 RRF マルチパス検索結果の融合ランキングアルゴリズム ハイブリッド検索結果の統合
クロスアテンション Cross-Attention Transformerにおけるqueryとdocument間のアテンション機構 Cross-Encoder内部のコアメカニズム

RerankのないRAG:5つの致命的なペインポイント

  1. セマンティックドリフト:高次元空間において、Bi-Encoderは類似しているが無関係なドキュメントを誤ってリコールしやすい。ユーザーが「Python例外処理」を聞いているのに「Pythonインストールガイド」が返ってくる——ベクトル距離は近いが、意味は全く異なる。

  2. キーワードの欠落:純粋な密検索は正確なキーワードマッチング能力が弱い。「OAuth2.0認可コードフロー」を検索すると、Bi-Encoderは一般的な「OAuth入門」を返す可能性がある。正確なキーワードマッチングシグナルが欠けているためだ。

  3. 粗いランキング:初期検索はベクトルコサイン類似度のみでランキングするため、query-document間の深い相互作用を捉えられない。Top-10の中で本当に関連性のあるものは2〜3件しかないかもしれない。

  4. ロングテールクエリの不正確さ:希少なエンティティ、専門用語、略語などのロングテールクエリに対して、Bi-Encoderのエンコード品質が著しく低下し、検索精度が急落する。

  5. マルチインテントの混同:1つのクエリに複数の意図が含まれる場合、Bi-Encoderの単一ベクトル表現では区別できず、意図が混在した結果が返される。

パターン1:Cohere Rerank API統合——最速で始められるRerankソリューション

Cohere Rerankは現在 最も成熟した商用Rerank APIで、100以上の言語をサポートし、レイテンシは50ms以下、迅速な統合に最適。

"""
Cohere Rerank API 統合例
依存関係のインストール: pip install cohere>=5.0
"""

import cohere
from typing import List, Dict

class CohereReranker:
    """Cohere Rerank API ラッパー"""

    def __init__(self, api_key: str, model: str = "rerank-v3.5"):
        self.client = cohere.ClientV2(api_key=api_key)
        self.model = model

    def rerank(
        self,
        query: str,
        documents: List[str],
        top_n: int = 5,
        max_chunks_per_doc: int = 3,
    ) -> List[Dict]:
        """
        ドキュメントリストをリランキング

        Args:
            query: ユーザークエリテキスト
            documents: ランキング対象のドキュメントリスト
            top_n: 上位N件の結果を返す
            max_chunks_per_doc: ドキュメントあたりの最大チャンク数

        Returns:
            リランキング結果リスト(index, relevance_score, documentを含む)
        """
        response = self.client.rerank(
            model=self.model,
            query=query,
            documents=documents,
            top_n=top_n,
            max_chunks_per_doc=max_chunks_per_doc,
        )

        reranked_results = []
        for result in response.results:
            reranked_results.append({
                "index": result.index,
                "relevance_score": result.relevance_score,
                "document": documents[result.index],
            })

        return reranked_results


# === 完全な使用例 ===
def demo_cohere_rerank():
    """Cohere Rerank 完全使用デモ"""

    reranker = CohereReranker(api_key="your-cohere-api-key")

    query = "PythonでJSONパース例外を処理するには?"

    documents = [
        "Pythonインストールガイド:公式サイトからインストーラをダウンロードし、ダブルクリックで実行してインストールを完了します。",
        "JSONパースエラー処理:json.loads()を使用する際、json.JSONDecodeError例外をキャッチし、デバッグ用に元のテキストを記録します。",
        "Pythonリスト内包表記はリストを作成する簡潔な構文です。例:[x**2 for x in range(10)]",
        "PythonでJSONデータを処理する際、json.loads()呼び出しをtry-exceptでラップし、入力が有効なJSON文字列か検証することを推奨します。",
        "Flaskフレームワークでは、jsonify関数を使用してJSONレスポンスを素早く返すことができます。",
    ]

    results = reranker.rerank(query=query, documents=documents, top_n=3)

    print(f"クエリ: {query}\n")
    for i, result in enumerate(results, 1):
        print(f"Top-{i} | 関連度: {result['relevance_score']:.4f}")
        print(f"  ドキュメント: {result['document'][:80]}...")
        print()


# === RAGパイプラインとの統合 ===
class RAGPipelineWithCohere:
    """Cohere Rerank統合RAGパイプライン"""

    def __init__(
        self,
        cohere_api_key: str,
        embedding_model_name: str = "BAAI/bge-large-ja-v1.5",
    ):
        from sentence_transformers import SentenceTransformer

        self.encoder = SentenceTransformer(embedding_model_name)
        self.reranker = CohereReranker(api_key=cohere_api_key)
        self.document_store: List[Dict] = []

    def index_documents(self, documents: List[str], metadata: List[Dict] = None):
        """ドキュメントのインデックス"""
        embeddings = self.encoder.encode(documents, normalize_embeddings=True)
        for i, (doc, emb) in enumerate(zip(documents, embeddings)):
            self.document_store.append({
                "text": doc,
                "embedding": emb.tolist(),
                "metadata": metadata[i] if metadata else {},
            })

    def retrieve(
        self,
        query: str,
        top_k: int = 10,
        rerank_top_n: int = 3,
    ) -> List[Dict]:
        """検索とリランキング"""
        import numpy as np

        query_embedding = self.encoder.encode([query], normalize_embeddings=True)[0]

        # 一次選別:コサイン類似度
        scored_docs = []
        for doc in self.document_store:
            score = float(np.dot(query_embedding, doc["embedding"]))
            scored_docs.append({**doc, "score": score})

        scored_docs.sort(key=lambda x: x["score"], reverse=True)
        top_candidates = scored_docs[:top_k]

        # 精密ランキング:Cohere Rerank
        candidate_texts = [doc["text"] for doc in top_candidates]
        reranked = self.reranker.rerank(
            query=query, documents=candidate_texts, top_n=rerank_top_n,
        )

        final_results = []
        for r in reranked:
            original_doc = top_candidates[r["index"]]
            final_results.append({
                "text": original_doc["text"],
                "metadata": original_doc["metadata"],
                "rerank_score": r["relevance_score"],
                "initial_score": original_doc["score"],
            })

        return final_results


if __name__ == "__main__":
    demo_cohere_rerank()

パターン2:Sentence-Transformersクロスエンコーダーリランキング——オープンソースソリューションの最適解

データプライバシー要件が高く、外部APIを呼び出せない場合、ローカルデプロイのCross-Encoderが最適な選択。

"""
Sentence-Transformers Cross-Encoder リランキング
依存関係のインストール: pip install sentence-transformers>=3.0
"""

from sentence_transformers import CrossEncoder
from typing import List, Dict, Optional
import logging

logger = logging.getLogger(__name__)


class CrossEncoderReranker:
    """Cross-Encoderベースのローカルリランカー"""

    # 推奨モデルとその最大シーケンス長
    SUPPORTED_MODELS = {
        "cross-encoder/ms-marco-MiniLM-L-6-v2": 512,
        "cross-encoder/ms-marco-MiniLM-L-12-v2": 512,
        "cross-encoder/stsb-roberta-large": 512,
        "BAAI/bge-reranker-large": 512,
        "BAAI/bge-reranker-v2-m3": 8192,
    }

    def __init__(
        self,
        model_name: str = "BAAI/bge-reranker-v2-m3",
        max_length: Optional[int] = None,
        device: Optional[str] = None,
    ):
        self.model_name = model_name
        self.max_length = max_length or self.SUPPORTED_MODELS.get(model_name, 512)

        logger.info(f"Cross-Encoderモデルを読み込み: {model_name}")
        self.model = CrossEncoder(
            model_name,
            max_length=self.max_length,
            device=device,
        )
        logger.info("モデルの読み込み完了")

    def rerank(
        self,
        query: str,
        documents: List[str],
        top_n: int = 5,
        batch_size: int = 32,
    ) -> List[Dict]:
        """
        Cross-Encoderでドキュメントをリランキング

        Args:
            query: クエリテキスト
            documents: ランキング対象ドキュメントリスト
            top_n: 上位N件の結果を返す
            batch_size: 推論バッチサイズ

        Returns:
            リランキング結果リスト
        """
        # (query, document) ペアを構築
        pairs = [(query, doc) for doc in documents]

        # バッチ推論で関連性スコアを取得
        scores = self.model.predict(pairs, batch_size=batch_size)

        # スコア降順でソート
        ranked_indices = scores.argsort()[::-1]

        results = []
        for rank, idx in enumerate(ranked_indices[:top_n]):
            results.append({
                "index": int(idx),
                "relevance_score": float(scores[idx]),
                "document": documents[idx],
                "rank": rank + 1,
            })

        return results

    def rerank_with_threshold(
        self,
        query: str,
        documents: List[str],
        threshold: float = 0.5,
        top_n: int = 10,
    ) -> List[Dict]:
        """
        閾値フィルタリング付きリランキング——閾値未満の結果は除外

        Args:
            query: クエリテキスト
            documents: ランキング対象ドキュメントリスト
            threshold: 関連性閾値
            top_n: 最大返却数

        Returns:
            フィルタリング済みリランキング結果
        """
        results = self.rerank(query, documents, top_n=top_n)
        filtered = [r for r in results if r["relevance_score"] >= threshold]

        logger.info(
            f"リランキング完了: 入力{len(documents)}件, "
            f"閾値フィルタ後{len(filtered)}件 (閾値={threshold})"
        )

        return filtered


# === 完全な使用例 ===
def demo_cross_encoder_rerank():
    """Cross-Encoder リランキング完全デモ"""

    reranker = CrossEncoderReranker(
        model_name="BAAI/bge-reranker-v2-m3",
    )

    query = "Kubernetes Podのグレースフル終了戦略"

    documents = [
        "Dockerコンテナの基本操作コマンドにはrun、stop、rmなどがあり、初心者の入門学習に適しています。",
        "Kubernetes Podグレースフル終了:terminationGracePeriodSecondsを設定し、PreStopフックを実装して、コンテナがSIGTERM受信後にクリーンアップを完了することを保証します。",
        "Kubernetes ServiceのタイプにはClusterIP、NodePort、LoadBalancerなどがあり、異なるネットワーク公開ニーズに対応します。",
        "Pod終了フロー:kubeletがSIGTERMを送信 → グレースフル終了期間を待機 → SIGKILLで強制終了。PreStopにスリープ遅延を追加してコネクションドレインを待つことを推奨。",
        "HelmはKubernetesのパッケージ管理ツールで、アプリケーションのデプロイとアップグレードプロセスを簡素化できます。",
    ]

    results = reranker.rerank(query=query, documents=documents, top_n=3)

    print(f"クエリ: {query}\n")
    for result in results:
        print(f"Rank-{result['rank']} | スコア: {result['relevance_score']:.4f}")
        print(f"  ドキュメント: {result['document'][:80]}...")
        print()


# === マルチクエリ融合リランキング ===
class MultiQueryReranker:
    """マルチクエリ融合リランキング:1つのクエリを複数のサブクエリに書き換え、ランキング結果を統合"""

    def __init__(self, cross_encoder_model: str = "BAAI/bge-reranker-v2-m3"):
        self.reranker = CrossEncoderReranker(model_name=cross_encoder_model)

    def rerank_multi_query(
        self,
        queries: List[str],
        documents: List[str],
        top_n: int = 5,
        fusion_strategy: str = "rrf",
    ) -> List[Dict]:
        """
        マルチクエリ融合リランキング

        Args:
            queries: 複数のクエリテキストリスト
            documents: ランキング対象ドキュメントリスト
            top_n: 上位N件の結果を返す
            fusion_strategy: 融合戦略、rrf(逆数ランク融合)またはavg(平均スコア)をサポート

        Returns:
            融合後のリランキング結果
        """
        doc_scores = {i: 0.0 for i in range(len(documents))}

        for query in queries:
            results = self.reranker.rerank(query, documents, top_n=len(documents))

            if fusion_strategy == "rrf":
                # 逆数ランク融合 (Reciprocal Rank Fusion)
                k = 60  # RRFスムージングパラメータ
                for result in results:
                    doc_scores[result["index"]] += 1.0 / (k + result["rank"])
            elif fusion_strategy == "avg":
                # 平均スコア融合
                for result in results:
                    doc_scores[result["index"]] += result["relevance_score"]

        # 正規化
        if fusion_strategy == "avg":
            for idx in doc_scores:
                doc_scores[idx] /= len(queries)

        # ソート
        sorted_indices = sorted(
            doc_scores.keys(), key=lambda x: doc_scores[x], reverse=True
        )

        final_results = []
        for rank, idx in enumerate(sorted_indices[:top_n]):
            final_results.append({
                "index": int(idx),
                "fusion_score": float(doc_scores[idx]),
                "document": documents[idx],
                "rank": rank + 1,
            })

        return final_results


if __name__ == "__main__":
    demo_cross_encoder_rerank()

パターン3:ハイブリッド検索(Dense + Sparse + Rerank)——検索効果の天井

単一の検索方式には常に盲点がある。ハイブリッド検索は、密検索の意味理解能力と疎検索の正確なマッチング能力を組み合わせ、さらにRerankで精密ランキングを行う——2026年のRAGシステムのベストプラクティス。

"""
ハイブリッド検索:Dense + Sparse + Rerank
依存関係のインストール:
  pip install sentence-transformers>=3.0
  pip install rank-bm25
  pip install numpy
"""

from sentence_transformers import SentenceTransformer, CrossEncoder
from rank_bm25 import BM25Okapi
from typing import List, Dict, Optional
import numpy as np
import logging

logger = logging.getLogger(__name__)


class HybridRetrieverWithRerank:
    """ハイブリッド検索 + リランキングの完全パイプライン"""

    def __init__(
        self,
        dense_model_name: str = "BAAI/bge-large-ja-v1.5",
        cross_encoder_model: str = "BAAI/bge-reranker-v2-m3",
        rrf_k: int = 60,
    ):
        # 密検索モデル(Bi-Encoder)
        self.dense_model = SentenceTransformer(dense_model_name)
        # クロスエンコーダー(リランキング用)
        self.cross_encoder = CrossEncoder(cross_encoder_model)
        # RRF融合パラメータ
        self.rrf_k = rrf_k

        # ドキュメントストレージ
        self.documents: List[str] = []
        self.dense_embeddings: Optional[np.ndarray] = None
        self.bm25: Optional[BM25Okapi] = None

    def _tokenize_ja(self, text: str) -> List[str]:
        """日本語トークナイズ(簡易版)"""
        # 本番環境ではMeCabやSudachiを使用
        import re
        return re.findall(r'\w+', text.lower())

    def index_documents(self, documents: List[str]):
        """ドキュメントをインデックス、密・疎インデックスを構築"""
        self.documents = documents

        # 密インデックスを構築
        logger.info("密ベクトルインデックスを構築中...")
        self.dense_embeddings = self.dense_model.encode(
            documents, normalize_embeddings=True, show_progress_bar=True,
        )

        # 疎インデックス(BM25)を構築
        logger.info("BM25疎インデックスを構築中...")
        tokenized_corpus = [self._tokenize_ja(doc) for doc in documents]
        self.bm25 = BM25Okapi(tokenized_corpus)

        logger.info(f"インデックス完了、合計 {len(documents)} 件のドキュメント")

    def _dense_search(self, query: str, top_k: int) -> List[Dict]:
        """密検索"""
        query_embedding = self.dense_model.encode(
            [query], normalize_embeddings=True,
        )[0]

        scores = np.dot(self.dense_embeddings, query_embedding)
        top_indices = np.argsort(scores)[::-1][:top_k]

        return [
            {"index": int(idx), "score": float(scores[idx]), "text": self.documents[idx]}
            for idx in top_indices
        ]

    def _sparse_search(self, query: str, top_k: int) -> List[Dict]:
        """疎検索(BM25)"""
        tokenized_query = self._tokenize_ja(query)
        scores = self.bm25.get_scores(tokenized_query)
        top_indices = np.argsort(scores)[::-1][:top_k]

        return [
            {"index": int(idx), "score": float(scores[idx]), "text": self.documents[idx]}
            for idx in top_indices
        ]

    def _rrf_fuse(
        self,
        dense_results: List[Dict],
        sparse_results: List[Dict],
    ) -> List[Dict]:
        """逆数ランク融合(Reciprocal Rank Fusion)"""
        rrf_scores: Dict[int, float] = {}

        for rank, result in enumerate(dense_results):
            idx = result["index"]
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1.0 / (self.rrf_k + rank + 1)

        for rank, result in enumerate(sparse_results):
            idx = result["index"]
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1.0 / (self.rrf_k + rank + 1)

        sorted_indices = sorted(
            rrf_scores.keys(), key=lambda x: rrf_scores[x], reverse=True,
        )

        return [
            {
                "index": int(idx),
                "rrf_score": float(rrf_scores[idx]),
                "text": self.documents[idx],
            }
            for idx in sorted_indices
        ]

    def _rerank(
        self,
        query: str,
        candidates: List[Dict],
        top_n: int,
    ) -> List[Dict]:
        """Cross-Encoderでリランキング"""
        pairs = [(query, c["text"]) for c in candidates]
        scores = self.cross_encoder.predict(pairs)

        for i, candidate in enumerate(candidates):
            candidate["rerank_score"] = float(scores[i])

        candidates.sort(key=lambda x: x["rerank_score"], reverse=True)
        return candidates[:top_n]

    def search(
        self,
        query: str,
        dense_top_k: int = 20,
        sparse_top_k: int = 20,
        rerank_top_n: int = 5,
    ) -> List[Dict]:
        """
        ハイブリッド検索 + リランキング完全フロー

        Args:
            query: クエリテキスト
            dense_top_k: 密検索の返却数
            sparse_top_k: 疎検索の返却数
            rerank_top_n: 最終リランキング返却数

        Returns:
            最終ランキング結果
        """
        # Step 1: デュアルパスリコール
        dense_results = self._dense_search(query, dense_top_k)
        sparse_results = self._sparse_search(query, sparse_top_k)

        # Step 2: RRF融合
        fused_results = self._rrf_fuse(dense_results, sparse_results)

        # Step 3: Cross-Encoder精密ランキング
        final_results = self._rerank(query, fused_results, rerank_top_n)

        return final_results


# === 完全な使用例 ===
def demo_hybrid_retrieval():
    """ハイブリッド検索完全デモ"""

    retriever = HybridRetrieverWithRerank()

    documents = [
        "Python GIL(グローバルインタプリタロック):GILは同時に1つのスレッドのみがPythonバイトコードを実行することを保証し、マルチスレッドはIO集約型タスクに適しています。",
        "Pythonマルチプロセスプログラミング:multiprocessingモジュールを使用してGIL制限を回避し、各プロセスは独立したGILとメモリ空間を持ちます。",
        "Python asyncio非同期プログラミング:async/await構文でコルーチンを記述し、HTTPリクエストやデータベースクエリなどの高並行IO操作に適しています。",
        "Pythonスレッドプール:concurrent.futures.ThreadPoolExecutorは便利なスレッドプールインターフェースを提供し、IO集約型タスクの並列実行に適しています。",
        "Pythonパフォーマンス最適化のヒント:cProfileでボトルネックを分析し、Cythonでホットコードをコンパイルし、numpyで純粋なPythonループを置き換えます。",
        "Go言語並行モデル:goroutineとchannelはGoの並行プリミティブで、Pythonスレッドより軽量、CPU集約型の並列計算に適しています。",
        "Rust所有権システム:コンパイル時チェックでメモリ安全性を保証し、ガベージコレクション不要、システムレベルの高性能プログラミングに適しています。",
        "Pythonメモリ管理:参照カウントが主、世代別ガベージコレクションが副、循環参照はgcモジュールが処理します。",
    ]

    retriever.index_documents(documents)

    query = "Python並行プログラミングのベストプラクティス"
    results = retriever.search(query, rerank_top_n=3)

    print(f"クエリ: {query}\n")
    for result in results:
        print(f"Rerankスコア: {result['rerank_score']:.4f} | RRFスコア: {result['rrf_score']:.4f}")
        print(f"  ドキュメント: {result['text'][:80]}...")
        print()


if __name__ == "__main__":
    demo_hybrid_retrieval()

パターン4:カスタムCross-Encoderファインチューニング——Rerankerにあなたのドメインを理解させる

汎用Cross-Encoderは専門ドメイン(医療、法律、金融)でパフォーマンスが低下する。ファインチューニングは必須の道。

"""
カスタム Cross-Encoder ファインチューニング
依存関係のインストール:
  pip install sentence-transformers>=3.0
  pip install datasets
"""

from sentence_transformers import CrossEncoder, InputExample
from sentence_transformers.cross_encoder.evaluation import CECorrelationEvaluator
from torch.utils.data import DataLoader
from typing import List, Tuple, Optional
import logging

logger = logging.getLogger(__name__)


class CrossEncoderFineTuner:
    """Cross-Encoder ファインチューナー"""

    def __init__(
        self,
        base_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
        num_labels: int = 1,
        max_length: int = 512,
    ):
        self.base_model = base_model
        self.num_labels = num_labels
        self.max_length = max_length
        self.model = CrossEncoder(
            base_model,
            num_labels=num_labels,
            max_length=max_length,
        )

    def prepare_training_data(
        self,
        query_doc_pairs: List[Tuple[str, str, float]],
    ) -> List[InputExample]:
        """
        訓練データの準備

        Args:
            query_doc_pairs: (query, document, relevance_score) のトリプルリスト
                relevance_score: 二値分類は0/1、回帰は0-1の連続値

        Returns:
            InputExample リスト
        """
        examples = []
        for query, doc, score in query_doc_pairs:
            examples.append(InputExample(texts=[query, doc], label=score))

        logger.info(f"訓練データ準備完了: {len(examples)} 件")
        return examples

    def train(
        self,
        train_examples: List[InputExample],
        val_examples: Optional[List[InputExample]] = None,
        output_path: str = "./fine_tuned_cross_encoder",
        epochs: int = 3,
        batch_size: int = 16,
        warmup_steps: int = 100,
        learning_rate: float = 2e-5,
    ):
        """
        ファインチューニング訓練

        Args:
            train_examples: 訓練データ
            val_examples: 検証データ(オプション)
            output_path: モデル保存パス
            epochs: 訓練エポック数
            batch_size: バッチサイズ
            warmup_steps: ウォームアップステップ数
            learning_rate: 学習率
        """
        train_dataloader = DataLoader(
            train_examples,
            shuffle=True,
            batch_size=batch_size,
        )

        # 評価器の構築
        evaluator = None
        if val_examples:
            val_pairs = [(ex.texts[0], ex.texts[1]) for ex in val_examples]
            val_labels = [ex.label for ex in val_examples]
            evaluator = CECorrelationEvaluator(
                sentences1=[p[0] for p in val_pairs],
                sentences2=[p[1] for p in val_pairs],
                scores=val_labels,
                name="validation",
            )

        # 訓練設定
        train_config = {
            "train_dataloader": train_dataloader,
            "evaluator": evaluator,
            "epochs": epochs,
            "warmup_steps": warmup_steps,
            "output_path": output_path,
            "show_progress_bar": True,
        }

        # num_labelsに基づいて損失関数を選択
        if self.num_labels == 1:
            # 回帰タスク:MSE損失
            self.model.fit(
                **train_config,
                loss_fct="MSE",
            )
        else:
            # 分類タスク:クロスエントロピー損失
            self.model.fit(
                **train_config,
            )

        logger.info(f"モデル保存先: {output_path}")

    def load_fine_tuned(self, model_path: str) -> CrossEncoder:
        """ファインチューニング済みモデルの読み込み"""
        self.model = CrossEncoder(model_path)
        logger.info(f"ファインチューニング済みモデルを読み込み: {model_path}")
        return self.model


# === ドメインデータ構築例 ===
def build_domain_training_data() -> List[Tuple[str, str, float]]:
    """
    ドメイン訓練データの構築(例:医療ドメイン)

    Returns:
        (query, document, relevance) トリプルリスト
    """
    training_pairs = [
        # ポジティブサンプル
        ("高血圧の治療法は?", "高血圧治療ガイドライン:第一選択薬にはACEI、ARB、CCBなどがあり、患者の合併症に応じて個別化して選薬する必要があります。", 1.0),
        ("糖尿病の食事注意事項", "糖尿病食事管理:総カロリー摂取を制御し、低GI食品を選択し、精製糖を制限し、食物繊維摂取を増加させます。", 1.0),
        ("風邪で発熱したら何を飲む?", "風邪の対症療法:体温が38.5°Cを超える場合はアセトアミノフェンまたはイブプロフェンを服用し、水分補給と休息に注意します。", 1.0),

        # ネガティブサンプル
        ("高血圧の治療法は?", "風邪の対症療法:体温が38.5°Cを超える場合はアセトアミノフェンまたはイブプロフェンを服用し、水分補給と休息に注意します。", 0.0),
        ("糖尿病の食事注意事項", "高血圧治療ガイドライン:第一選択薬にはACEI、ARB、CCBなどがあり、患者の合併症に応じて個別化して選薬する必要があります。", 0.0),
        ("風邪で発熱したら何を飲む?", "Pythonインストールガイド:公式サイトからインストーラをダウンロードし、ダブルクリックで実行して完了します。", 0.0),

        # ハードネガティブ(類似しているが無関係)
        ("高血圧の治療法は?", "低血圧の診断基準:収縮期血圧90mmHg未満または拡張期血圧60mmHg未満、薬物要因を排除する必要があります。", 0.2),
        ("糖尿病の食事注意事項", "糖尿病薬物療法:メトホルミンは2型糖尿病の第一選択薬で、腎機能と乳酸値のモニタリングが必要です。", 0.4),
    ]

    return training_pairs


# === 完全なファインチューニングフロー ===
def demo_fine_tuning():
    """Cross-Encoder ファインチューニング完全デモ"""

    fine_tuner = CrossEncoderFineTuner(
        base_model="cross-encoder/ms-marco-MiniLM-L-6-v2",
        num_labels=1,
    )

    # 訓練データの準備
    training_pairs = build_domain_training_data()
    train_examples = fine_tuner.prepare_training_data(training_pairs)

    # 訓練セットと検証セットに分割
    split_idx = int(len(train_examples) * 0.8)
    train_data = train_examples[:split_idx]
    val_data = train_examples[split_idx:]

    # ファインチューニング訓練
    fine_tuner.train(
        train_examples=train_data,
        val_examples=val_data,
        output_path="./models/medical_cross_encoder",
        epochs=3,
        batch_size=8,
        learning_rate=2e-5,
    )

    # ファインチューニング済みモデルの読み込みとテスト
    model = fine_tuner.load_fine_tuned("./models/medical_cross_encoder")
    scores = model.predict([
        ("高血圧の治療法は?", "高血圧治療ガイドライン:第一選択薬にはACEI、ARB、CCBなどがあります。"),
        ("高血圧の治療法は?", "風邪の対症療法:体温が38.5°Cを超える場合はアセトアミノフェンを服用します。"),
    ])
    print(f"関連ドキュメントスコア: {scores[0]:.4f}")
    print(f"無関係ドキュメントスコア: {scores[1]:.4f}")


if __name__ == "__main__":
    demo_fine_tuning()

パターン5:本番級RAGパイプライン——プロトタイプから本番稼働までの完全ソリューション

前述の4つのパターンをデプロイ可能な本番級RAGパイプラインに統合し、キャッシュ、フォールバック、モニタリングなどのエンジニアリング機能を含む。

"""
本番級 RAG パイプライン(リランキング付き)
依存関係のインストール:
  pip install sentence-transformers>=3.0
  pip install rank-bm25
  pip install redis
  pip install numpy
"""

from sentence_transformers import SentenceTransformer, CrossEncoder
from rank_bm25 import BM25Okapi
from typing import List, Dict, Optional
from dataclasses import dataclass, field
import numpy as np
import hashlib
import logging
import time

logger = logging.getLogger(__name__)


@dataclass
class RerankConfig:
    """リランキング設定"""
    dense_model_name: str = "BAAI/bge-large-ja-v1.5"
    cross_encoder_model: str = "BAAI/bge-reranker-v2-m3"
    dense_top_k: int = 20
    sparse_top_k: int = 20
    rerank_top_n: int = 5
    rrf_k: int = 60
    rerank_threshold: float = 0.3
    enable_cache: bool = True
    cache_ttl: int = 3600
    max_query_length: int = 512
    max_doc_length: int = 8192


@dataclass
class SearchResult:
    """検索結果"""
    text: str
    metadata: Dict = field(default_factory=dict)
    rerank_score: float = 0.0
    initial_score: float = 0.0
    retrieval_method: str = "hybrid_rerank"


class ProductionRAGPipeline:
    """本番級RAGパイプライン"""

    def __init__(self, config: RerankConfig):
        self.config = config

        # モデルの読み込み
        logger.info(f"Denseモデルを読み込み: {config.dense_model_name}")
        self.dense_model = SentenceTransformer(config.dense_model_name)

        logger.info(f"Cross-Encoderモデルを読み込み: {config.cross_encoder_model}")
        self.cross_encoder = CrossEncoder(config.cross_encoder_model)

        # ドキュメントストレージ
        self.documents: List[str] = []
        self.doc_metadata: List[Dict] = []
        self.dense_embeddings: Optional[np.ndarray] = None
        self.bm25: Optional[BM25Okapi] = None

        # キャッシュ(本番環境ではRedisに置き換え)
        self._cache: Dict[str, tuple] = {}

        # モニタリングメトリクス
        self._metrics = {
            "total_queries": 0,
            "cache_hits": 0,
            "avg_latency_ms": 0.0,
            "avg_rerank_score": 0.0,
        }

    def index_documents(self, documents: List[str], metadata: Optional[List[Dict]] = None):
        """ドキュメントのインデックス"""
        start_time = time.time()

        self.documents = documents
        self.doc_metadata = metadata or [{} for _ in documents]

        # 密インデックス
        self.dense_embeddings = self.dense_model.encode(
            documents, normalize_embeddings=True, show_progress_bar=True,
        )

        # BM25インデックス
        import re
        tokenized = [re.findall(r'\w+', doc.lower()) for doc in documents]
        self.bm25 = BM25Okapi(tokenized)

        elapsed = (time.time() - start_time) * 1000
        logger.info(f"インデックス完了: {len(documents)}件のドキュメント, 所要時間{elapsed:.0f}ms")

    def _get_cache_key(self, query: str) -> str:
        """キャッシュキーの生成"""
        raw = f"{query}:{self.config.dense_top_k}:{self.config.rerank_top_n}"
        return hashlib.md5(raw.encode()).hexdigest()

    def _check_cache(self, query: str) -> Optional[List[SearchResult]]:
        """キャッシュの確認"""
        if not self.config.enable_cache:
            return None

        cache_key = self._get_cache_key(query)
        if cache_key in self._cache:
            cached_data, cached_time = self._cache[cache_key]
            if time.time() - cached_time < self.config.cache_ttl:
                self._metrics["cache_hits"] += 1
                return cached_data

        return None

    def _set_cache(self, query: str, results: List[SearchResult]):
        """キャッシュに書き込み"""
        if not self.config.enable_cache:
            return

        cache_key = self._get_cache_key(query)
        self._cache[cache_key] = (results, time.time())

    def _validate_query(self, query: str) -> str:
        """クエリの前処理とバリデーション"""
        query = query.strip()
        if not query:
            raise ValueError("クエリテキストを空にすることはできません")
        if len(query) > self.config.max_query_length:
            logger.warning(f"クエリが長すぎます({len(query)}文字)、{self.config.max_query_length}に切り詰めます")
            query = query[: self.config.max_query_length]
        return query

    def _dense_search(self, query: str) -> List[Dict]:
        """密検索"""
        query_emb = self.dense_model.encode([query], normalize_embeddings=True)[0]
        scores = np.dot(self.dense_embeddings, query_emb)
        top_indices = np.argsort(scores)[::-1][: self.config.dense_top_k]

        return [
            {"index": int(idx), "score": float(scores[idx]), "text": self.documents[idx]}
            for idx in top_indices
        ]

    def _sparse_search(self, query: str) -> List[Dict]:
        """疎検索"""
        import re
        tokenized_query = re.findall(r'\w+', query.lower())
        scores = self.bm25.get_scores(tokenized_query)
        top_indices = np.argsort(scores)[::-1][: self.config.sparse_top_k]

        return [
            {"index": int(idx), "score": float(scores[idx]), "text": self.documents[idx]}
            for idx in top_indices
        ]

    def _rrf_fuse(
        self,
        dense_results: List[Dict],
        sparse_results: List[Dict],
    ) -> List[Dict]:
        """RRF融合"""
        rrf_scores: Dict[int, float] = {}

        for rank, result in enumerate(dense_results):
            idx = result["index"]
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1.0 / (self.config.rrf_k + rank + 1)

        for rank, result in enumerate(sparse_results):
            idx = result["index"]
            rrf_scores[idx] = rrf_scores.get(idx, 0) + 1.0 / (self.config.rrf_k + rank + 1)

        sorted_indices = sorted(rrf_scores.keys(), key=lambda x: rrf_scores[x], reverse=True)

        return [
            {"index": int(idx), "rrf_score": float(rrf_scores[idx]), "text": self.documents[idx]}
            for idx in sorted_indices
        ]

    def _rerank(self, query: str, candidates: List[Dict]) -> List[SearchResult]:
        """Cross-Encoderリランキング"""
        pairs = [(query, c["text"]) for c in candidates]
        scores = self.cross_encoder.predict(pairs)

        results = []
        for i, candidate in enumerate(candidates):
            rerank_score = float(scores[i])
            if rerank_score >= self.config.rerank_threshold:
                results.append(SearchResult(
                    text=candidate["text"],
                    metadata=self.doc_metadata[candidate["index"]],
                    rerank_score=rerank_score,
                    initial_score=candidate.get("rrf_score", candidate.get("score", 0.0)),
                    retrieval_method="hybrid_rerank",
                ))

        results.sort(key=lambda x: x.rerank_score, reverse=True)
        return results[: self.config.rerank_top_n]

    def search(self, query: str) -> List[SearchResult]:
        """
        本番級検索エントリポイント

        Args:
            query: ユーザークエリ

        Returns:
            ランキング済み検索結果
        """
        start_time = time.time()
        self._metrics["total_queries"] += 1

        # クエリバリデーション
        query = self._validate_query(query)

        # キャッシュ確認
        cached = self._check_cache(query)
        if cached is not None:
            logger.info("キャッシュヒット")
            return cached

        try:
            # デュアルパスリコール
            dense_results = self._dense_search(query)
            sparse_results = self._sparse_search(query)

            # RRF融合
            fused = self._rrf_fuse(dense_results, sparse_results)

            # リランキング
            results = self._rerank(query, fused)

            # キャッシュに書き込み
            self._set_cache(query, results)

            # モニタリングメトリクスの更新
            elapsed_ms = (time.time() - start_time) * 1000
            self._metrics["avg_latency_ms"] = (
                self._metrics["avg_latency_ms"] * 0.9 + elapsed_ms * 0.1
            )
            if results:
                self._metrics["avg_rerank_score"] = (
                    self._metrics["avg_rerank_score"] * 0.9
                    + results[0].rerank_score * 0.1
                )

            logger.info(f"検索完了: {len(results)}件の結果, 所要時間{elapsed_ms:.0f}ms")
            return results

        except Exception as e:
            logger.error(f"検索失敗: {e}")
            # フォールバック:密検索結果のみ返却
            dense_results = self._dense_search(query)[: self.config.rerank_top_n]
            return [
                SearchResult(
                    text=r["text"],
                    metadata=self.doc_metadata[r["index"]],
                    initial_score=r["score"],
                    retrieval_method="dense_fallback",
                )
                for r in dense_results
            ]

    def get_metrics(self) -> Dict:
        """モニタリングメトリクスの取得"""
        return {
            **self._metrics,
            "cache_hit_rate": (
                self._metrics["cache_hits"] / max(self._metrics["total_queries"], 1)
            ),
            "document_count": len(self.documents),
        }


# === 完全な使用例 ===
def demo_production_pipeline():
    """本番級RAGパイプライン完全デモ"""

    config = RerankConfig(
        dense_top_k=20,
        sparse_top_k=20,
        rerank_top_n=3,
        rerank_threshold=0.3,
        enable_cache=True,
    )

    pipeline = ProductionRAGPipeline(config)

    documents = [
        "Python例外処理ベストプラクティス:try-exceptで特定の例外をキャッチし、裸のexceptを避け、例外コンテキスト情報を記録します。",
        "Python型アノテーション:typingモジュールを使用して関数シグネチャを定義し、mypyと組み合わせて静的型チェックを行います。",
        "Pythonデコレータパターン:デコレータは関数の動作を変更する高階関数で、ログ、キャッシュ、権限チェックなどの横断的関心事によく使用されます。",
        "Pythonジェネレータとイテレータ:yieldキーワードでジェネレータを作成し、遅延評価でメモリを節約、大規模データセットの処理に適しています。",
        "Pythonコンテキストマネージャ:with文と__enter__/__exit__を組み合わせ、リソースの正しい解放を保証します。",
        "Go言語エラー処理:複数戻り値(error型)を使用して例外を代替し、各エラーを明示的に処理します。",
        "Rustライフタイム:コンパイラがライフタイムアノテーションで参照の有効性を保証し、ダングリングポインタを防止します。",
        "Python並行モデル:GILがマルチスレッド並列を制限、asyncio(IO集約)またはmultiprocessing(CPU集約)を推奨。",
    ]

    metadata = [
        {"source": "python-guide", "category": "error-handling"},
        {"source": "python-guide", "category": "type-system"},
        {"source": "python-guide", "category": "design-pattern"},
        {"source": "python-guide", "category": "advanced"},
        {"source": "python-guide", "category": "advanced"},
        {"source": "go-guide", "category": "error-handling"},
        {"source": "rust-guide", "category": "memory-safety"},
        {"source": "python-guide", "category": "concurrency"},
    ]

    pipeline.index_documents(documents, metadata)

    # 検索
    query = "Pythonエラー処理と例外キャッチ"
    results = pipeline.search(query)

    print(f"クエリ: {query}\n")
    for result in results:
        print(f"Rerankスコア: {result.rerank_score:.4f} | 方法: {result.retrieval_method}")
        print(f"  ドキュメント: {result.text[:80]}...")
        print(f"  メタデータ: {result.metadata}")
        print()

    # モニタリングメトリクス
    metrics = pipeline.get_metrics()
    print("モニタリングメトリクス:")
    for key, value in metrics.items():
        print(f"  {key}: {value}")


if __name__ == "__main__":
    demo_production_pipeline()

落とし穴ガイド:Rerankの5つのよくある罠

罠1:全ドキュメントにCross-Encoderランキングを実行

# ❌ 間違い:10万件のドキュメント全件にCross-Encoder推論を実行
all_documents = load_100k_documents()
results = cross_encoder.predict([(query, doc) for doc in all_documents])
# 結果:推論に数時間かかり、GPUメモリオーバーフロー

# ✅ 正しい:まずBi-EncoderでTop-Kを一次選別、その後Cross-Encoderで精密ランキング
from sentence_transformers import SentenceTransformer, CrossEncoder

bi_encoder = SentenceTransformer("BAAI/bge-large-ja-v1.5")
cross_encoder = CrossEncoder("BAAI/bge-reranker-v2-m3")

# Step 1: Bi-Encoderで高速一次選別
query_emb = bi_encoder.encode([query], normalize_embeddings=True)
scores = np.dot(doc_embeddings, query_emb.T).flatten()
top_k_indices = np.argsort(scores)[::-1][:50]  # Top-50のみ取得

# Step 2: Cross-EncoderでTop-50を精密ランキング
candidates = [all_documents[i] for i in top_k_indices]
pairs = [(query, doc) for doc in candidates]
rerank_scores = cross_encoder.predict(pairs)

罠2:Cross-Encoderの最大シーケンス長を無視

# ❌ 間違い:長いドキュメントをそのまま入力、モデルの最大長を超えて切り詰められる
model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")  # max_length=512
# 2000字のドキュメントを入力すると後半が切り詰められ、重要な情報が失われる
result = model.predict([("クエリ", long_document)])

# ✅ 正しい:長いコンテキストモデルを選択するか、ドキュメントをチャンク分割
# 選択肢1:長いコンテキストモデルを選択
model = CrossEncoder("BAAI/bge-reranker-v2-m3")  # max_length=8192

# 選択肢2:ドキュメントをチャンク分割して各チャンクをスコアリング
def rerank_long_document(query: str, doc: str, chunk_size: int = 400, overlap: int = 50):
    chunks = []
    for i in range(0, len(doc), chunk_size - overlap):
        chunks.append(doc[i:i + chunk_size])
    pairs = [(query, chunk) for chunk in chunks]
    scores = model.predict(pairs)
    return max(scores)  # 最高スコアのチャンクスコアをドキュメントスコアとする

罠3:BM25トークナイザと言語の不一致

# ❌ 間違い:日本語ドキュメントに英語トークナイザ(スペース分割)を使用
from rank_bm25 import BM25Okapi
corpus = ["Python例外処理はプログラミングの基礎スキル", "機械学習モデルの訓練には大量のデータが必要"]
tokenized = [doc.split() for doc in corpus]  # 日本語をスペースで分割、各文が1つのトークンになる
bm25 = BM25Okapi(tokenized)  # BM25が完全に機能しない

# ✅ 正しい:日本語トークナイザを使用
# 本番環境ではMeCabやSudachiを使用
import re

tokenized = [re.findall(r'\w+', doc) for doc in corpus]
bm25 = BM25Okapi(tokenized)

罠4:Rerank閾値の不適切な設定

# ❌ 間違い:閾値を設定せず、すべての結果を返す(無関係なものも含む)
results = reranker.rerank(query, documents, top_n=10)
# すべてのドキュメントが無関係でも10件返される

# ✅ 正しい:合理的な閾値を設定し、低スコア結果をフィルタリング
def smart_rerank(query, documents, top_n=10, min_threshold=0.3, max_threshold=0.7):
    results = reranker.rerank(query, documents, top_n=top_n)
    # 動的閾値:最高スコアの60%を閾値とするが、min_thresholdを下回らない
    if results:
        dynamic_threshold = max(min_threshold, results[0]["relevance_score"] * 0.6)
        dynamic_threshold = min(dynamic_threshold, max_threshold)
        filtered = [r for r in results if r["relevance_score"] >= dynamic_threshold]
        return filtered if filtered else [results[0]]  # 少なくとも1件は返す
    return []

罠5:キャッシュがドキュメント更新を考慮していない

# ❌ 間違い:キャッシュが永続し、ドキュメント更新後に古い結果を返す
cache = {}
def search(query):
    if query in cache:
        return cache[query]  # ドキュメントは更新済みだがキャッシュが無効化されていない
    results = do_search(query)
    cache[query] = results
    return results

# ✅ 正しい:バージョン付きキャッシュ戦略
class VersionedCache:
    def __init__(self, ttl_seconds: int = 3600):
        self._cache: Dict[str, tuple] = {}
        self._doc_version: int = 0
        self._ttl = ttl_seconds

    def invalidate_on_update(self):
        """ドキュメント更新時に呼び出し、すべてのキャッシュを無効化"""
        self._doc_version += 1

    def get(self, key: str) -> Optional[list]:
        if key in self._cache:
            cached_data, cached_version, cached_time = self._cache[key]
            # バージョン不一致またはTTL期限切れなら無効化
            if (cached_version == self._doc_version
                    and time.time() - cached_time < self._ttl):
                return cached_data
            del self._cache[key]
        return None

    def set(self, key: str, value: list):
        self._cache[key] = (value, self._doc_version, time.time())

エラートラブルシューティングクイックリファレンス

エラー現象 考えられる原因 トラブルシューティング手順 解決策
Cross-Encoder推論OOM バッチ入力ペア数が多すぎる batch_sizeとドキュメント長を確認 batch_sizeを減らすか、バッチ推論に分割
Rerank後精度が逆に低下 モデルとドメインの不一致 ドメインデータでモデル性能を評価 ドメインモデルに変更またはファインチューニング
BM25が空の結果を返す トークナイザの不一致 トークナイズ結果を出力して確認 日本語はMeCab、中国語はjiebaを使用
Cohere APIタイムアウト ネットワークまたはクォータの問題 API Keyとネットワークを確認 timeoutを増やし、リトライ機構を実装
密検索がすべて類似スコアを返す ベクトルが正規化されていない encode時のnormalizeパラメータを確認 normalize_embeddings=Trueを設定
RRF融合後結果が悪化 2パス検索結果の重複度が低い 各パスのリコール率を分析 top_kとrrf_kパラメータを調整
ファインチューニング後過学習 訓練データが少ない、または分布が不均一 訓練セットサイズとラベル分布を確認 データ追加、早期終了、正則化の追加
長いドキュメントのRerankスコアが異常 max_lengthを超えて切り詰められる ドキュメント長とモデルmax_lengthを確認 長いコンテキストモデルまたはチャンク戦略を使用
キャッシュヒット率が極めて低い キャッシュキーにランダム要素が含まれる キャッシュキー生成ロジックを確認 キャッシュキーにはqueryと設定パラメータのみを含める
GPU利用率が低い CPU-GPUデータ転送のボトルネック GPU利用率をモニタリング batch_sizeを増やし、DataLoaderを使用

高度な最適化:Rerank効果を向上させる5つのキー戦略

1. クエリ書き換え(Query Rewriting)

Rerankの前に、LLMでユーザーの元のクエリをより正確な検索クエリに書き換える:

def rewrite_query_with_llm(original_query: str, llm_client) -> List[str]:
    """LLMでクエリを書き換え、複数のサブクエリを生成"""
    prompt = f"""以下のユーザークエリを3つのより正確な検索クエリに書き換えてください。1行に1つ:
元のクエリ:{original_query}
要件:暗黙のコンテキストを補足し、曖昧さを排除し、核心的な意図を保持する。"""

    response = llm_client.chat(prompt)
    sub_queries = [q.strip() for q in response.strip().split("\n") if q.strip()]
    return [original_query] + sub_queries  # 元のクエリを保持

2. 適応型Top-K戦略

クエリの複雑さに応じて一次選別数を動的に調整:

def adaptive_top_k(query: str, base_top_k: int = 20) -> int:
    """クエリの特徴に基づいてTop-Kを適応的に調整"""
    # 短いクエリ(キーワード型)はより多くの候補が必要
    if len(query) <= 10:
        return base_top_k * 2
    # 長いクエリ(記述型)は意味が明確、候補は少なくてよい
    elif len(query) >= 50:
        return base_top_k
    else:
        return int(base_top_k * 1.5)

3. 段階的リランキング

まず軽量モデルで粗いランキング、その後重量モデルで精密ランキング:

def tiered_rerank(query, documents, top_n=5):
    """段階的リランキング:軽量モデル粗ランキング → 重量モデル精密ランキング"""
    # Tier 1: 軽量Cross-Encoder粗ランキング(MiniLM、推論高速)
    light_reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
    light_results = light_reranker.rank(query, documents, top_k=20)

    # Tier 2: 重量Cross-Encoder精密ランキング(Large、より正確)
    heavy_reranker = CrossEncoder("BAAI/bge-reranker-large")
    candidates = [documents[r["corpus_id"]] for r in light_results]
    final_results = heavy_reranker.rank(query, candidates, top_k=top_n)

    return final_results

4. マルチモデルアンサンブル

def ensemble_rerank(query, documents, models, top_n=5):
    """マルチモデルアンサンブルリランキング"""
    all_scores = {}

    for model_name, weight in models:
        model = CrossEncoder(model_name)
        pairs = [(query, doc) for doc in documents]
        scores = model.predict(pairs)

        for i, score in enumerate(scores):
            if i not in all_scores:
                all_scores[i] = 0.0
            all_scores[i] += float(score) * weight

    sorted_indices = sorted(all_scores.keys(), key=lambda x: all_scores[x], reverse=True)
    return [documents[i] for i in sorted_indices[:top_n]]

5. ネガティブフィードバック学習

def collect_hard_negatives(
    query: str,
    documents: List[str],
    reranker: CrossEncoderReranker,
    user_feedback: Dict[int, bool],
) -> List[Tuple[str, str, float]]:
    """ユーザーのネガティブフィードバックをハードネガティブサンプルとして収集"""
    training_pairs = []
    results = reranker.rerank(query, documents, top_n=len(documents))

    for result in results:
        idx = result["index"]
        is_relevant = user_feedback.get(idx, None)
        if is_relevant is not None:
            training_pairs.append((
                query,
                documents[idx],
                1.0 if is_relevant else 0.0,
            ))

    return training_pairs

Rerankアプローチ比較

アプローチ レイテンシ 精度 コスト デプロイ方法 ユースケース
Cohere Rerank API 50-200ms ★★★★☆ 従量課金 クラウドAPI 迅速な統合、多言語対応
BGE-Reranker-v2-m3 20-100ms ★★★★★ GPU推論 ローカルデプロイ 高精度、長いドキュメント
MiniLM-L-6-v2 5-30ms ★★★☆☆ CPUで実行可能 ローカルデプロイ 低レイテンシ、リソース制限環境
ColBERTレイトインタラクション 30-80ms ★★★★☆ GPU推論 ローカルデプロイ 細粒度マッチング
カスタムファインチューニングモデル ベースモデル依存 ★★★★★ 訓練+推論 ローカルデプロイ 専門ドメイン
ハイブリッド検索+Rerank 100-300ms ★★★★★ GPU推論 ローカルデプロイ 本番級ベストプラクティス

💡 選択ガイド:迅速な検証にはCohere Rerank、コストパフォーマンス重視ならBGE-Reranker、専門ドメインではファインチューニングが必須、本番環境ではハイブリッド検索+Rerankを推奨。

まとめ

RerankはRAGのオプションの拡張ではなく、必須要件である。RerankのないRAGシステムは、ブレーキのない自動車のようなもの——走ることはできても、正確に止まることができない。Cohere Rerank APIの5分間統合から、ハイブリッド検索+Cross-Encoderの本番級パイプラインまで、5つのキーパターンがプロトタイプから本番稼働までの完全なパスをカバーする。覚えておいてほしい:まずBi-Encoderでリコール、次にCross-Encoderで精密ランキング——これが2026年のRAG検索の黄金律だ。

オンラインツール推奨

Rerankパイプラインを構築する際、以下のオンラインツールが開発効率を向上させます:

  • 📄 JSONフォーマッター — Rerank APIが返すJSON結果をフォーマットし、レスポンス構造を素早く確認
  • 🌐 cURL to Code — Cohere Rerank APIのcURL例をワンクリックでPythonコードに変換
  • 🔒 ハッシュ計算ツール — ドキュメントフィンガープリントを生成してキャッシュキーに使用、効率的なキャッシュ管理を実現

ブラウザローカルツールを無料で試す →

#Rerank#交叉编码器#RAG检索增强#Python AI#2026#AI与大数据