---
title: "BM25とは？読み方・計算式とk1・bの意味、Python実装（rank_bm25・bm25s）の落とし穴"
url: "https://www.issoh.co.jp/tech/details/4388/"
published: 2024-12-03
updated: 2026-10-20
categories: ["LLM・RAG"]
publisher: "株式会社一創"
---

# BM25とは？読み方・計算式とk1・bの意味、Python実装（rank\_bm25・bm25s）の落とし穴

BM25（Okapi BM25）は、検索クエリに含まれる単語が文書にどれだけ、どんな偏り方で出てくるかから関連度スコアを計算するランキング関数です。ElasticsearchやOpenSearchの既定のスコアリングであり、RAGのキーワード検索にも使われます。式そのものは短いのですが、実装によってIDFの計算が違い、Pythonの定番ライブラリでは条件次第でスコアが全件0になります。本記事では計算式を構成要素ごとに分解し、実装差とPythonでの落とし穴を実行結果付きで整理します。

## まとめ

- **BM25は「ビーエム・ニジュウゴ」と読み、BMはBest Match、25はソフトウェア内の関数番号**。ロンドンのCity大学で開発されたOkapi検索システムで最初に使われ、1994年のTREC-3の論文で広く知られた。
- **スコアは「IDF × 文書長で補正した単語頻度の飽和項」の総和**。k1は単語頻度の効き方の上限、bは長い文書への減点の強さを決める。
- **IDFの式とスコアの倍率は実装ごとに違う**。Lucene／Elasticsearchは常に正になる式、rank\_bm25は負になりうる元の式に下限を付けた式を使う。Elasticsearch 9.5.5は(k1+1)を掛けるため、後述のk1=1.2にそろえた5文書の計算例では、bm25s（lucene）の2.2倍の値になる。
- **rank\_bm25のBM25Okapiは、半数の文書に出る語のIDFが0になる**。文書数が少ないとスコアが全件0になりやすい。bm25sの既定（Lucene式）ではこの問題が起きない。
- **日本語は形態素解析で分かち書きしてから渡す**。空白や区切り記号のない日本語では、空白分割やbm25sの既定トークナイザで長い文字列が1トークンになり、短い検索語と一致しにくい。

## BM25の意味と読み方、名前の由来

BM25は「ビーエム・ニジュウゴ」と読みます。City大学の研究成果報告では「BMはBest Match、25はソフトウェア内の関数番号」と説明されています（Best Matchingと表記されることもあります）。Okapi BM25とも呼ばれます。OkapiはBM25が最初に実装された検索システムです。その前身は1982〜1988年にPolytechnic of Central Londonで開発され、City大学のTREC用システムへと発展しました。背景にあるのは、Stephen RobertsonやKaren Spärck Jonesらが1970〜80年代に築いた確率的な検索モデルです。BM25は番号付きの重み付け関数の系列の1つで、後述のbを1にするとBM11、0にするとBM15と呼ばれる関数になります。

BM25の名前が広く知られたのは1994年の評価会議TREC-3で、論文は Robertson, Walker, Jones, Hancock-Beaulieu, Gatford「Okapi at TREC-3」です。理論的な背景（確率的関連性モデル）をまとめた解説としては、Robertson と Zaragoza の「The Probabilistic Relevance Framework: BM25 and Beyond」（2009年）がよく参照されます。

## BM25の計算式と3つの構成要素

クエリQ（単語q1…qn）に対する文書Dのスコアは、クエリの単語ごとに計算した値の合計です。

```
score(D, Q) = Σ IDF(qi) * ( f(qi, D) * (k1 + 1) ) / ( f(qi, D) + k1 * (1 - b + b * |D| / avgdl) )

f(qi, D) : 文書D内での単語qiの出現回数
|D|      : 文書Dの長さ（単語数）
avgdl    : 全文書の平均の長さ
k1, b    : 調整パラメータ（Luceneの既定は k1=1.2, b=0.75）
```

