---
title: "LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索"
url: "https://www.issoh.co.jp/tech/details/3751/"
published: 2024-10-04
updated: 2026-09-27
categories: ["LLM・RAG"]
publisher: "株式会社一創"
---

# LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索

BM25Retrieverは、LangChainでキーワード一致に基づく文書検索を担うRetrieverです。埋め込みベクトルを作らずに動くため、型番・エラーコード・専門用語のように「その語がそのまま含まれる文書」を探す用途に強く、RAGの検索段でベクトル検索と併用されます。この記事では、正しいimportと最小実装から、k1・bのチューニング、日本語文書のための`preprocess_func`、EnsembleRetrieverによるハイブリッド検索までを実装コード付きで解説します。旧来よく混同される「`rank_bm25`を直接使うコード」ではなく、LangChainの`BM25Retriever`そのものの使い方に絞ります。

## まとめ：BM25Retriever実装の要点

- importは `from langchain_community.retrievers import BM25Retriever`。内部で `rank_bm25` を使うため `pip install rank_bm25` が必須。
- コーパスは `from_texts`（文字列）か `from_documents`（Documentオブジェクト）で渡し、検索は `.invoke(query)` で実行する。
- スコアの挙動は `bm25_params` の `k1`・`b` で調整する。
- 日本語は既定の空白分割では機能しないため、形態素解析した `preprocess_func` を渡す。
- 意味の言い換えに弱いので、ベクトル検索と `EnsembleRetriever` で組み合わせると精度が上がる。

以下、それぞれを実装コードとともに順に見ていきます。

## BM25Retrieverとは｜キーワード検索を担うLangChainのRetriever

BM25Retrieverは、TF-IDFを発展させたランキング関数BM25で、クエリと各文書の一致度をスコア化して上位を返すRetrieverです。ベクトル検索が埋め込みモデルで「意味の近さ」を測るのに対し、BM25は「語がどれだけ含まれるか」を測ります。埋め込みAPIの呼び出しもGPUも不要で、少量のコーパスならその場でインデックスを構築できる手軽さが利点です。

### BM25のスコアリングとTF-IDFとの違い（k1・b）

BM25がTF-IDFと違うのは、単語頻度の「飽和」と文書長の「補正」を持つ点です。同じ語が何度出ても頻度スコアは`k1`で頭打ちになり、長い文書が有利になりすぎないよう`b`で正規化します。`rank_bm25`のBM25Okapi既定値は`k1=1.5`、`b=0.75`で、この2値がBM25Retrieverの検索結果を左右する主要パラメータです。

### ベクトル検索ではなくBM25Retrieverを選ぶ場面

BM25Retrieverが向くのは、ユーザーが正確な語で検索する場面です。製品コード「`ABC-1200`」、エラー文言「`Connection refused`」、条文番号や社内固有名詞などは、意味が近いだけの文書より完全一致の文書が正解になります。逆に「解約したい」を「退会」と言い換えるような表記ゆれ・同義語には弱く、ここだけをBM25で解こうとすると取りこぼします。この弱点を埋めるのが後述のハイブリッド検索です。

## BM25Retrieverの導入と最小実装（インストール〜invoke）

BM25RetrieverはLangChainのcommunityパッケージに含まれ、実体の計算は`rank_bm25`が担います。まず両方をインストールします。

```
pip install langchain-community rank_bm25
```

### 正しいimportとfrom\_textsでの最小検索

importは`langchain_community.retrievers`から行います。文字列のリストをそのまま渡すなら`from_texts`が最短です。返す件数は`k`（既定4）で指定します。

```
from langchain_community.retrievers import BM25Retriever

texts = [
    "BM25はキーワードの一致度で文書をスコアリングする",
    "ベクトル検索は意味の近さで文書を探す",
    "ハイブリッド検索は両者を組み合わせる",
]

retriever = BM25Retriever.from_texts(texts, k=2)
docs = retriever.invoke("キーワード 一致")
for d in docs:
    print(d.page_content)
```

