elasticsearch 评分算法BM25

介绍

BM25(Okapi BM25)是工业界最主流的 全文检索相关性打分算法,核心是对 TF‑IDF 做词频饱和+长度归一化修正,让短文档更占优、高频词不过度权重、长文档不天然占便宜。下面从原理、公式、参数、流程、优缺点、与 TF‑IDF 对比、工程实现、调优与应用场景,一次性讲透。

BM25 是什么

  • 全称:Okapi BM25(Best Matching 25),1970s 由 Robertson 等人提出,因是第25版且效果最佳而得名。
  • 定位:概率检索模型下的词袋(Bag‑of‑Words)排序算法,只看词是否出现、不看语序。
  • 用途:计算查询 Q 与文档 D 的相关性分数,分数越高排越前;是 Elasticsearch、Solr、OpenSearch 的默认相关性算法。

应用场景

  1. 传统搜索引擎:ES、Solr、OpenSearch 默认排序
  2. RAG 检索:大模型知识库检索(先 BM25 召回,再重排)
  3. 站内搜索:电商商品、新闻、博客、知识库搜索
  4. 短文本匹配:标题、摘要、关键词检索
  5. 混合检索:BM25(关键词)+ 向量检索(语义),兼顾精准与语义

公式


全文检索完整流程

  1. 建索引阶段

    • 文档分词 → 词项列表

    • 统计:总文档数 N、各词 n(q_i)、文档长度 |D|、平均长度 {avgdl}

    • 计算每个词的 IDF 并存入索引
  2. 检索阶段

    • 查询分词 → (q1 \ldots qn)

    • 对每个查询词:

      • 查倒排索引,拿到包含该词的文档列表
      • 计算该词在文档中的 TF
      • 代入公式计算单词语分
    • 所有词分数求和 → 文档最终 BM25 分
    • 按分数降序排序,返回 Top‑K

BM25 vs TF‑IDF

维度 TF‑IDF BM25
词频 TF 线性增长,无上限 饱和增长,有天花板((k_1+1))
文档长度 长文档天然占优 归一化,短文档更占优
IDF 计算 无平滑,易无穷大 +0.5 平滑,稳定有界
排序效果 易被关键词堆砌作弊 抗作弊,相关性更准
工业应用 早期搜索引擎 ES/Solr 默认,RAG 检索首选

优缺点总结

优点

  1. 相关性精准:解决 TF‑IDF 两大缺陷,排序更符合人类直觉
  2. 轻量高效:纯统计、无训练、可解释、速度快(适合海量数据)
  3. 鲁棒性强:IDF 平滑、词频饱和,抗噪声、抗作弊
  4. 易调优:仅 2 个核心参数,场景适配简单

缺点

  1. 词袋假设:不考虑语序、语义、上下文(如“我吃苹果”≠“苹果吃我”)
  2. 无法理解语义:同义词、近义词、多义词处理弱(需结合词向量/大模型)
  3. 依赖分词质量:中文需精准分词,否则效果差

工程实现

Python 极简版

import math
from collections import defaultdict

# 1. 计算 IDF
def compute_idf(documents):
    N = len(documents)
    idf = defaultdict(int)
    for doc in documents:
        unique_words = set(doc)
        for word in unique_words:
            idf[word] += 1
    # BM25 平滑 IDF
    for word in idf:
        idf[word] = math.log((N - idf[word] + 0.5) / (idf[word] + 0.5))
    return idf

# 2. 计算单文档 BM25 分数
def bm25_score(query, doc, idf, k1=1.2, b=0.75, avgdl=100):
    doc_len = len(doc)
    score = 0.0
    for word in query:
        if word not in doc:
            continue
        tf = doc.count(word)
        idf_val = idf[word]
        # BM25 公式
        numerator = tf * (k1 + 1)
        denominator = tf + k1 * (1 - b + b * doc_len / avgdl)
        score += idf_val * numerator / denominator
    return score

# 3. 批量检索排序
def bm25_search(query, documents, top_k=3):
    idf = compute_idf(documents)
    avgdl = sum(len(doc) for doc in documents) / len(documents)
    # 计算所有文档分数
    scores = []
    for i, doc in enumerate(documents):
        s = bm25_score(query, doc, idf, avgdl=avgdl)
        scores.append((i, s))
    # 排序取 Top-K
    scores.sort(key=lambda x: x[1], reverse=True)
    return [documents[i] for i, s in scores[:top_k]]

进阶:BM25 与向量检索结合(RAG 最佳实践)

  • BM25 强项:关键词精准匹配、速度快、可解释
  • 向量检索强项:语义理解、同义词、上下文
  • 最佳组合:BM25 召回 Top‑50 + 向量重排 Top‑10,兼顾速度、精准、语义

原文出处:/show_1GW3LxPO7Q2z.html