IDFは単語ごとの重みで、残りの分数が単語頻度の飽和項です。文書長の補正（b）は、この飽和項の分母に組み込まれています。

### IDF：単語の希少性による重み付け

全文書数をN、単語qiを含む文書数をn(qi)とすると、元の論文系のIDFは次の式です。

```
IDF(qi) = ln( (N - n(qi) + 0.5) / (n(qi) + 0.5) )
```

1,000文書のうち1文書にしか出ない語は6.50、100文書に出る語は2.19と、珍しいほど重くなります。問題は、半数以上の文書に出る語です。500文書に出る語はちょうど0、900文書に出る語は-2.19と負になります。「その語を含む文書ほど順位が下がる」という直感に反する結果になるため、実装側で補正を入れています（後述の実装差の章）。

### k1：単語頻度の飽和の調整

TFに生の出現回数を使い、ベクトル正規化を行わないTF-IDFでは、単語の重みは出現回数に比例します。BM25では単語頻度の寄与が飽和します。k1=1.2、b=0.75、平均的な長さの文書で、単語頻度の部分がどう伸びるかを計算すると次のとおりです。

| 出現回数   | 1回    | 2回    | 3回    | 5回    | 10回   | 20回   |
| ------ | ----- | ----- | ----- | ----- | ----- | ----- |
| 単語頻度の値 | 1.000 | 1.375 | 1.571 | 1.774 | 1.964 | 2.075 |

20回出ても1回の約2倍で、上限はk1+1（ここでは2.2）です。k1を大きくすると上限が上がり、出現回数の差が順位に効くようになります。k1=0にすると回数を無視し、「含むか含まないか」だけの判定になります。同じ語を何度も繰り返してスコアを稼ぐ文書が上位に来にくいのは、この飽和のためです。

### b：長い文書を減点する強さ

長い文書はそれだけで多くの単語を含むため、何もしないと有利になります。bは、文書の長さを平均と比べてどれだけ補正するかを0〜1で決めます。同じ3回の出現でも、k1=1.2、b=0.75のとき、平均の半分の長さの文書では1.760、平均の2倍の長さの文書では1.294と差がつきます。b=0にすると長さを一切見なくなり、b=1で平均との比にそのまま比例して補正します。

既定のb=0.75は汎用の値です。文書長がほぼ揃っているデータでは、bを変えても差は小さくなります。長さがばらつく場合は、長さの理由で考えます。同じ内容を冗長に書いているから長いならbを高めに、扱う話題が多く内容が詳しいから長いならbを低めにします。Robertson と Zaragoza の解説が冗長性仮説・範囲仮説と呼んでいる区別です。どちらの場合も、評価用のクエリと正解文書を用意し、nDCGやRecallで比べてから決めます。

## BM25スコアの計算例：5文書の手計算と実装比較

式だけでは数字の動きがつかみにくいので、空白で区切った5つの短い文書を「bm25 search」で検索した場合のスコアを、Luceneの式（k1=1.2、b=0.75）で最後まで計算します。

| 文書 | 本文                                    | 長さ | bm25の回数 | searchの回数 |
| -- | ------------------------------------- | -- | ------- | --------- |
| 1  | bm25 is a ranking function for search | 7  | 1       | 1         |
| 2  | bm25 bm25 bm25 score                  | 4  | 3       | 0         |
| 3  | vector search with embeddings         | 4  | 0       | 1         |
| 4  | inverted index stores terms           | 4  | 0       | 0         |
| 5  | tokenizer splits text into terms      | 5  | 0       | 0         |

平均の長さavgdlは (7+4+4+4+5)÷5 = 4.8 です。「bm25」も「search」も5文書中2文書に出るので、IDFはどちらも ln(1 + (5-2+0.5)÷(2+0.5)) = ln(2.4) = 0.8755 になります。残りは文書ごとの単語頻度の項です。