クエリ実行は`.invoke()`を使います。以前の`.get_relevant_documents()`は非推奨で、新しいコードでは`.invoke()`に統一します。

### from\_documentsによるメタデータ付き文書の検索

出典やIDを結果に残したいときは、`Document`にメタデータを持たせて`from_documents`で渡します。RAGで「どのFAQから引いたか」を追跡する用途で使います。

```
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document

docs = [
    Document(page_content="請求書の再発行はマイページから", metadata={"source": "faq-01"}),
    Document(page_content="パスワードの再設定手順", metadata={"source": "faq-02"}),
]

retriever = BM25Retriever.from_documents(docs, k=3)
results = retriever.invoke("請求書 再発行")
print(results[0].metadata)
```

## k1・bとbm25\_paramsによる検索精度の調整

`k1`と`b`は、`from_texts`／`from_documents`の`bm25_params`にまとめて渡します。

```
retriever = BM25Retriever.from_texts(
    texts,
    bm25_params={"k1": 1.2, "b": 0.75},
    k=3,
)
```

調整の指針は明確です。`k1`を上げると同じ語が繰り返し出る文書ほど強く優遇され、下げると1回出れば十分という評価に近づきます。`b`は1.0に近づけるほど長い文書へのペナルティが強くなり、0に近づけると文書長を無視します。短いFAQやチャットログのように長さが揃ったコーパスでは`b`を小さめ、記事とスニペットが混在するコーパスでは既定の0.75付近から始めて、実クエリで上位の妥当性を見ながら詰めるのが実務的です。

## BM25Retrieverでの日本語文書検索（preprocess\_func）

BM25Retrieverの既定の前処理は「空白で区切る」だけです。単語間に空白のない日本語では文全体が1トークンになり、ほぼ検索が効きません。日本語では形態素解析した結果を返す`preprocess_func`（`Callable[[str], List[str]]`）を渡します。ここではJanomeを使います。

```
from janome.tokenizer import Tokenizer
from langchain_community.retrievers import BM25Retriever

tokenizer = Tokenizer()

def preprocess_func(text):
    return [token.surface for token in tokenizer.tokenize(text)]

texts = ["請求書の再発行はマイページから", "パスワードの再設定手順"]
retriever = BM25Retriever.from_texts(texts, preprocess_func=preprocess_func, k=2)
docs = retriever.invoke("請求書 再発行")
```

MeCabやSudachiでも同様に、トークンの表層形（`surface`）のリストを返せば置き換えられます。精度を上げるなら、助詞・記号を落とす、表記を正規化する（全角半角・カタカナ長音の統一）といった処理を`preprocess_func`内に足します。なお、この関数は後述の永続化でpickle化されうるため、`lambda`ではなくモジュール直下の`def`で定義しておくのが安全です。

## ベクトル検索と組み合わせるハイブリッド検索（EnsembleRetriever）

キーワード一致のBM25と、意味の近さで探すベクトル検索は得意分野が逆で、両方を走らせて結果を統合すると取りこぼしが減ります。LangChainでは`EnsembleRetriever`が複数のRetrieverの結果を[Reciprocal Rank Fusion（RRF）](/tech/details/8015/)でまとめます。RRFは各Retrieverでの順位の逆数を足し合わせて再ランキングする方式で、スコアの尺度が違うBM25とベクトル検索を公平に混ぜられます。

```
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

bm25 = BM25Retriever.from_texts(texts, preprocess_func=preprocess_func, k=4)
vector = FAISS.from_texts(texts, OpenAIEmbeddings()).as_retriever(search_kwargs={"k": 4})

ensemble = EnsembleRetriever(retrievers=[bm25, vector], weights=[0.5, 0.5])
docs = ensemble.invoke("解約したい")
```

