Содержание
  1. Определение лексического ранжирования BM25
  2. Формула и вариант IDF
  3. TF, df, N, длина документа и средняя длина
  4. Разбор трёх документов и ранжирование
  5. Насыщение частоты k1 и нормализация длины b
  6. Нулевое совпадение и роль анализатора
  7. Относительный score: не вероятность и не универсальная шкала
  8. Учебный код и проверка результата
  9. Частые вопросы
Справочник

Что такое BM25: как поиск оценивает совпадения слов

13 ноября 2026 · 10 минут чтения

BM25 — это лексическая функция ранжирования, которая оценивает, насколько документ соответствует запросу, складывая вклады отдельных слов. Что такое BM25: как поиск оценивает совпадения слов — вопрос про три множителя: частоту терма TF, редкость слова IDF и поправку на длину документа. Формула возвращает относительное число, по которому документы сортируются внутри одного запроса.

Определение лексического ранжирования BM25

BM25 работает на уровне токенов. Запрос разбивается на слова, для каждого слова считается вклад в документ, вклады суммируются. Итог зависит от того, сколько раз слово встретилось в документе, насколько оно редкое во всём индексе и какова длина документа относительно средней.

В документации Lucene, на которую ссылается API BM25Similarity, функция описана как сумма по термам запроса. В обзоре Elastic «Practical BM25» IDF-компонента измеряет, как часто терм встречается во всех документах, и штрафует распространённые слова. Это ключевая мысль: редкое слово несёт больше сигнала, чем частое.

Лексическое ранжирование опирается на точные токены. Словоформы, синонимы и опечатки оно само не связывает. В документации Weaviate указано, что проект использует BM25F — расширение BM25 для нескольких полей индекса. Это отдельный вариант той же идеи: вклад поля получает вес.

Практический вывод для читателя: BM25 хорошо отвечает на запросы с конкретными терминами — артикулами, именами, техническими словами. Для перефразированных запросов нужен дополнительный слой, например векторный поиск. В документации Weaviate отмечено, что keyword-поиск комбинируется с векторным в гибридный режим.

Формула BM25 для одного терма запроса выглядит так:

КОД
score(D, Q) = Σ IDF(t) · [ tf · (k1 + 1) ] / [ tf + k1 · (1 − b + b · |D| / avgdl) ]

Здесь D — документ, Q — запрос, t — терм, tf — частота терма в документе, |D| — длина документа, avgdl — средняя длина. Сумма идёт по термам запроса, которые присутствуют в документе.

Формула и вариант IDF

IDF в BM25 измеряет редкость терма. В документации Lucene приведён вариант:

КОД
IDF = log(1 + (docCount − docFreq + 0.5) / (docFreq + 0.5))

docCount в Lucene — число документов со значением рассматриваемого поля в области сбора статистики, docFreq — число документов с термом в этом поле. Чем больше документов содержат терм, тем меньше IDF. Слагаемое 0.5 сглаживает крайние случаи, когда терм есть почти везде или почти нигде.

Вариантов IDF несколько. Классическая формула из статьи Robertson и Spärck Jones отличается от варианта Lucene. Elasticsearch и Lucene используют именно вариант с log(1 + ...), и это важно: score из Lucene и score из другой библиотеки с другой формулой напрямую сравнивать нельзя.

В обзоре Elastic IDF описан как компонент, который штрафует распространённые термы. В документации Lucene формула приведена явно. Если вы читаете чужой код и видите log((N − df + 0.5) / (df + 0.5)) без единицы внутри логарифма, это другая реализация с другим поведением на границах.

При docFreq = docCount (терм есть во всех документах) вариант Lucene даёт log(1 + 0.5 / (docCount + 0.5)) — небольшое положительное число. Классический вариант без единицы дал бы отрицательное значение. Это причина, по которой реализации расходятся в оценке стоп-слов.

IDF — множитель редкости терма. При одном поле и общей области статистики его значение одинаково для оцениваемых документов. В распределённом индексе статистика может быть локальной для шарда, поэтому область её сбора нужно учитывать.

TF, df, N, длина документа и средняя длина

Разберём переменные по отдельности.

TF (term frequency) — сколько раз терм встречается в конкретном документе. Частоту считают по токенам после выбранного анализа текста. Поиск вхождений произвольной подстроки даст другой показатель.

df (document frequency) — в скольких документах индекса встречается терм хотя бы один раз. Не путать с TF: df считается по документам, TF — внутри одного документа.

N (docCount) — число документов со значением выбранного поля в используемой области статистики. В однополевом учебном примере ниже N = 3.