```
文書1の「bm25」  : 1 / (1 + 1.2 × (1 - 0.75 + 0.75 × 7 / 4.8)) = 1 / 2.6125 = 0.3828
文書1のスコア    : 0.8755 × 0.3828（bm25）+ 0.8755 × 0.3828（search）= 0.6702
文書2の「bm25」  : 3 / (3 + 1.2 × (1 - 0.75 + 0.75 × 4 / 4.8)) = 3 / 4.05 = 0.7407
文書2のスコア    : 0.8755 × 0.7407 = 0.6485
文書3の「search」: 1 / (1 + 1.05) = 0.4878 → スコア 0.8755 × 0.4878 = 0.4271
```

ここでは分子の(k1+1)を省いています。現在のLuceneと同じ書き方で、全文書に同じ係数が掛かるだけなので順位は変わりません。文書2は「bm25」を3回含むのに、検索語を2つとも含む文書1を抜けません。3回出ても単語頻度の項は文書1の1回分の約1.9倍にとどまり、「search」の点がないからです。同じ計算をライブラリと検索エンジンで実行すると、次の値が返ります。

| 文書  | 手計算    | bm25s（lucene） | Elasticsearch 9.5.5 | rank\_bm25 BM25Okapi |
| --- | ------ | ------------- | ------------------- | -------------------- |
| 1   | 0.6702 | 0.6702        | 1.4745              | 0.5667               |
| 2   | 0.6485 | 0.6485        | 1.4267              | 0.5483               |
| 3   | 0.4271 | 0.4271        | 0.9395              | 0.3611               |
| 4・5 | 0      | 0             | ヒットなし               | 0                    |

3つともk1=1.2、b=0.75にそろえています。bm25sは手計算と一致し、Elasticsearchはちょうど2.2倍（k1+1倍）です。rank\_bm25はIDFが ln(3.5÷2.5) = 0.3365 と小さい一方で(k1+1)を掛けるため、また別の値になります。この例では3つとも順位が同じですが、スコアの絶対値は実装をまたいで比べられません。差が生まれる理由は次の章で説明します。仮想環境で `python -m pip install rank-bm25==0.2.2 bm25s==0.3.13 Janome==0.5.0` を実行すると、後述の日本語例まで含めて必要なライブラリがそろいます。検証に使ったコードは次のとおりです。

```
import math
import bm25s
from rank_bm25 import BM25Okapi

docs = [
    "bm25 is a ranking function for search",
    "bm25 bm25 bm25 score",
    "vector search with embeddings",
    "inverted index stores terms",
    "tokenizer splits text into terms",
]
tok = [d.split() for d in docs]
query = ["bm25", "search"]
N, avgdl, k1, b = len(tok), sum(map(len, tok)) / len(tok), 1.2, 0.75

def hand(doc):
    s = 0.0
    for q in query:
        n = sum(q in d for d in tok)
        idf = math.log(1 + (N - n + 0.5) / (n + 0.5))
        f = doc.count(q)
        s += idf * f / (f + k1 * (1 - b + b * len(doc) / avgdl))
    return s

print([round(hand(d), 4) for d in tok])
r = bm25s.BM25(method="lucene", k1=k1, b=b)
r.index(tok, show_progress=False)
res, sc = r.retrieve([query], k=5, show_progress=False)
print(sorted(zip(res[0].tolist(), [round(float(x), 4) for x in sc[0]])))
print(BM25Okapi(tok, k1=k1, b=b).get_scores(query).round(4))
```

```
[0.6702, 0.6485, 0.4271, 0.0, 0.0]
[(0, 0.6702), (1, 0.6485), (2, 0.4271), (3, 0.0), (4, 0.0)]
[0.5667 0.5483 0.3611 0.     0.    ]
```

## BM25とTF-IDFの違い

BM25はTF-IDFの発展形と説明されることが多いですが、違いは次の2点に集約されます。

