スキップしてメイン コンテンツに移動

【温故知新】RAGに「キーワード検索(BM25)」を足したら、AIが劇的に賢くなった話

【温故知新】RAGに「キーワード検索(BM25)」を足したら、AIが劇的に賢くなった話

はじめに:「型番」が検索できないAIなんて役立たずだ

「この製品コード『A-1234』の在庫教えて」とAIに聞いて、「すみません、分かりません」と返されたことありませんか? 文脈は合っているのに、固有の記号に弱い。これが最新のAI(ベクトル検索)の弱点です。
現場の実務では、ふわっとした意味検索よりも、型番やエラーコードの「完全一致」検索の方が重要だったりします。「流行りの技術(Vector)さえ入れればOK」と思っていると、現場から総スカンを食らいます。

基礎知識:ベクトル検索が苦手なこと、キーワード検索が得意なこと

ベクトル検索は「意味の近さ」を計算します。「美味しい」と「美味」は近くになりますが、「A-1」と「A-2」は(文字は似ていても)全く別物として扱うのが苦手です。

一方、昔ながらのキーワード検索(BM25など)は、単語の出現頻度を見るので、固有名詞や専門用語にめっぽう強い。この両者を組み合わせるのが、最強のソリューション ハイブリッド検索 です。

実装・設定:EnsembleRetrieverで「いいとこ取り」

LangChainの EnsembleRetriever を使えば、2つの検索器を簡単に合体できます。

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

# 1. キーワード検索器
bm25_retriever = BM25Retriever.from_documents(docs)

# 2. ベクトル検索器
vector_retriever = vectorstore.as_retriever()

# 3. 合体!(ハイブリッド)
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5] # 半々の割合で評価
)

これで、「型番」でビシッと当てつつ、「文脈」でもフォローするという隙のない検索システムが完成します。

応用テクニック:重み付け(Weight)の黄金比

weights=[0.5, 0.5] が基本ですが、ドメインによって調整が必要です。
マニュアル検索のような「用語」重視ならBM25を強め(0.7)に、FAQのような「悩み相談」系ならベクトルを強め(0.7)にすると、肌感覚に合う結果になります。

トラブルシューティング:日本語の壁(分かち書き)

ここで一つ、落とし穴があります。LangChainの BM25Retriever はデフォルトでスペース区切りを前提としています。つまり、日本語をそのまま突っ込んでも検索できません。
「あれ?全くヒットしないぞ?」と焦る前に、必ず形態素解析(分かち書き)を行う前処理関数を挟みましょう。これを忘れると、BM25はただの無能になります。

# Janomeを使った簡易トークナイザーの例
from janome.tokenizer import Tokenizer

def japanese_tokenizer(text):
    t = Tokenizer()
    return [token.surface for token in t.tokenize(text)]

# 前処理関数として渡す
bm25_retriever = BM25Retriever.from_documents(
    docs, 
    preprocess_func=japanese_tokenizer
)

これだけで、劇的にヒット率が変わります。「ライブラリ入れたのに動かない」系トラブルの8割は、こうした言語依存の仕様理解不足が原因です。

まとめ:枯れた技術を見捨てるな

最新の論文技術ばかり追いかけて、足元の「確実性」をおろそかにしてはいけません。
BM25のような「枯れた技術(成熟した技術)」には、長年使われてきただけの理由があります。新旧技術を組み合わせて最適解を出すのが、経験豊富なエンジニア(=おじさん)の役目ですよ。

この記事はAI技術を活用して作成されましたが、内容は慎重に確認されています。

コメント