`weights`は各Retrieverの寄与度で、合計は自由です。ユーザーが型番やエラーメッセージで検索しがちなら`[0.6, 0.4]`とBM25側を厚く、自然文の質問が中心なら`[0.4, 0.6]`とベクトル側を厚くします。まずは`[0.5, 0.5]`から始め、実クエリで外した検索を見て寄せるのが定石です。意図ごとに検索戦略を切り替える上位設計は[OmniRAGの使い分けパターン](/tech/details/5363/)が参考になります。なお、ベクトル側の埋め込みは`OpenAIEmbeddings`に限らず任意（ローカルなら`HuggingFaceEmbeddings`等）に置き換えられます。

## BM25Retrieverの運用上の落とし穴（永続化・API・import）

実装で事故になりやすいのは、性能よりも運用面です。

- **インメモリで永続化されない**：BM25Retrieverはコーパスとインデックスをメモリ上に保持し、ディスクへ自動保存しません。プロセスを再起動するたびに`from_texts`で作り直すか、Retrieverを`pickle`で保存・復元します。大規模コーパスを毎回構築するとメモリと初期化時間が増えるため、コーパス更新頻度に応じて再構築か永続化かを決めます。
- **get\_relevant\_documentsは非推奨**：古い記事のコードは`.get_relevant_documents()`を使いますが、現行は`.invoke()`です。移行時に置き換えます。
- **importパスの変更に注意**：BM25Retrieverは`langchain_community`、EnsembleRetrieverは`langchain`に分かれています。バージョン間でパッケージ構成が動いた経緯は[LangChain v1.0の変更点](/tech/details/9663/)を確認すると把握しやすいです。引数の最終仕様は[BM25Retrieverの公式APIリファレンス](https://reference.langchain.com/python/langchain-community/retrievers/bm25/BM25Retriever)で確認できます。
- **preprocess\_funcはmodule直下で定義**：pickle化する場合、`lambda`やローカル関数は保存できません。

## よくある質問

**Q. BM25RetrieverとTFIDFRetrieverはどちらを使うべきですか？**  
文書長の補正と頻度飽和を持つBM25の方が実務での精度が安定しやすく、基本はBM25Retrieverで問題ありません。どちらもLangChainのcommunityに含まれ、使い方はほぼ同じです。

**Q. from\_textsとfrom\_documentsの違いは何ですか？**  
渡すデータ型の違いです。文字列のリストなら`from_texts`、メタデータ付きの`Document`を扱うなら`from_documents`を使います。出典やIDを結果に残したいなら後者です。

**Q. 日本語で検索結果が出ないのはなぜですか？**  
既定の前処理が空白分割のためです。日本語は形態素解析した`preprocess_func`（Janome・MeCab・Sudachi等）を渡さないと、文全体が1トークンになり一致しません。

**Q. get\_relevant\_documentsは使えなくなりましたか？**  
非推奨です。動く場合もありますが、新しいコードは`.invoke(query)`に統一してください。

**Q. BM25Retrieverはディスクに保存できますか？**  
自動保存はされません。プロセスごとに`from_texts`で再構築するか、Retrieverを`pickle`で保存・復元します。

## 関連記事

- [RRF（Reciprocal Rank Fusion）とは？RAG-Fusionでの仕組み・スコア計算・実装を解説](/tech/details/8015/)
- [OmniRAGとは？意図に応じてDB・ベクトル検索・ナレッジグラフを使い分けるRAG設計パターン](/tech/details/5363/)
- [PageIndexとは？ベクトル不要の推論型RAGの仕組み・使い方・従来RAGとの違い](/tech/details/8760/)
- [LangChain v1.0とは｜create\_agent・ミドルウェア・langchain-classicを実装例で解説](/tech/details/9663/)
- [LangChainとLangGraphの違い｜v1.0で逆転した関係と使い分け](/tech/details/4059/)

---

出典: [LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索](<https://www.issoh.co.jp/tech/details/3751/>)（株式会社一創）