|D| (длина документа) — число токенов в документе. В документации Lucene средняя длина выражена как sumTotalTermFreq / docCount: суммарная частота всех термов, делённая на число документов.

avgdl (средняя длина) — среднее число токенов на документ по индексу. Нужна как база сравнения: документ длиннее среднего получает штраф, короче — небольшое преимущество.

Переменная Что считает От чего зависит
TF вхождения терма в документе документ, терм
df документы с термом индекс, терм
N всего документов индекс
|D| токенов в документе документ
avgdl средняя длина индекс

Средняя длина пересчитывается при изменении индекса. В распределённых системах статистика может считаться по шарду, и тогда avgdl у разных шардов отличается. В документации Azure AI Search отмечено, что учитываются только поля, помеченные как searchable, либо поля из searchFields запроса.

Разбор трёх документов и ранжирование

Возьмём учебный корпус из трёх документов:

КОД3 строки
d1 = [api, timeout]
d2 = [api, api, api, error]
d3 = [login, error]

Запрос — api. Считаем параметры: N = 3, df = 2 (терм api есть в d1 и d2), avgdl = (2 + 4 + 2) / 3 = 8/3 ≈ 2.667. Параметры k1 = 1.2, b = 0.75.

IDF по варианту Lucene:

КОД
IDF = ln(1 + (3 − 2 + 0.5) / (2 + 0.5)) = ln(1 + 1.5 / 2.5) = ln(1.6) ≈ 0.470

Теперь TF-множитель для каждого документа. Знаменатель: tf + k1 · (1 − b + b · |D| / avgdl).

d1: tf = 1, |D| = 2. Отношение |D|/avgdl = 2 / 2.667 = 0.75. Знаменатель = 1 + 1.2 · (0.25 + 0.75 · 0.75) = 1 + 1.2 · 0.8125 = 1.975. Числитель = 1 · 2.2 = 2.2. TF-множитель = 2.2 / 1.975 ≈ 1.114.

d2: tf = 3, |D| = 4. Отношение = 4 / 2.667 = 1.5. Знаменатель = 3 + 1.2 · (0.25 + 0.75 · 1.5) = 3 + 1.2 · 1.375 = 4.65. Числитель = 3 · 2.2 = 6.6. TF-множитель = 6.6 / 4.65 ≈ 1.419.

d3: tf = 0. TF-множитель = 0, score = 0.

Итоговое ранжирование: d2 (score ≈ 0.667), d1 (score ≈ 0.524), d3 (0). Три повторения api дают d2 примерно на 27% больше, чем одно вхождение в d1. Втрое большего результата нет: насыщение частоты и штраф за длину действуют одновременно.

Насыщение частоты k1 и нормализация длины b

k1 управляет насыщением частоты. В документации Lucene сказано: k1 контролирует нелинейную нормализацию частоты терма. При малых tf рост score почти линейный, при больших — выходит на плато. Значение по умолчанию в Elasticsearch — 1.2, как указано в обзоре Elastic.

b управляет нормализацией длины. В документации Lucene: b контролирует, насколько длина документа нормализует значения tf. Значение по умолчанию в Elasticsearch — 0.75.

Параметр Роль Значение по умолчанию в Elasticsearch
k1 насыщение частоты 1.2
b нормализация длины 0.75

При b = 0 длина документа игнорируется, все документы оцениваются по TF и IDF. При b = 1 штраф за длину максимален. При k1 → 0 TF-множитель для присутствующего терма стремится к 1: повторения перестают различать документы с совпадением. При больших k1 насыщение наступает позже.

Практический эффект: длинный документ, где терм встретился один раз, проигрывает короткому с тем же TF. Это защита от «раздутых» страниц, где нужное слово тонет в тексте. Одновременно это причина, по которой короткие заголовки и описания часто обгоняют длинные статьи при равном TF.

Нулевое совпадение и роль анализатора

Если терма нет в документе, TF = 0, и весь TF-множитель обнуляется. Документ получает score = 0 по этому терму. Если запрос состоит из одного такого терма, все документы получат 0 и порядок будет произвольным.

Проверим на том же корпусе запрос connection. Терма нет ни в одном документе: df = 0, IDF считается, но TF = 0 у всех. Результат — 0 для d1, d2, d3. Это пример отсутствия токенов. Синонимию BM25 не восстанавливает: connection и соединение для него разные термы.

Здесь вступает анализатор. Токенизация определяет, что считается словом. В документации Weaviate указано, что метод токенизации по умолчанию — word. В README библиотеки rank_bm25 отмечено, что пакет не делает предобработку текста: класс принимает список списков строк — токенов документов. Предобработку корпуса и запроса выполняет вызывающий код, и она должна совпадать.

