中文搜索相关度排序:从BM25到语义混合排序的工程实践
你有没有在搜索日志里看到过这样的词用户搜“苹果最新款”结果点进去的是水果礼盒商品页。这不是段子是中文场景相关度排序里每天都在发生的经典翻车现场。原因也很简单——传统的关键词匹配只认字面不认意图而中文恰恰是一门“同一串字符、多套语义”的语言。这篇内容想跟你聊的就是2026年中文场景下相关度排序的核心技术思路从为什么简单匹配不够用到语义模型怎么落地再到字面召回和语义召回如何混合排序整体都是基于我实际做过、也踩过坑的方案总结适合正在做搜索排序、推荐排序、企业知识库检索的开发者参考。我先把结论放在前面中文相关度排序在2026年已经不是“要不要用语义模型”的问题而是“怎么把字面匹配和语义匹配有分寸地组合起来”的问题。纯靠BM25一类的词法匹配会被同义词、口语化表达、否定逻辑打得毫无还手之力纯靠向量模型又会在数字型号、专有名词、长尾实体上翻车。真正稳定可落地的方案是让两条路并行召回再用一套融合排序机制把双方的优势接住。下面我会按“问题分析—技术选型—实操搭建—评估踩坑”这条线一步步展开。1. 先别急着调模型中文场景下简单匹配的失效现场1.1 为什么“分词关键词权重”在中文里经常翻车简单匹配的核心逻辑是把用户输入的查询语句切分成词再给每个词算权重最后把包含这些词且权重高的文档排到前面。英文场景里这套逻辑还算抗打毕竟单词之间有天然空格形态变化也能靠词干还原处理。但中文里换个说法意思就完全变了“降噪耳机 通勤 地铁 不掉”这个查询用户真正想表达的是“戴在耳朵上不容易掉”传统倒排索引却很可能只看到“通勤”“地铁”这类强词忽略“不掉”这个意图核心。更麻烦的是字面匹配无法处理同义改写和口语化表达。用户搜“本子”可能指笔记本也可能指漫画单行本搜“充电口松了”可能描述的是硬件故障也可能是想找转接头。这种语义层面的事靠维护同义词表可以解决一部分但同义词表永远跟不上用户玩梗的速度。我见过不少团队把大量人力投入到扩充同义词和纠错词表中最后表越来越大、维护越来越重召回效果却依然顾此失彼。还有一个容易翻车的是否定逻辑和比较逻辑。“辣和不辣”“便宜和更便宜”字面上就差一个字但相关结果几乎完全相反。用户搜“不要苹果系统”的时候如果系统把“苹果”当成强关键词那召回结果基本就是灾难性的。这一类问题单纯靠调整词的权重没有出路因为问题根本不在于词本身而在于词在句子结构里扮演的角色。1.2 相关度排序不等于召回先分清要解决的是哪一段很多刚接触这块的开发者会把“召回率不高”和“排序不准”混为一谈然后一股脑去调模型结果问题没解决线上还变慢了。这里我觉得有必要先把链路拆开召回的职责是从全量文档里快速找出一个几百到几千规模的小候选集重点看“有没有”排序的职责则是在这个候选集上重新计算谁更相关重点看“准不准”。中文场景相关度排序真正要死磕的是排序这一段。举个例子用户搜“降噪耳机”召回环节用关键词就能轻松找出上万条相关商品但真正让用户满意的其实是排序结果里前十个商品合不合口味。这个时候模型要能判断“主动降噪”比“被动降噪”更契合“佩戴稳固”比“外观好看”的权重更高这些判断如果只靠字面匹配完全做不出来。所以做这个方向的第一步不是什么高端模型而是先把团队目标对齐我们到底是在优化哪一段如果是为了减少无结果率那是召回和索引的问题如果是为了提升点击和转化那核心是在候选集基础上做精确排序。想清楚这一点后面选模型、做数据、定评估指标都不用走弯路。2. 2026年技术选型从字面匹配到语义排序的三步演进2.1 词法匹配为什么至今没被淘汰先把BM25摆出来说清楚。BM25的核心思想并不复杂如果一个词在某个文档里出现得多同时在全局文档里出现得少那这个文档很可能和查询相关。它用词频和逆文档频率的组合来给文档打分还通过两个参数控制了词频饱和度和文档长度的影响这两个参数在经典实现里一般取k11.2到2.0、b0.75左右。中文场景用BM25前置工作通常是分词和文本归一化——简体繁体转换、全半角转换、大小写统一这些不做干净再好的算法也白搭。那为什么到现在BM25还没被淘汰因为它有几个不可替代的优势。第一是快纯词法匹配在索引结构下可以做到毫秒级响应这对线上系统极其重要。第二是稳它不会出现“意思对但字完全不对”的离谱召回比如用户搜“iPhone 15 手机壳”BM25一定能把带“15”字样的候选翻出来而纯向量模型在这种数字约束上反而容易飘。第三是可控运营团队可以明确知道某个词被什么逻辑召回排查bad case的成本低。所以我对词法匹配的态度一直是它不是要被替代的东西而是整个系统的地基。2026年了该用的倒排索引还是要用BM25还是召回阶段最可靠的底座之一。问题只在于不能让它单独扛全部排序职责。2.2 语义向量的优势与边界语义向量匹配的基本思路是把查询和文档分别映射到一个高维向量空间里用余弦相似度这类度量来判断相关性。两个文本哪怕没有一个词是重合的只要语义接近向量距离也会很近。这个特性正好补上了BM25最弱的短板同义词、口语化、句子改写。用户搜“通勤适合戴的耳机”文档写的是“坐地铁轻巧不易掉落”字面上几乎没有交集但语义模型能算出很高的相关度这在纯词法时代是不可想象的。但向量模型也绝对不是什么万能药。我自己的使用体验是语义模型的上限很大程度由训练数据决定你拿通用语料训出来的模型放到垂直行业里效果会明显打折。比如在一个医疗器械知识库里用户搜“血糖仪试纸”如果训练语料里没有足够多的医疗器械术语模型可能把“血糖仪”理解成泛泛的“检测设备”召回的精度远不如老老实实的字面匹配。另外向量模型对数字、型号、年份这类精确实体的感知能力比较弱用户搜“thinkpad x1 carbon 2026 款”语义模型很容易把注意力放在“ThinkPad”和“笔记本”上而忽略“2026”。这个问题不是换个更大的模型就能自动解决的需要在数据层面专门做约束。所以我的建议是把向量模型当成一个“更懂人话”的优化组件而不是替代性的全部方案。它负责把字面之外的相关性找回来但字面硬约束还是得靠词法匹配兜底。两个召回通道并行后续融合排序才是当前性价比最高的架构。匹配方式优势中文场景下的短板成本BM25词法匹配快、稳、可控能精确命中数字和专有名词无法理解同义改写、口语化、否定逻辑索引维护和分词成本低语义向量匹配能处理语义相近但字面不同的文本精确实体感知弱、长尾领域容易飘需要微调、向量索引GPU开销高混合排序兼顾字面硬约束和语义泛化两头都沾工程复杂度明显上升需要两套召回通道和统一排序框架2.3 混合排序什么场景下必须融合那什么样的场景必须要走混合排序我判断有两个硬指标一是候选文档集规模大且语义分散比如电商全品类、新闻资讯全站二是用户查询里有大量口语化、同义改写表达比如面向消费者的应用。这俩条件只要满足一个单靠BM25或单靠向量模型都会有明显缺陷融合带来的收益会非常可观。融合的做法也不止一种。简单一点的是给两路召回结果分别打分再做加权求和麻烦一点的是把两路结果的位置信息丢给一个融合公式比如 RRFReciprocal Rank Fusion。RRF的公式是score Σ 1 / (k rank)其中k是个常数常用60rank是文档在某一路召回结果里的名次。它最大的好处是不需要把BM25分数和余弦相似度统一到同一个量纲直接用排名做融合规避了分数归一化带来的一堆麻烦而且实测效果稳定。我在这块的建议是不要上来就追求复杂的融合模型先跑RRF基线。RRF虽然简单但在绝大多数场景下已经能赢过精心调权的加权融合而且它几乎不消耗额外算力。把RRF作为基线跑通之后再去尝试用学习排序模型对融合后的Top结果做重排效果会进一步提升。注意这里“融合”和“重排”是两回事前者负责把召回通道合并出候选集后者负责在候选集内部重新精算相关度这两件事的职责别混淆。3. 手把手搭建一套中文相关度排序系统3.1 数据准备没有标注数据时怎么构造样本聊到实操很多人第一反应是“我没有标注数据怎么办”。其实中文场景下构造训练样本的路子非常多最常用的就是搜索点击日志。用户搜索一个查询点击了某个结果这个查询和文档的配对天然就是一个正样本同一次搜索里曝光但没有点击的结果可以拿来当负样本候选。需要注意点击日志要过滤掉那些明显无效的点击比如点击后在页面停留不到几秒就返回的这种多半是误触。负样本的构造比正样本更讲究。如果随机抽样做负样本模型很快就学会了“跟查询完全无关”的反例但当真正的困难样本出现时——比如一个查询下面有两个都匹配但用户只选了一个的文档——模型就分不清了。所以我强烈建议做困难负样本挖掘。一个很实用的做法是用BM25先召回复杂度得分高的文档然后人工或规则筛掉其中明显不相关的作为负样本加入训练集。这样模型被迫去学习“粗看相关但细看不对”的边界排序能力会有明显提升。# 困难负样本挖掘伪代码示例 def mine_hard_negatives(query, docs_pool, retriever, top_k20): candidate retriever.retrieve(query, docs_pool, top_ktop_k) negatives [] for doc in candidate: # 用规则或模型判断是否与query相关 if not is_relevant(query, doc): negatives.append(doc) return negatives我个人的做法会把数据分成三块点击日志里的真实配对、BM25挖掘的困难负样本、人工审核的少量精标数据。人工精标不需要太多500到1000条就能让模型在垂直方向上明显变乖关键是每条标注要认真别贪多。标注的时候也别只看“词面像不像”要看“人是否觉得这个结果有用”。这个标准直接决定了后面模型学出来的“相关观”是否贴合业务。3.2 模型微调中文语义模型训练的四个关键点有了数据之后模型微调就是核心环节。中文语义模型微调我总结下来有四个关键点值得盯住。第一是温度系数的设置。对比学习训练中温度系数控制了模型对困难样本的敏感程度温度太低模型会过于激进把差异很小的两个文本强行分开导致排序结果抖动温度太高则模型学不到细腻区别。我常用的初始值是0.05到0.1然后根据验证集上的NDCG变化逐步调整。这个听起来只是个小参数实际影响非常大值得专门做几组对照实验。第二是损失函数的选择。最常见的是InfoNCE这类对比损失把每个查询对应的正文档和一批负文档拉近推远。但要注意中文场景里“相关”不是绝对二分而是一个程度问题。如果有能力在对比损失基础上加一个排序损失会更好让模型学习“正文档比负文档更相关”的同时也能把握“相关也有高低”的程度差异。第三是领域自适应。通用中文语义模型在垂直领域里效果差是必然的因为预训练语料和你业务里的术语分布完全不同。我见过最好的做法是用业务领域的语料做增量预训练再用业务标注数据做微调。如果增量预训练成本太高至少也要把业务语料里的高频词、专有名词专门加入词表或构造专门的训练样本不然模型根本不认识这些词。第四是数据增强。利用大模型做同义改写可以低成本扩充正样本。但这里有个度的问题改写过头会引入与业务无关的表达反而把模型带偏。我做增强时一般会限定改写方向——只改表达方式不改实体词不做天马行空的扩展。否则模型学的不是“中文表达多样性”而是“瞎联想”。# 对比学习训练时的关键参数参考 temperature 0.07 batch_size 128 learning_rate 2e-5 max_seq_len 64 # InfoNCE损失的核心逻辑示意 def infonce_loss(query_vec, pos_doc_vec, neg_doc_vecs, temp): all_docs torch.cat([pos_doc_vec] neg_doc_vecs, dim0) logits torch.matmul(query_vec, all_docs.T) / temp labels torch.zeros(len(query_vec), dtypetorch.long) return torch.nn.functional.cross_entropy(logits, labels)3.3 融合排序与重排让结果既懂语义又稳得住关键词训练完模型接下来的动作就是搭建线上排序链路。我建议的默认结构是BM25召回一路向量召回一路两路都取Top100合并去重后进入融合层。融合层先用RRF算出一个粗排分然后对Top50再走精排层。精排层可以用一个轻量级的交叉编码器或者基于特征的学习排序模型在这个小集合上做精确打分最后输出排序结果。RRF融合实现的伪代码非常简单我给你写了个可以直接照抄的逻辑def rrf_score(rank, k60): return 1.0 / (k rank) def fuse_rankings(lexical_results, vector_results, k60): scores {} for rank, doc_id in enumerate(lexical_results, start1): scores[doc_id] scores.get(doc_id, 0) rrf_score(rank, k) for rank, doc_id in enumerate(vector_results, start1): scores[doc_id] scores.get(doc_id, 0) rrf_score(rank, k) return sorted(scores.items(), keylambda item: item[1], reverseTrue)但线上系统跟离线实验不完全一样工程上还有很多细节要处理。比如向量检索要用近邻索引常见实现有HNSW和IVF系列检索速度和处理精度要做一个权衡候选池上限不能设得太大否则精排的计算压力会拖垮延迟。我自己一般把向量召回的候选池上限控制在200到300精排候选控制在50到80线上平均耗时能控制在几十毫秒量级。如果精排层用的是特征排序模型那特征设计也很讲究。我的常用特征集包括BM25原始分、语义余弦相似度、标题命中词数、类目一致性、商品热度、新品因子。这里有个很容易踩的坑特征之间量纲差异很大语义相似度一般在0到1之间BM25分可能是几十分甚至几百分在训练排序模型时一定要做特征归一化否则高量纲特征会压制低量纲特征模型学来学去只盯着一个维度看。4. 评估与踩坑中文排序上线前最容易翻车的地方4.1 离线指标与线上指标怎么配合做排序改进评估是绝对不能含糊的环节。离线评估我一般看四个指标NDCG10、MRR、RecallK和P1。NDCG10能衡量排序的整段质量MRR更关心第一个正确答案的位置RecallK则检验召回通道有没有把该捞的货捞上来。如果要评估“用户最关心的那个结果有没有排前面”P1比NDCG直接得多。不过有一句话我一直挂在嘴边离线涨点线上翻车是排序项目里最常见的魔咒。原因在于离线数据集终究是静态的而线上用户的真实行为会受到标题吸引力、图片、价格、库存、个性化偏好等多重因素干扰。所以我始终要求所有排序改动必须走灰度实验拿点击率、无结果率、长尾查询满意度这类线上指标来说话。很多团队砍掉灰度这一步直接全量上线结果用户反馈炸了才发现离线评估覆盖不了真实场景。我踩过最典型的一个坑是离线NDCG涨了大约3个点但线上点击率反而降了。追溯原因发现模型把一些“语义相关但标题完全不吸引人”的文档排了上去用户看完摘要就不想点。这给我上了一课相关度排序的目标是“让用户在搜索结果里快速找到并愿意点击的内容”不只是“判断语义相关”。所以后来我的精排特征里一定会带上点击预估相关的特征比如历史点击率、同位置平均点击表现。排序模型的输入除了相关度还要有“吸引力”这个维度。4.2 高频问题与排查实录实操中遇到的问题五花八门我挑几个高频且容易被忽略的写在这里。这些问题都不是理论推演是我自己做项目时遇到过的真实情况。问题现象可能原因排查思路与解决方案搜“苹果”时水果和手机结果混杂一词多义向量模型分不清上下文加入类目特征或多意图识别模块根据查询上下文区分对高频歧义词单独做意图路由搜“手机 2026款 价格”时型号匹配不准向量模型对数字年份感知弱字面召回兜底保留BM25通道在训练数据里增加带数字实体的配对样本搜“降噪耳机 不掉”召不回“佩戴稳固”的文档训练数据里缺少口语化表达用大模型做受限同义改写扩充数据或人工构造口语化模板搜“不要红色的手机壳”返回大量红色壳否定逻辑没学会增加否定句式样本或在前处理层做否定词检测对否定意图单独处理向量召回的结果排序抖动剧烈温度系数太低模型对细微差异过于敏感调高温度系数重新在验证集上评估同时看是不是困难负样本挖掘做得太凶导致训练方差变大另外还有一个经常被忽略的问题是训练数据里的标签噪声。点击日志本身有不少“误点”和“为了比价而点”的行为这些数据直接训练会污染模型。我的做法是在数据清洗阶段严格过滤掉低质量点击并且对剩下的数据做一次置信度打分大部分样本权重调低只有高置信度的样本才让模型重点学习。长尾查询也是一个老大难。中文搜索里用户的表达千奇百怪“便宜点的手机待机久”这种查询别说BM25语义模型如果没有在长尾数据上见过类似表达一样会懵。对此我三个建议一类长尾集中在高频分支词上做数据增强二类靠向量模型的泛化能力兜底三类确保线上日志里持续回流新查询定期迭代二次训练。最后再分享一个我自己的体会。做中文场景下的相关度排序最忌讳的是把模型当成“唯一正确答案”。我见过太多项目把全部精力投在改模型结构上而忽略了数据质量、特征设计和两路召回的平衡。实际上我最后那版效果最稳定的方案模型结构就是非常普通的双塔加交叉重排真正下功夫的是数据清洗和困难负样本挖掘。你可以在后面根据自己的业务场景继续扩展——比如给向量召回加上个性化向量偏置或者在精排模型里融入用户长期行为特征。这些方向都是在今天这套地基上长出来的先把地基打牢后面的迭代才有意义。