| 観点       | TF-IDF            | BM25            |
| -------- | ----------------- | --------------- |
| 出現回数の効き方 | TFの定義・正規化に依存      | k1+1で頭打ち        |
| 文書の長さ    | ベクトル正規化で間接的に扱う    | bで平均長との比を明示的に補正 |
| 主な用途     | 特徴量化・類似度計算        | 検索の順位付け         |
| 調整パラメータ  | TF変換・IDF平滑化・正規化など | k1とb            |

TF-IDFは文書をベクトルに変換してコサイン類似度で比べる用途が中心で、BM25は「クエリに対して文書を並べる」ための関数です。TF-IDFの重み付けそのものは[TF-IDFとは？単語の重み付けの仕組みとコサイン類似度・ベクトル検索での使い方を実装目線で解説](/tech/details/15207/)で扱っています。

## 実装ごとに違うIDFとパラメータの既定値

「BM25」と名乗っていても、IDFの式とk1の既定値は実装で違います。同じコーパスでもスコアの絶対値は一致せず、ときに順位も入れ替わります。主な実装を並べます。

| 実装                          | IDFの式                     | 負のIDF         | (k1+1) | 既定のk1 / b      |
| --------------------------- | ------------------------- | ------------- | ------ | -------------- |
| Lucene 8以降・OpenSearch 3.0以降 | ln(1 + (N-n+0.5)/(n+0.5)) | 起きない          | 掛けない   | 1.2 / 0.75     |
| Elasticsearch（9.5.5で確認）     | Luceneと同じ                 | 起きない          | 掛ける    | 1.2 / 0.75     |
| rank\_bm25 BM25Okapi        | ln((N-n+0.5)/(n+0.5))     | 平均IDF×0.25に置換 | 掛ける    | 1.5 / 0.75     |
| bm25s（既定 `method="lucene"`） | Luceneと同じ                 | 起きない          | 掛けない   | 1.5 / 0.75     |
| SQLite FTS5 bm25()          | ln((N-n+0.5)/(n+0.5))     | 0以下を1e-6に置換   | 掛ける    | 1.2 / 0.75（固定） |

