RAGの検索失敗を解決する:BM25とハイブリッド検索の実践ガイド

AI tutorial - IT technology blog
AI tutorial - IT technology blog

問題:セマンティック検索が失敗する時

多くのエンジニアは、テキストを埋め込み(embeddings)に変換し、ベクトルデータベースに投入することからRAG(検索拡張生成)プロジェクトを開始します。これは魔法のように感じられます。しかし、ユーザーが特定のSKUやエラーコードを検索し、それに対して一般的なトラブルシューティングガイドが返されてしまうと、その魔法はすぐに解けてしまいます。

最近行ったテクニカルサポートボットの監査では、純粋なベクトル検索において、「0x8004210B」のような一意のIDを含むクエリの18%で失敗が発生していることが判明しました。問題は、ベクトル検索がセマンティック(意味的)であり、リテラル(逐語的)ではないことにあります。検索エンジンは「king(王)」が「queen(女王)」に近いことは理解しますが、「エラーA」と「エラーB」は言語的な文脈が同じであるため、しばしば交換可能なものとして扱ってしまいます。医療記録や金融ログのような高精度が求められる環境では、「惜しい」は失敗と同じです。正確な一致が必要なのです。

ハイブリッド検索(Hybrid Search)はこの問題を解決します。高密度ベクトル(Dense Vectors)の概念的な理解と、BM25(Sparse Vectors:疎ベクトル)の外科的な精度を組み合わせることで、詳細を見失うことなく意図を理解するシステムを構築できます。

クイックスタート:重み付け融合の実装

ハイブリッドシステムを構築するには、2つの異なる世界の性質を融合させる必要があります。ここでは、相互ランク融合(RRF:Reciprocal Rank Fusion)が業界標準となっています。これにより、スコアリングの尺度が異なるキーワード一致とベクトル近傍を、スケールを気にせずに組み合わせることができます。以下に、これらのストリームを統合する方法を示すPythonの実装例を示します。

from rank_bm25 import BM25Okapi
import numpy as np

# サンプルの技術ドキュメント
docs = [
    "Outlookでエラー0x8004210Bを修正する方法",
    "メールクライアントの一般的なセットアップガイド",
    "部品番号 #99-AF-12:高度なネットワークプロトコルのトラブルシューティング"
]

# 1. キーワード検索のセットアップ (BM25)
tokenized_corpus = [doc.split(" ") for doc in docs]
bm25 = BM25Okapi(tokenized_corpus)

# 2. 正確なコードに対するシミュレーションクエリ
query = "0x8004210B"
tokenized_query = query.split(" ")

# BM25スコアの計算
bm25_scores = bm25.get_scores(tokenized_query)
print(f"BM25スコア: {bm25_scores}")
# BM25は正確なエラーコードの一致を重視します

本番環境でこのレイヤーを追加すると、特に専門用語や部品番号が多いデータにおいて、「誤ったドキュメント」の検索エラーを大幅に削減できます。BM25は、埋め込みモデルが平滑化してしまいがちな特定のトークンを捉えます。

なぜBM25が依然として精度で勝るのか

高密度ベクトル vs 疎ベクトル

高密度ベクトル(Dense vectors)は文章の「雰囲気」を表します。類義語や意図をうまく捉えることができますが、異なる2つの技術用語が似たような文章に現れるために、同じベクトル空間に配置されてしまう「衝突」が起こりやすいという欠点があります。

BM25(Best Matching 25)はTF-IDFの進化形です。単語の頻度をカウントしますが、飽和曲線(saturation curve)を適用します。これにより、単一の単語が繰り返されることでスコアが支配されるのを防ぎます。これは抽象的な関係よりも、離散的でリテラルなトークンに焦点を当てるため、「疎(Sparse)」なアプローチと呼ばれます。

相互ランク融合(RRF)のロジック

コサイン類似度スコア(0.0から1.0)と、20以上の値を取り得るBM25スコアを単純に加算することはできません。RRFは、スコアではなく順位(ランク)を見ることでこの問題を回避します。あるドキュメントがBM25で1位、ベクトル検索で50位だった場合、RRFは両方のリストでの位置を使用して統合スコアを計算します。

標準的な公式は score = 1 / (rank + k) です。通常、k は60に設定されます。この定数は、非常に上位にランクされた項目が、もう一方のリストで少し低い順位にある関連項目を完全に圧倒してしまわないようにするためのものです。

最適化:ハイブリッドバランスの調整

すべてのデータセットが同じではありません。キーワードを重視すべきものもあれば、セマンティクスを重視すべきものもあります。重み付けされたアルファ(alpha)パラメータを使用して、特定のデータに最適なスイートスポットを見つけましょう。

def hybrid_score(vector_rank, bm25_rank, alpha=0.3):
    # アルファ値0.3はBM25を優先(技術ドキュメントに最適)
    # アルファ値0.7はベクトル検索を優先(クリエイティブなテキストに最適)
    k = 60
    return (alpha * (1 / (vector_rank + k))) + ((1 - alpha) * (1 / (bm25_rank + k)))

「語彙外(Out-of-Vocabulary)」問題の解決

埋め込みモデルは、トレーニングデータに含まれていなかった社内のプロジェクト名や新しいブランドIDの扱いに苦戦することがよくあります。ユーザーが「ProjectX-Alpha」を検索した場合、ベクトルモデルはそれを「Project Alpha」にマッピングしてしまい、特定の「X」という識別子を失う可能性があります。BM25はそのIDを一意の指紋として扱うため、正しいドキュメントを確実に検索結果の上位に保持できます。

本番環境へのチェックリスト

  • ハードメタデータフィルタ: ユーザーが「2024年」のドキュメントのみを必要としていることがわかっている場合、データベース全体を検索しないでください。まずメタデータでフィルタリングし、その後にハイブリッド検索を実行します。
  • トークンのクリーンアップ: BM25はマッチングに依存します。ステミング(語幹抽出)や句読点の削除を行うことで、「Fixing」「Fixes」「Fix」がすべて同じヒットとしてカウントされるようにします。
  • アルファ値を賢く設定する: ドキュメントの70%が技術用語である場合は、アルファ値0.3から始めてください。会話形式のWikiなどの場合は、0.7を試してください。
  • 50のクエリでベンチマーク: 推測に頼らないでください。よくある50の質問に対して、上位3位(Top-3)の結果を手動で確認します。このわずかな投資が、後の膨大なデバッグ時間を防ぐことにつながります。

ハイブリッド検索は単なる技術的なアップグレードではなく、信頼性の向上を目的とした修正です。セマンティック検索に死角があることを認めることで、単に人間のような会話を模倣するだけでなく、ユーザーが正確性を求めて真に信頼できるRAGパイプラインを構築し、本番環境でのAIの失敗を防ぐことにつながります。

Share: