5. LangChainによるBM25の実装
LangChainにおけるBM25Retriever
LangChain には、BM25 アルゴリズムによる検索を簡単に利用できる BM25Retriever(langchain_community.retrievers)が用意されています。
このコンポーネントは、内部で軽量な Python ライブラリである rank_bm25 を使用しており、外部サーバーを立ち上げることなくローカル環境のメモリ上で高速にキーワード検索を行えます。
標準の BM25Retriever は、英語テキストを前提としているため、単語を空白(スペース)で区切る仕様になっています。
日本語テキストをそのまま渡すと、文章全体が1つの長い単語として扱われてしまい、正しく検索できません。
そのため、形態素解析ライブラリ(Janome や MeCab など)を使って「日本語トークナイズ関数」を自作し、preprocess_func 引数に渡すことが必須となります。
必要なパッケージのインストール
まずは、必要なライブラリをインストールします。
ここでは、追加の C++ コンパイラや外部バイナリのセットアップが不要で、pip だけで手軽に動かせる日本語形態素解析器「Janome」を使用します。
pip install langchain langchain-community rank-bm25 janome
日本語対応BM25Retrieverの実装コード
日本語の文章群に対して BM25 でキーワード検索を行う最小限の実装コードは、次の通りです。
from janome.tokenizer import Tokenizer
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
# 1. Janome形態素解析器の初期化
tokenizer = Tokenizer()
def japanese_tokenizer(text: str) -> list[str]:
"""日本語テキストを形態素解析して単語リストに分割する前処理関数."""
# 名詞、動詞、形容詞などを抽出(記号や助詞は除外することも可能)
tokens = []
for token in tokenizer.tokenize(text):
pos = token.part_of_speech.split(",")[0]
# 不要な助詞や記号を省き、自立語を中心にトークン化
if pos in ["名詞", "動詞", "形容詞"]:
tokens.append(token.base_form)
return tokens
# 2. 検索対象ドキュメントの準備
documents = [
Document(
page_content="有給休暇の申請は、取得希望日の3日前までに勤怠管理システムから提出してください。",
metadata={"source": "勤怠規程", "doc_id": "HR-001"},
),
Document(
page_content="深夜勤務手当は、午後10時から午前5時までの労働に対して25%増しで支給されます。",
metadata={"source": "就業規則", "doc_id": "HR-002"},
),
Document(
page_content="社内ネットワークに接続する端末は、ウイルス対策ソフトの常時有効化が義務付けられています。",
metadata={"source": "セキュリティガイド", "doc_id": "SEC-001"},
),
Document(
page_content="経費精算システムのエラーコード ERR-502 は、通信タイムアウトを意味します。",
metadata={"source": "FAQ", "doc_id": "IT-101"},
),
]
# 3. BM25Retrieverの構築(日本語トークナイザーを指定)
bm25_retriever = BM25Retriever.from_documents(
documents=documents,
preprocess_func=japanese_tokenizer,
k=2, # 上位2件を取得
)
# 4. 検索の実行
query = "エラーコード ERR-502 の意味を知りたい"
results = bm25_retriever.invoke(query)
# 5. 検索結果の表示
print(f"検索クエリ: {query}\n")
for i, doc in enumerate(results, start=1):
print(f"[{i}] スコア上位文書 (ID: {doc.metadata['doc_id']}):")
print(f" 内容: {doc.page_content}")
print(f" 出典: {doc.metadata['source']}")
コードのポイント解説
▸ 日本語トークナイズ関数 japanese_tokenizer
preprocess_func に渡す関数は、「テキスト文字列を受け取り、単語の文字列リストを返す」インターフェースを満たす必要があります。
Janome の token.base_form(基本形)を取得することで、「申請した」「申請する」といった活用の違いを同じ単語として統一できます。
▸ k パラメータの指定
k は検索結果として取得する文書の件数です。
デフォルトは 4 ですが、bm25_retriever.k = 3 のように後から変更することも可能です。
▸ 固有名詞やコードに対する強さ
上記のサンプルコードでは、「ERR-502」という記号混じりのエラーコードが含まれています。
ベクトル検索では見落とされがちなこのような完全一致キーワードも、BM25 であれば確実に該当の FAQ ドキュメント(IT-101)を最上位にヒットさせることができます。
次章では、この BM25Retriever とベクトル検索を融合させた「ハイブリッド検索」を実装します。