Luceneは式に1を足して、どんな語でもIDFが正になるようにしています。1,000文書中900文書に出る語でも0.106と小さな正の値です。また、Lucene 8では分子の(k1+1)を削っています（[Lucene 8.0.0の変更履歴](https://lucene.apache.org/core/8%5F0%5F0/changes/Changes.html)のLUCENE-8563）。全文書に同じ係数が掛かるだけなので順位は変わりません。

ここで、ElasticsearchとOpenSearchの扱いが分かれます。Elasticsearchは旧来のスコアの大きさを保つため、9.5.5でも既定のBM25に [LegacyBM25Similarity](https://github.com/elastic/elasticsearch/blob/v9.5.5/server/src/main/java/org/elasticsearch/index/similarity/SimilarityService.java) を使い、(k1+1)を掛け直しています。OpenSearchは3.0で既定をLuceneの `BM25Similarity` に切り替えました（[PR #17306](https://github.com/opensearch-project/OpenSearch/pull/17306)）。前章の例でElasticsearchだけ2.2倍だったのはこのためです。前処理・統計・クエリなどの採点条件をそろえたうえでOpenSearch 3系の既定のBM25Similarityを使うと、スコアはElasticsearchの1÷(k1+1)になり、k1=1.2なら1÷2.2です。ElasticsearchからOpenSearchへ移行するときや、両者のスコアを閾値で比べている仕組みでは、この倍率を考慮しておく必要があります。Elasticsearchは5.0から既定の類似度がBM25で、インデックスの `similarity` 設定でk1とbを変えられます。基本構成は[Elasticsearchとは？転置インデックスとシャード設計・ライセンス系列で決める採用可否](/tech/details/17011/)を参照してください。

SQLite FTS5の `bm25()` は、良い一致ほど小さい（負の大きい）値を返します。`ORDER BY bm25(表名)` の昇順がそのまま関連度順になるための仕様です。IDFが0以下になる語は1e-6に置き換えるので、後述するrank\_bm25のような全件0は起きません。使い方は[FTS5とは？SQLite全文検索の使い方と日本語対応・bm25](/tech/details/9314/)にまとめています。

### Elasticsearchの\_explainによるスコア内訳の確認

Elasticsearchのスコアが想定と合わないときは、[Explain API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-explain) で1文書分の計算過程を出せます。前章の5文書を、空白で区切るだけの `whitespace` アナライザでElasticsearch 9.5.5（Lucene 10.5.1）のシャード1つの索引に登録し、文書1を「bm25」で調べた結果が次です。

```
GET /bm25demo/_explain/1
{ "query": { "match": { "text": "bm25" } } }
```

```
0.73724 weight(text:bm25 in 0) [PerFieldSimilarity], result of:
  0.73724 score(freq=1.0), computed as boost * idf * tf from:
    2.2 boost
    0.87547 idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:
      2 n, number of documents containing term
      5 N, total number of documents with field
    0.38278 tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:
      1.0 freq, occurrences of term within document
      1.2 k1, term saturation parameter
      0.75 b, length normalization parameter
      7.0 dl, length of field
      4.8 avgdl, average length of field
```

idfの0.87547とtfの0.38278は手計算の値と同じで、boostの2.2が(k1+1)です。2.2 × 0.87547 × 0.38278 = 0.73724 になります。読むときの注意は2つあります。

- **Nとavgdlはシャード単位で数える**：既定の検索では、文書数や平均長をシャード単位で数えます。文書の少ない索引を複数シャードに分けると、同じ文書でもシャードによってスコアが変わります。[Elasticsearchの検索API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-search)で `search_type=dfs_query_then_fetch` を付けると、全シャードの統計で採点されます。
- **長い文書のdlは近似値**：Luceneは文書長を1バイトに丸めて保存します。符号化した値が39を超えると、explainに「dl, length of field (approximate)」と表示されます（[BM25Similarityのソース](https://github.com/apache/lucene/blob/main/lucene/core/src/java/org/apache/lucene/search/similarities/BM25Similarity.java)）。長い文書では、丸め前の文書長を使った手計算とスコアがずれます。差の大きさは文書長やパラメータに依存します。

## PythonでBM25を使う：rank\_bm25とbm25s

PythonでBM25を手軽に試せるライブラリとして、本記事では rank\_bm25（PyPIのパッケージ名は `rank-bm25`）と bm25s の2つを比較します。scikit-learnにはBM25のクラスがありません（`TfidfVectorizer` と `TfidfTransformer` まで）。以下のコードはPython 3.9.6、rank-bm25 0.2.2、bm25s 0.3.13、Janome 0.5.0で実行した出力です。

### rank\_bm25の基本：BM25Okapiとget\_top\_n

rank\_bm25は、単語に分割済みの文書リストを受け取ります。分割はライブラリがしてくれないので、呼び出し側で行います。

```
from rank_bm25 import BM25Okapi

corpus = [
    "Hello there good man!",
    "It is quite windy in London",
    "How is the weather today?",
]
tokenized_corpus = [doc.split(" ") for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)  # 既定: k1=1.5, b=0.75, epsilon=0.25

query = "windy London".split(" ")
print(bm25.get_scores(query))
print(bm25.get_top_n(query, corpus, n=1))
```

```
[0.         0.93729472 0.        ]
['It is quite windy in London']
```

`get_scores` は全文書のスコアを配列で返し、`get_top_n` は上位n件の元文書を返します。ただし、検索語が一致せず全件0点でも文書を返すため、一致の有無はスコアなどで別途確認します。BM25Okapiのほかに、長い文書を減点しすぎないよう補正した `BM25L` と `BM25Plus` も同じ使い方で呼べます。2026年10月15日時点で、PyPIの最新リリースは2022年2月16日公開の0.2.2のままです。

### bm25sの事前計算と検索速度

bm25sは、スコアを索引作成時に疎行列へ事前計算しておく実装です。2026年10月7日に0.3.13が出ており、現在も更新が続いています。Intel Core i9-9880H上で、乱数シード0、語彙5,000語、各20〜120語の2万文書、各3語の100クエリを使い、rank\_bm25は逐次採点、bm25sは上位10件を一括取得する条件で6回測定しました。rank\_bm25の `get_scores` が4.4〜4.9秒、bm25s 0.3.13の `retrieve` が0.13〜0.18秒で、検索はおよそ30倍速い結果です。その代わり索引作成はbm25sのほうが遅く、rank\_bm25の1.5〜2.5秒に対して3.0〜4.0秒かかりました。rank\_bm25はクエリのたびに全文書を走査します。今回の2万文書の条件で検索待ち時間を優先するならbm25sを選びますが、実データでは索引作成時間とメモリ使用量も確認します。

bm25sの `method` は `"lucene"`（既定）、`"robertson"`、`"atire"`、`"bm25l"`、`"bm25+"` から選べます。既定の `"lucene"` でk1を1.2にすると、OpenSearch 3系と同じ式になります。前述の5文書の計算例ではElasticsearchと順位が一致し、Elasticsearchのスコアがbm25sの2.2倍になります。一般の文書では、前処理・シャード統計・文書長の丸めなどの違いにより、順位や倍率が一致しないことがあります。

## rank\_bm25でスコアが全件0になる原因

rank\_bm25を小さなデータで試すと、スコアがすべて0になることがあります。原因は[BM25Okapiのソース](https://github.com/dorianbrown/rank%5Fbm25/blob/master/rank%5Fbm25.py)にある、IDFの下限の付け方です。負のIDFは「平均IDF×epsilon（既定0.25）」に置き換えますが、ちょうど0のIDFは対象外です。平均IDF自体が負の場合は、置換後も負になります。

```
from rank_bm25 import BM25Okapi

corpus = [["bm25", "python"], ["bm25", "rust"], ["vector", "search"], ["faiss", "index"]]
bm25 = BM25Okapi(corpus)
print(bm25.idf["bm25"])            # 4文書中2文書に出る語
print(bm25.get_scores(["bm25"]))
```

```
0.0
[0. 0. 0. 0.]
```

4文書中2文書に出る語は ln((4-2+0.5)/(2+0.5)) = ln(1) = 0 で、どの文書にも点が付きません。一方、上のコードの3文書目 `["vector", "search"]` の「vector」を「bm25」に置き換えると、「bm25」の出現文書数は3となり、IDFは約-0.85から約0.14に補正されるので、**半数に出る語より、もっとありふれた語のほうが重くなる**という逆転が起きます。文書が2件だけのときは、片方にしか出ない語でもIDFが0です。

数十件程度のFAQやテスト用のデータでこれに当たると、「BM25が効かない」と誤解しがちです。まず `bm25.idf` を見て、検索に効かせたい語のIDFが0や負になっていないかを確かめます。そのうえで、IDFの計算方法を変えます。

- bm25sを既定の `method="lucene"` で使う（IDFが常に正になる）
- rank\_bm25のまま使うなら `BM25L` に替える（IDFが ln((N+1)/(n+0.5)) で常に正。上の4文書の例では「bm25」を含む2文書に0.866が付く）

`BM25Plus` もIDFは正になりますが、rank\_bm25の実装では検索語を含まない文書にもdelta分の点を足すため、上の例では一致しない2文書にも0.916が付きます。一致の有無を点の有無で判定したい用途には向きません。

## 日本語でBM25を使うときの分かち書き

日本語は単語の間に空白がないため、分割の仕方で結果がほぼ決まります。`split(" ")` で分けると文全体が1語になり、クエリと完全に一致しない限りスコアは0です。bm25sに付属する `bm25s.tokenize` も、既定の正規表現 `(?u)\b\w\w+\b` が漢字・かなの連続を1語として扱うため、「BM25は検索クエリと文書の…」が丸ごと1トークンになります。

形態素解析器で分割し、助詞などを除いてから渡すのが基本です。次の例はJanomeで名詞・動詞・形容詞の原形だけを残し、bm25sで検索します。

```
import bm25s
from janome.tokenizer import Tokenizer

docs = [
    "BM25は検索クエリと文書の関連度を計算するランキング関数です",
    "ベクトル検索は埋め込みの類似度で文書を探します",
    "Elasticsearchの既定のスコアリングはBM25です",
    "形態素解析で日本語の文章を単語に分割します",
]
t = Tokenizer()

def tokenize_ja(text):
    # 名詞・動詞・形容詞の原形だけを残し、助詞や助動詞を落とす
    return [tk.base_form.lower() for tk in t.tokenize(text)
            if tk.part_of_speech.split(",")[0] in ("名詞", "動詞", "形容詞")]

retriever = bm25s.BM25()  # 既定: method="lucene", k1=1.5, b=0.75
retriever.index([tokenize_ja(d) for d in docs], show_progress=False)

query = tokenize_ja("BM25の検索")
print(query)
results, scores = retriever.retrieve([query], k=2, show_progress=False)
for i, s in zip(results[0], scores[0]):
    print(round(float(s), 3), docs[i])
```

```
['bm', '25', '検索']
0.712 BM25は検索クエリと文書の関連度を計算するランキング関数です
0.625 Elasticsearchの既定のスコアリングはBM25です
```

JanomeはBM25を「bm」と「25」に分けますが、クエリ側も同じ関数で分割するので一致します。**この例では文書とクエリに同じ分割関数を使います**。一般には、両者の処理後のトークンが意図どおり一致するように設計します。なお、このトークン列をそのままrank\_bm25のBM25Okapiに渡すと、「bm」「25」「検索」がいずれも4文書中2文書に出るため、前章の理由でスコアは \[0\. 0\. 0\. 0.\] になります。

Elasticsearchでは、形態素解析をkuromojiアナライザが担います。辞書追加やN-gramとの併用は[Elasticsearchの日本語全文検索｜kuromojiのanalyzer構成と辞書追加、N-gram併用の判断](/tech/details/17033/)で扱っています。

## BM25とベクトル検索の使い分け

BM25は、前処理後のトークンの一致を手がかりに採点します。同義語展開などの前処理がなければ、表記の異なる言い換えを直接は扱えません。「車」と「自動車」、「PC」と「パソコン」のような言い換えは、埋め込みを使うベクトル検索が得意です。型番・エラーコード・社内用語・人名のように表記が手がかりになる検索では、識別に必要なトークンを保持できれば、BM25でその一致を順位付けに反映できます。

学習データと違う分野で比べると、BM25はいまも強い基準線です。18データセットで検索モデルのゼロショット性能を比べた[BEIR](https://arxiv.org/abs/2104.08663)（Thakur ら、2021年）は、BM25を頑健なベースラインと位置づけています。

RAGでは、BM25とベクトル検索を両方走らせて結果を統合するハイブリッド検索が定番です。統合方式（RRFとスコア正規化）の違いは[ハイブリッド検索とは？RAGでBM25とベクトル検索を統合する仕組み・RRF・実装コード](/tech/details/8014/)、LangChainでBM25を組み込む方法は[LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索](/tech/details/3751/)を参照してください。BM25で候補を多めに取り、上位をクロスエンコーダで並べ替える構成も有効です（[リランクモデルとは？ベクトル検索と組み合わせる種類・仕組み・選び方【2026年版】](/tech/details/4413/)）。

BM25だけで済ませるべきでないのは、ユーザーが自然文で質問し、文書側と語彙がずれる場面です。社内FAQに「パスワードを忘れた」と書かれていて、ユーザーが「ログインできない」と聞くような検索では、BM25単独では答えの文書に点が付きません。

どちらを主にするかは、一般論より自分のデータで決めます。実際の質問と正解文書の組を数十件用意し、BM25単体・ベクトル検索単体・両者の統合で、上位k件に正解が入る割合（Recall@k）を比べます。社内文書を対象にRAGの検索方式を評価から設計したい場合は、[RAG構築支援](/service/ai/rag/)で相談を受け付けています。

## BM25の派生：BM25F・BM25L・BM25+

| 派生版   | 変更点                    | 向く場面          |
| ----- | ---------------------- | ------------- |
| BM25F | タイトル・本文などフィールド別に重みを付ける | 構造のある文書       |
| BM25L | 長い文書への減点を緩める（delta）    | 長文が多いコーパス     |
| BM25+ | 単語を含むだけで下限の点を足す（delta） | 長文で一致語が埋もれる場合 |

BM25Fは Robertson、Zaragoza、Taylor が2004年のCIKMで提案しました。フィールドごとに出現回数を重み付きで足し合わせてから飽和をかけるのが要点で、フィールド別にBM25を計算してから足すのとは結果が違います。BM25LとBM25+はrank\_bm25にもbm25sにも実装があります。既定のdeltaは、rank\_bm25ではBM25Lが0.5、BM25Plusが1、bm25sではどちらも0.5です。

## よくある質問

### BM25のスコアに上限はありますか？

全クエリ・全コーパスに共通する上限はありません。ただし、クエリ・コーパス・パラメータを固定すれば、単語頻度の寄与が飽和するため有限の上限があります。スコアの値そのものを「0.8以上なら関連あり」のような閾値に使うのは避け、同じクエリ内での順位として扱います。ベクトル検索のスコアと混ぜるときは、順位ベースのRRFか正規化を挟みます。

### k1とbはいくつに設定すればよいですか？

まずはLuceneの既定（k1=1.2、b=0.75）から始めます。k1は1.2〜2.0、bは0.75前後がよく使われる範囲です。変えるのは、評価用のクエリと正解文書を用意して、nDCGやRecallで改善を確認できたときだけにします。

### BM25はGoogleの検索順位（SEO）に使われていますか？

Googleは検索ランキングの内部式を公開しておらず、BM25を使っているかは外部から確認できません。BM25は検索エンジンを自分で作るときの部品であって、SEOの施策として直接最適化する対象ではありません。

### LLMやRAGの時代にBM25はまだ使われていますか？

使われています。RAGのキーワード検索側、ハイブリッド検索の片翼、リランク前の候補取得がBM25の主な役割です。型番や固有名詞の一致をそのまま点に反映できるうえ、GPUも学習も要らないため、最初に置く検索として外しにくい手法です。

### BM25の元論文はどれですか？

BM25の出典として一般に引かれるのは、Robertsonら「Okapi at TREC-3」（TREC-3、1994年）です。Apache LuceneのBM25Similarityも、この論文を出典に挙げています。式の導出と背景を体系的に読むなら、Robertson と Zaragoza「The Probabilistic Relevance Framework: BM25 and Beyond」（Foundations and Trends in Information Retrieval、2009年）が向いています。

## 関連記事

- [ハイブリッド検索とは？RAGでBM25とベクトル検索を統合する仕組み・RRF・実装コード](/tech/details/8014/)
- [LangChain BM25Retrieverの使い方｜インストール・日本語対応・ハイブリッド検索](/tech/details/3751/)
- [TF-IDFとは？単語の重み付けの仕組みとコサイン類似度・ベクトル検索での使い方を実装目線で解説](/tech/details/15207/)
- [FTS5とは？SQLite全文検索の使い方と日本語対応・bm25](/tech/details/9314/)
- [リランクモデルとは？ベクトル検索と組み合わせる種類・仕組み・選び方【2026年版】](/tech/details/4413/)
- [O1 Embedderとは？思考を生成してから検索する埋め込みモデルの仕組みと精度](https://www.issoh.co.jp/tech/details/5441/)

---

出典: [BM25とは？読み方・計算式とk1・bの意味、Python実装（rank\_bm25・bm25s）の落とし穴](<https://www.issoh.co.jp/tech/details/4388/>)（株式会社一創）