Практическое правило: если запрос и корпус токенизируются по-разному, BM25 сравнивает разные множества токенов. Стемминг, приведение регистра, удаление стоп-слов и разбиение по дефису должны быть одинаковыми с обеих сторон.

Относительный score: не вероятность и не универсальная шкала

Score BM25 — относительная величина. Он не является вероятностью релевантности и не имеет верхней границы. В документации Azure AI Search прямо сказано: диапазон неограничен. Шкалы оценок разных запросов могут различаться, поэтому сравнивать их значения напрямую нельзя.

Сравнивать можно только документы внутри одного запроса к одному индексу. Между индексами score несопоставим: разные N, разные avgdl, разные анализаторы, разные значения k1 и b. Между разными реализациями — тоже: вариант IDF в Lucene отличается от классической формулы.

В документации Azure AI Search отмечено, что статистика считается по шарду или реплике, и порядок документов с равными score нестабилен. Это следствие распределённой архитектуры: у каждого шарда свои N и avgdl.

Практический вывод: score используйте для сортировки внутри запроса. Для порогов «релевантно / нерелевантно» нужна отдельная калибровка на ваших данных. Для гибридного поиска score BM25 и векторное расстояние приводятся к общей шкале отдельным шагом — в документации Weaviate это описано как комбинирование keyword- и векторного поиска.

Учебный код и проверка результата

Ниже — полный учебный файл на стандартной библиотеке Python. Он печатает промежуточные величины и ранжирование для запросов api и connection. Это искусственный корпус, не замер поисковика.

PYTHON26 строк
# Учебный корпус; формула IDF варианта Lucene. Не замер поисковика.
from math import log

corpus = {"d1": ["api", "timeout"],
          "d2": ["api", "api", "api", "error"],
          "d3": ["login", "error"]}
k1, b = 1.2, 0.75
N = len(corpus)
avgdl = sum(map(len, corpus.values())) / N

def score(term, document):
    df = sum(term in tokens for tokens in corpus.values())
    idf = log(1 + (N - df + 0.5) / (df + 0.5))
    tf = document.count(term)
    denominator = tf + k1 * (1 - b + b * len(document) / avgdl)
    tf_factor = tf * (k1 + 1) / denominator
    return idf * tf_factor, {"tf": tf, "df": df, "idf": idf,
                             "length": len(document), "avgdl": avgdl,
                             "denominator": denominator, "tf_factor": tf_factor}

for term in ["api", "connection"]:
    rows = [(name, *score(term, tokens)) for name, tokens in corpus.items()]
    rows.sort(key=lambda row: (-row[1], row[0]))
    print("Запрос:", term)
    for name, value, details in rows:
        print(name, round(value, 6), details)

Что проверить в выводе. Для запроса api порядок должен быть d2, d1, d3. Для запроса connection все три score равны нулю. Если порядок другой — проверьте подсчёт df по числу содержащих терм документов и пересчёт avgdl по всему корпусу.

Что менять под себя: список документов и токенов, значения k1 и b, набор запросов. Токенизацию добавляйте перед подачей в score, одинаковую для корпуса и запроса.

Частые вопросы

Какую формулу IDF использует Lucene?

В документации BM25Similarity указан вариант IDF = log(1 + (docCount − docFreq + 0.5) / (docFreq + 0.5)). В учебном примере используется этот вариант. При сравнении реализаций сверяйте формулу IDF и параметры ранжирования.

Чем BM25 отличается от TF-IDF?

В рассмотренной формуле BM25 параметр k1 управляет насыщением частоты, b — нормализацией длины. Elastic также указывает различие формул IDF в TF-IDF и BM25. Для содержательного сравнения выберите конкретную реализацию TF-IDF и проверьте обе функции на одном корпусе и запросах.

Как BM25 используется в RAG?

BM25 служит лексическим ретривером: по запросу находит документы с точными совпадениями токенов. Результаты объединяются с векторным поиском в гибридную выдачу. В документации Weaviate это описано как комбинирование keyword- и векторного поиска.

Почему одинаковые score у разных документов?

При равных score порядок определяется вторичным критерием, часто идентификатором документа. В документации Azure AI Search отмечено, что порядок при равных score нестабилен. Для воспроизводимости добавляйте явный tie-breaker.

Можно ли сравнивать score между индексами?

Нет. Score зависит от N, avgdl, анализатора и значений k1 и b конкретного индекса. В документации Azure AI Search указано, что диапазон score неограничен. Сравнивайте документы внутри одного запроса к одному индексу.

Читайте также

3 материала