想象你是一家婚恋平台的资深红娘。一位用户提交了择偶要求:“希望对方爱旅行、会做饭、怕猫”。档案库里躺着几十万份资料,你必须在两三秒内把最可能合拍的人排在最前面——这正是搜索引擎在做的事。用户敲下一串查询词,搜索引擎从海量文档中挑出“最相关的”结果,而“最相关”不能靠人工翻档案,要靠一个可计算的数字:相关性评分。 早期的匹配只看“合不合格”,后来人们发现用户要的是“有多合适”的排序。于是诞生了 TF-IDF:看关键词在档案里出现几次、这个要求有多稀有;再进化到 BM25:承认同一句话说三遍就腻,长档案里的一笔带过不算数;最后还允许红娘开小灶,比如“同城加分”“学历加权”。这就是搜索引擎相关性评分从粗糙到精细的全过程。 这篇文章用红娘牵线的故事,带你搞懂 ES 默认的 BM25 打分原理,以及如何调优排序让用户更快找到目标。
掌握相关性评分原理,你就能在 Elasticsearch 搜索项目里读懂打分机制、精准调优排序,让用户更快找到目标——这是搜索与推荐工程师的核心功底,也是 RAG 检索质量的关键。
可视化演示
展示TF-IDF到BM25的评分演进,对比线性词频与饱和曲线的差异,直观呈现字段长度归一化和多字段打分对搜索排序的影响。
本节配有交互动效(进入视口后自动加载,约 3KB)。
TF-IDF:线性词频权重 TF-IDF中词频线性增长,高频词过度加权 BM25引入词频饱和+字段长度归一化 BM25解决了TF-IDF词频无限增长的缺陷
搜索排序概述
红娘的办公室里,桌上有“必须见一面”与“再看一眼”两摞档案,这是最原始的二分法。可很快她发现:用户要的不是“过没过线”,而是“谁排在最上面”。给每份档案打一个匹配度数字,从高到低排成一列,才算真正高效。搜索引擎面临的处境一模一样——用户敲下查询词,文档库里的候选千千万,系统必须在一瞬间给出一个有先后次序的答案。
在搜索引擎内部,这个任务分为两步:召回与排序。召回借助倒排索引,先筛出与查询词相关的候选文档;排序阶段再逐一打分,按分数从高到低返回。这个分数就是相关性评分(relevance score)。Elasticsearch 的排序阶段并非一个黑盒,而是一条清晰的公式链:命中的查询词越多越加分,词越稀有越加分,词在文档里出现得越充分越加分。
红娘把“综合匹配分”定为公式后,接下来每一节都在升级其中的一环:先搞清“出现次数”怎么算才不笨(TF-IDF),再加入“说多了就腻”的饱和(BM25),然后处理长短档案(长度归一化),最后分栏目加权(多字段)与定制加分(function_score)。整本打分手册就这样一页页丰满起来。
- 协调因子 coord衡量查询中有多少个词真的出现在文档里,命中词越多,说明文档覆盖需求越全面,给一个整体加成。
- 对查询词求和把查询里每个词单独打分再相加,让多个证据互相叠加,而不是只看某一个词。
- 逆文档频率 idf(t)衡量这个词在整个文档集合里到底多稀有,越稀有越能区分不同文档,权重越大。
- 词频与归一化 tf_norm词在文档里出现得越多贡献越大,但会结合文档长度做归一,避免长文档天然占便宜。
TF-IDF 原理
红娘的第一版规则只有两条直觉:一,要求的关键词在档案里出现得越多,说明对方越符合,这是词频;二,越是人人都写、毫无特色的要求(比如“会吃饭”),匹配上了也毫无价值;越是只有极少数人写的要求(比如“会开挖掘机”),一旦匹配就是强烈命中,这是稀有度。常见词看次数,稀有词看身份,两条线索相乘。
这套直觉在信息检索领域叫 TF-IDF。TF(Term Frequency)统计查询词在文档里出现的次数;IDF(Inverse Document Frequency)用文档总数除以包含该词的文档数再取对数,衡量一个词的“身份稀缺度”。两者相乘,就是经典的 TF-IDF 相关度。
TF-IDF 第一次让“有多合适”变成可计算的数字,但两个漏洞立刻暴露:词频是线性的,把关键词堆 50 次分数就翻十倍,注水严重;长文档天然词频高,同样一句话,写在长文里比写在短文里显得更“重要”。红娘很快发现,那些把“旅行”写满三页的档案反而最可疑——于是 BM25 登场了。
- 词频 tf(t,d)统计词 t 在文档 d 里出现的次数,出现越多,说明文档越聚焦在这个概念上。
- 文档总数 N索引中全部文档的数量,它是计算稀有度的整体参照系。
- 文档频率 df(t)统计包含词 t 的文档有多少篇,注意是篇数而非出现次数。
- 稀有度对数文档越多、含该词的文档越少,比值越大;取对数是为了防止比值悬殊带来的分数爆炸。
- 相乘合成把“文中出现次数”与“全局稀有度”乘起来,两个信号互相制衡,得到最终 TF-IDF 分。
BM25 词频饱和
红娘学乖了一条人情世故:一个人说三遍“我爱旅行”非常可信,说十遍就开始像祥林嫂,说五十遍基本可以断定是复制粘贴。心动感不会随着重复次数成倍增加,这叫词频的饱和效应。在新规则里,每多重复一次,边际收益都在递减。
把这种直觉数学化的正是 BM25(Best Matching 25),它是概率检索模型的代表作,也是 Elasticsearch 5.0 之后的默认打分算法。BM25 把每个查询词的贡献拆成三块相乘:平滑后的 IDF、饱和的词频项、长度归一化因子。整体公式如下:
词频饱和项画成曲线,是一条先陡后平的渐近线:开头每多出现一次都明显加分,到后面几乎纹丝不动。参数 k1 控制“腻得有多快”,默认 1.2;k1 越大,饱和越慢,词频差异越能拉开分差。红娘在档案上画圈,画到第十个圈时连自己都烦了——k1 就是那支让她保持克制的笔。
- 对查询词累加查询 Q 包含 n 个词,每个词独立算出贡献后求和,多词查询就是多份证据叠加。
- BM25 版逆文档频率与 TF-IDF 的 log 类似,但加了 0.5 平滑项,避免某个词出现在全部文档时出现怪异负值。
- 词频 f(q_i,D)词 q_i 在文档 D 中出现的次数,它是饱和曲线横轴上的输入变量。
- 词频饱和项分子分母都含词频,f 很小时近似线性,f 越大越逼近上限 k1+1,重复次数的边际收益递减。
- 长度归一化分母用平均长度 avgdl 校准文档长度,避免长文档因为分母偏大而系统性吃亏,这一块正是“理想文档长度”的来源。
理想文档长度
同是写一句“爱旅行”,写在 500 字的小卡片上,是浓得化不开的人设;写在 5 万字自传第 409 页,只是路过的一笔。红娘打分的另一条铁律由此诞生:文档越短,一个词的“浓度”越高,越接近铁证。搜索引擎里这叫字段长度归一化(field length normalization),也叫“理想文档长度”。
BM25 没有拿文档绝对长度直接做分母,而是用当前文档长度与集合平均长度之比来校准,并由参数 b 控制惩罚力度。归一化因子如下:
b=0 时红娘完全不看档案厚薄,长短一视同仁;b 越接近 1,长档案里那句“爱旅行”越被稀释。Elasticsearch 默认 b=0.75,是个多年实践折中的稳妥值。遇到全是短标题的商品库,可以调高 b 让“标题党”出头;遇到全是长报告的文档库,则要调低 b,防止长文档全军覆没。
- 当前文档长度 |D|文档包含的词数,它是长度惩罚的输入变量,词数越多分母越大。
- 集合平均长度 avgdl所有文档长度的平均值,是长度的“基准秤砣”,每篇文档都跟它比较。
- 相对长度比当前文档是平均长度的几倍,大于 1 表示偏长,小于 1 表示偏短。
- 惩罚系数 b0 到 1 之间的旋钮,控制长度因素对打分的干预程度,b 越大,长文档被稀释越厉害。
多字段打分
红娘手里的档案其实分好几个栏目:基本信息、兴趣爱好、自我评价。兴趣栏写“旅行”和学历栏捎带“旅行”,含义完全不同。聪明的红娘会给不同栏目配不同权重:兴趣爱好栏更值钱,基本信息栏是及格线。搜索引擎里的文档同样由多个字段组成,标题、正文、标签各司其职。
Elasticsearch 的每个字段都可以独立计算一次 BM25 分数,再把它们组合起来。组合有两大流派:一是把各字段分数加权求和,让所有证据一起说话,对应 multi_match 的 most_fields 策略;二是只取分数最高的那个字段,相信“最响亮的声音”,对应 best_fields 策略。
实际项目中,标题字段给 3 倍权重、标签字段 1.5 倍、正文字段 1 倍,是再常见不过的配置——这就像一场分栏目加权、各取所长的红娘评审。加权求和让“证据越多越好”,取最大值让“最强项说了算”,二者各有用武之地。
- 字段权重 w_f业务赋予每个字段的重要度,标题常常比正文更值钱,权重越高影响越大。
- 单字段 BM25 分 score_f把字段内容当作独立的小文档,单独跑一次 BM25 打分。
- 加权求和所有字段的证据一起累加,字段越多、越全面分数越高,适合“广撒网”的搜索场景。
- 取最大值只保留最强字段的分数,适合“一项专精就足够”的场景,避免弱字段拖后腿。
function_score 定制
基础分是“标准答案”,但红娘总被要求开小灶:央企工作加分、同城加分、年龄差太大减分。这些业务规则与词语匹配无关,却切切实实影响“合不合适”。搜索引擎给这类需求留了一扇后门:function_score 查询。
function_score 在 BM25 原始分之上叠加自定义函数。最简单的用法是给通过过滤条件的文档乘一个固定权重;常见的是衰减函数——按数值字段(如发布时间、地理位置、价格)离理想点的远近来平滑降分;还可以干脆跑一段脚本算出任意想要的因子。
红娘的“同城加 3 分”,落到系统里就是给城市字段命中的文档乘一个加分因子;“刚发布的新闻优先”就是给发布时间套一条高斯衰减曲线,越旧越淡。function_score 是把业务经验翻译成数学规则的最后一公里,但必须克制使用——权重开得太大,会把 BM25 辛苦算出的文本相关度冲得干干净净。
- 原始查询分 query_scoreBM25 等原生打分算出的文本相关度,是定制加分的底座。
- 定制因子 factor_j每个函数产出一个因子,可以是固定 weight、衰减函数值或脚本返回值。
- 连乘组合默认 multiply 模式把所有因子相乘,越相关的文档被放大得越明显,同时也可能被小因子压下去。
- 其他组合模式除了连乘,还能用 sum 把因子累加,或用 replace 完全替换原始分,由 boost_mode 控制。
调优实践
红娘不敢拍脑袋定规则,她会把三年前的配对数据翻出来当考卷,让新旧规则各跑一遍,看哪套规则排出的前三名成功率更高。调参的本质,就是一场可重复、可对比、可回归的实验。搜索调优也一样,改一个参数之前,先想清楚观察指标是什么。
搜索调优的关键旋钮其实就那几颗:k1 控制词频的差异化程度,b 控制长度惩罚力度,字段 boost 控制业务权重,function_score 承载自定义规则。不同场景参数取向完全不同:短文本商品库、长文知识库、新闻时效库,各有各的“甜点区间”。一个典型的加权调优写法如下:
一个可落地的调优流程是:发现问题(该靠前的文档靠后了)→ 用 explain 拆解分数构成 → 调整旋钮 → 用标注好的查询集做回归对比 → 上线观察。红娘最终会沉淀出一套属于自己的“评分手册”,而系统里那几颗旋钮,就是她反复校准后留下的刻痕。
- 标题字段分数商品或文章标题的 BM25 分,标题匹配查询词后得到,是排序的主信号。
- 标题权重 3业务认为标题匹配比正文重要三倍,直接乘 3 放大它的影响。
- 正文字段分数正文的 BM25 分,正文更长、词频分散,通常贡献较小但稳定。
- 权重 1 与求和正文按原样计入,两项相加得到最终排序分,整个式子就是一次标准的字段加权。
AI 应用场景
电商搜索排序调优
阶段:检索排序与相关性调优
解决的问题:传统做法直接用 BM25 默认参数,结果“堆关键词”的长标题商品总霸占前排,用户真正想要的精准商品沉到后面。通过调低 k1 让词频饱和来得更快、给标题字段加大 boost、再用 function_score 给促销品加权,排序就能贴合业务目标。
具体案例:某电商搜索团队把商品标题字段 boost 设为 3、品牌字段设为 2,同时把 b 调低到 0.6 避免长标题被过度惩罚,再对促销商品叠加固定权重因子。上线后搜索点击率显著提升,首屏商品与用户意图的匹配度明显改善。
企业知识库检索与 RAG 召回
阶段:检索召回与排序
解决的问题:企业内部知识库文档长短悬殊,默认 BM25 下长文档要么永远占优、要么永远垫底;同时旧文档和新政策文档混在一起。用 b=0.6 温和处理长度归一化,标题字段加权,再给“最后更新时间”套一条高斯衰减曲线,让新文档浮出水面。
具体案例:某公司 50 万篇文档的知识库接入 BM25 排序后,员工搜索“报销新规”,带时间衰减的 function_score 让 2024 年的新规定排在旧版本前面,首屏命中率显著提升,RAG 问答的检索质量随之改善。
搜索效果回归实验
阶段:效果评估与参数调优
解决的问题:调参无据可依,改一个参数全站结果大变样,上线后才发现效果变差。用人工标注查询集 + 排序评价指标做回归对比,让每次调参都有数据背书。
具体案例:搜索团队维护了 200 条带人工标注的查询集,每周跑一次排序评测,对比新旧参数下的文档排序质量;每次调整 k1、b 或 boost 都先过一遍回归测试,再决定是否上线。
Python 演示
import mathfrom collections import Counter class BM25Scorer: def __init__(self, corpus, k1=1.2, b=0.75): self.corpus = corpus self.k1 = k1 self.b = b self.doc_count = len(corpus) self.doc_lengths = [len(doc.split()) for doc in corpus] self.avgdl = sum(self.doc_lengths) / float(self.doc_count) self.df_map = Counter() self.tf_maps = [] for doc in corpus: tokens = doc.split() freq = Counter(tokens) self.tf_maps.append(freq) for word in set(tokens): self.df_map[word] += 1 def idf(self, word): df = self.df_map.get(word, 0) return math.log(1.0 + (self.doc_count - df + 0.5) / (df + 0.5)) def score(self, query, doc_id): total = 0.0 dl = self.doc_lengths[doc_id] tf_map = self.tf_maps[doc_id] for word in query.split(): f = tf_map.get(word, 0) if f == 0: continue norm = 1.0 - self.b + self.b * dl / self.avgdl tf_component = f * (self.k1 + 1.0) / (f + self.k1 * norm) total += self.idf(word) * tf_component return total def main(): # 玩具语料: 5 篇英文文档 corpus = [ "the cat hunts mice at night", "the owl is a night hunter and eats mice", "python is a language for text search", "search engines use python for scoring", "mice steal grain from farm stores", ] query = "mice night" scorer = BM25Scorer(corpus) result = sorted( range(len(corpus)), key=lambda i: scorer.score(query, i), reverse=True ) for i in result: print("doc %d score=%.4f %s" % (i, scorer.score(query, i), corpus[i])) if __name__ == "__main__": main()
self.avgdl = sum(self.doc_lengths) / float(self.doc_count)算出集合平均长度,长度归一化全靠它当“基准秤砣”self.df_map[word] += 1统计每个词出现在多少篇文档里,这是 IDF 的原始输入return math.log(1.0 + (self.doc_count - df + 0.5) / (df + 0.5))BM25 平滑版 IDF,加了 0.5 防止分母为零norm = 1.0 - self.b + self.b * dl / self.avgdl长度归一化因子,文档越长分母越大,分数被稀释tf_component = f * (self.k1 + 1.0) / (f + self.k1 * norm)饱和词频项,f 增大时分值逼近上限total += self.idf(word) * tf_component每个词的贡献累加,得到文档最终相关性分key=lambda i: scorer.score(query, i), reverse=True按分数降序排列,把最相关的文档排最前
常见误区
误区我写文档时特意把用户常搜的词多重复几遍,搜索排名一定能上去,反正词频是越多分越高。
正解BM25 的词频项是一条饱和曲线,出现 3 次和出现 10 次分差很小,堆到 50 次几乎不再加分。真正拉开差距的是内容匹配质量、字段权重与长度归一化。“堆词”在搜索引擎里反而可能被判定为作弊信号。就像红娘听到对方把“爱旅行”重复 50 遍,不仅不加分,还会怀疑档案造假。
为何常见:TF-IDF 的线性词频直觉太深入人心,很多人不知道 BM25 已经引入了饱和机制,仍用“越多越好”的旧观念指导实践。
误区IDF 大的词一定是好词,调权时应该给罕见词疯狂加权重,反正越稀有越有价值。
正解IDF 只反映区分度,不反映相关性。一个词在数据里极其罕见,可能只是因为它是错别字或冷门专有名词,命中它并不能代表文档相关。BM25 的 IDF 还做了平滑,防止极端值统治整个分数。红娘不会因为只有一人写了“会手语”就把对方排第一,她还要看其他栏目与整体匹配度。
为何常见:把“稀有等于有价值”的日常直觉直接搬到检索里,忽略了相关性是多个信号综合评判的结果。
误区长文档总是稀释关键词、体验差,把 b 调到 1,彻底惩罚长文档就一劳永逸了。
正解b 控制的是“相对平均长度”的惩罚程度。把所有长文档都打死,短标题文档会霸榜,但短文档信息量少、主题可能不聚焦,结果既不相关又不完整。b=0.75 是多年实践的折中值,具体取值要靠实验数据说话。红娘既不会无视档案厚薄,也不会一票否决所有长自传。
为何常见:把“长文档体验不好”的经验绝对化,忽略了长度惩罚的本意是校准“关键词浓度”,而不是筛选文章体裁。
误区我把标题字段 boost 调大,这个字段相关的文档就一定能排到前面,甚至原本不匹配的词也能被加权捞出来。
正解boost 是给“已经匹配上”的分数做乘法,不匹配的文档该是 0 分还是 0 分。boost 只放大已有信号,不能创造匹配;function_score 的 weight 也只在过滤条件命中的文档上生效。就像红娘的“同城加分”只作用于通过初筛的候选人,不会把基本要求都不符合的档案捞回来。
为何常见:把“加权”误解为“添加”,没理解 boost 本质上是乘性操作而非加性操作。
面试题
Q1. BM25 算法中 k1 参数的主要作用是什么?
- A.控制 IDF 的缩放幅度
- B.控制词频饱和速度← 正确答案
- C.控制文档长度惩罚
- D.控制查询词的权重
答案:B。k1 是 BM25 词频饱和项里的参数,控制重复出现的边际收益递减速度。k1 越大饱和越慢,词频差异越能拉开分差;k1 越小,出现两三次就接近打满。A 说的是 IDF 的缩放,C 对应 b 参数,D 在 BM25 中不存在。就像红娘对“我爱旅行”说几遍才腻,k1 决定她的耐心程度。
Q2. BM25 打分公式中包含哪些可调参数?
- A.k1 词频饱和参数← 正确答案
- B.b 长度惩罚参数← 正确答案
- C.a 窗口大小参数
- D.g 梯度下降率
答案:AB。BM25 的可调参数就是 k1 和 b:k1 控制词频饱和速度,b 控制长度归一化强度。C 中“窗口大小”是 NLP 里的概念,D 中“梯度下降”是机器学习训练的概念,两者都不属于 BM25 打分模型。就像红娘的评分手册只有“腻不腻”和“介不介意档案厚”两个旋钮。
Q3. 相比 TF-IDF,BM25 最重要的改进是什么?
- A.引入词频饱和机制← 正确答案
- B.加入分词器支持
- C.支持多种检索语言
- D.提高索引构建速度
答案:A。TF-IDF 的词频是线性的,堆词就能刷分;BM25 把线性词频换成饱和曲线,重复出现边际收益递减,还加了长度归一化。B 分词属于预处理环节,C 多语言支持与打分算法无关,D 索引构建速度是存储引擎的范畴。就像红娘不再数“我爱旅行”说了几遍,而是看说到第几句就不再心动。
Q4. 文档长度归一化中,avgdl 表示什么意思?
- A.全部文档字数总和
- B.文档集合平均长度← 正确答案
- C.查询关键词总长度
- D.索引分片的个数
答案:B。avgdl 是 average document length 的缩写,即文档集合的平均长度,长度归一化以它为基准:当前文档长度与它相比,得出“偏长还是偏短”。A 是总长度不是平均,C 是查询维度,D 是分布式存储概念,都与 avgdl 无关。
Q5. 一个关键词在短文档里出现一次,比在长文档里出现一次更相关,主要靠哪个机制实现?
- A.逆文档频率 IDF
- B.字段长度归一化← 正确答案
- C.词频饱和曲线
- D.查询权重提升
答案:B。短文档出现一次关键词“浓度高”,靠的是长度归一化:同样的词频除以更短的长度,得分占比更大。A 管的是词的全局稀有度,C 管重复次数的边际递减,D 是字段层面的权重,都不负责“同样一次在不同长度文档里的价值差”。
Q6. 哪些操作可以调整搜索结果的相关性排序?
- A.调整 k1 和 b 参数← 正确答案
- B.给字段设置不同 boost← 正确答案
- C.使用 function_score← 正确答案
- D.修改索引分片数
答案:ABC。k1 和 b 是 BM25 固有参数,字段 boost 是多字段打分的权重,function_score 是叠加自定义因子,三者的修改都会直接改变分数。而 D 修改分片数只影响存储分布与并发性能,不改变打分公式。就像红娘改的是评分标准,而不是档案柜的摆放方式。
Q7. 关于 BM25 的 IDF 项,以下哪个说法正确?
- A.罕见词的 IDF 通常更高← 正确答案
- B.高频词的 IDF 必然为负
- C.IDF 与文档集合无关
- D.IDF 恒定等于 1.0
答案:A。文档频率越低,IDF 越高,罕见词区分度大。B 错误,高频词 IDF 趋近于 0 但不会轻易为负,即使为负也只是个平滑信号;C 明显错误,IDF 依赖整个文档集合统计;D 更离谱,IDF 随词的稀有度变化。就像红娘眼里,“会开挖掘机”这种冷门要求一旦匹配,身份的稀缺感格外强。
Q8. function_score 中 gauss 衰减函数最适合什么场景?
- A.按距离或时间衰减加权← 正确答案
- B.精确匹配后固定加分
- C.对词频做饱和压缩
- D.把分数替换成常数
答案:A。gauss 衰减函数按数值字段(时间、位置、价格)离理想点的距离平滑降分,越接近越相关,适合“刚发布的新闻优先”“离用户近的门店优先”这类场景。B 固定加分是 weight 的职责,C 属于 k1 做的事情,D 是 replace 模式而非衰减函数的能力。
Q9. BM25 对一篇文档打分时,结果可能受哪些因素影响?
- A.词频与文档长度← 正确答案
- B.词的文档频率← 正确答案
- C.集合平均长度← 正确答案
- D.硬件内存大小
答案:ABC。词频、文档长度、文档频率、集合平均长度全部是 BM25 公式的输入变量。D 硬件内存只影响查询性能,不进入打分公式。就像红娘的评分只取决于档案内容和她的评分标准,与办公桌抽屉有多大无关。
Q10. 想让“标题”字段的匹配对排序贡献更大,最直接的方式是?
- A.给字段设置更大 boost← 正确答案
- B.删除其他字段
- C.降低 k1 参数
- D.提高 b 参数值
答案:A。给标题字段设更大的 boost,会直接放大标题匹配的分数贡献,这是多字段打分最常用的手段。B 删除字段会丢失召回信息;C 的 k1 是全局词频饱和参数,不针对具体字段;D 的 b 是长度惩罚参数,与“标题更值钱”这个诉求无关。