关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

中文站内搜索为什么总搜不准:分词、召回与排序三段各自的账

发布时间:2026-10-29

搜不到的那个词明明就在页面上,问题却不在同一层

客服转来一张截图:用户在站内搜了一个词,结果页一条都没有。运营把那篇内容打开一查,词就写在正文第二段里,标题里没有、正文里确实有。换个说法再搜一次,这回搜出来了,但用户想要的那篇排在第十页。

这两句抱怨经常出现在同一张工单里,也经常被当成同一件事处理:加权重、提热度、把几篇文档手工置顶。做了一轮之后回头看,用户反馈没什么变化。

它们其实是两件事。前一句是期望的那篇文档没有进入结果集合,后一句是它进了集合但没有排到前面。前者发生在召回层,后者发生在排序层。排序层的所有手段都只在集合内部调整先后,没有能力把一个不在集合里的文档拉进来。定位顺序错了,工作量就落在了不改变结果的那一段上。

本文要算的就是这笔账:三段各自负责什么、症状怎么区分、改哪一段的见效程度与代价如何。下面的判断不引用任何具体引擎的参数与实测数值,不同实现之间的差异不小,最终都要以自身数据与验证为准。

先把"搜不准"分成三段:召回、排序、呈现

一次查询从敲下回车到页面出现结果,中间有三道关口。每道关口漏掉的东西,后面一道补不回来。

第一道是召回:查询被切分之后拿到索引里去取文档列表,形成一个候选集合,它决定的是"有没有"。第二道是排序:候选集合按打分排出先后,它决定的是"谁在前"。第三道是呈现:靠前的文档取回原文,生成标题、高亮片段与摘要,它决定的是"用户信不信"。

呈现这一道最容易被忽略,影响却很直接。用户搜的词没有出现在高亮里,或者摘要是一句被截断的半句话,即使排序完全正确,用户也倾向于判定搜错了——他判断的依据就是眼前那几行字。

三段的依赖是单向的。召回没找到的文档,排序再准也无从下手;排序压在后面的文档,呈现做得再到位也很少被看到。这个顺序决定了排查的顺序:只能从最前面那一道开始问。

区分三段的方法不复杂。拿一条具体的"查询—期望结果"样本,先问期望的文档有没有进候选集合;进了,再问它排在第几位;排得靠前,再看高亮与摘要是不是让它看起来像一个答案。三个问题对应三段,答案落在哪一段,就先动哪一段。

还有一个前提:三段问题可以同时存在,也可以互相掩盖。排序把一堆不相关的文档排到了前面,看起来像排序问题,实际是召回带进来的噪音;召回漏了一篇关键文档,用户换词搜到后把它排在第一位,看起来一切正常,实际是查询侧切分与索引侧不一致。不做逐条定位,就只能看到表面那一层。

第一段:中文分词与词典,粒度决定召回边界

英文检索可以先按空格把句子分成词,中文没有天然的词语边界,一串汉字在哪里断开需要依靠规则。这是中文检索在第一步就与英文分岔的地方,也是很多零结果问题的根源所在。

常见的切分思路大致分两类。一类基于词典:拿一份词表去匹配文本,匹配上了就算一个词。一类基于统计:用语料中统计出来的共现信息判断哪些字倾向于组合在一起。两类思路各有长短,实际系统里混用也很常见,但无论用哪一类,都要面对同一个问题——粒度。

粒度切得细,长词被拆成多个短词,用户搜其中任何一部分都能命中,召回变广,代价是噪音变多:搜一个完整词,可能把只含有其中一部分的文档一起带进来。粒度切得粗,词更完整、更准,代价是漏:用户搜的是一个长词的前半段,而索引里存的是整词,两者对不上,结果就是零。

中文分词还有两类典型难点。交集型歧义指同一串字可以有多种切法,相邻的两个词共享了中间的字,切法不同得到的词项就不同。组合型歧义指几个字单独成词、合起来也成词,切与不切直接影响后续匹配。两者带来的后果是一样的:索引侧切出来的词项,可能与查询侧切出来的词项对不上。

由此得出一条硬要求:索引时用的切分规则与查询时用的切分规则必须一致。同一个词在两边被切成不同的东西,文档里明明有,查询却命中不了,表现出来的症状就是"什么都没搜到"。检查这一项,通常比调整任何权重都更接近问题的真实位置。

词典也不是一次配好就结束的东西。新产品名、新型号、缩写、行业说法会持续出现,词典没收录的词要么被切碎,要么退化成单字序列,匹配行为变得难以预期。维护词典的人力是这一段最容易被低估的代价,它不体现在某一次调参上,而是长期持续占用。

第二段:召回策略与布尔组合,哪些查询更贵

倒排索引的基本结构是"词项到文档列表"的映射:每个词项后面挂着包含它的文档编号,还常常附带位置信息。查询到达后,把相关词项的文档列表取出来做交、并、补,就得到候选集合。

除了这层映射,索引还要保存文档的字段数据,用途不同,成本也不同。一个字段可以设为参与检索、仅用于过滤、仅用于展示,或者参与排序。参与检索的字段要建倒排,是最贵的;仅用于过滤的字段只需能快速判断取值;仅用于展示的字段只要能随文档取回;参与排序的字段需要在打分时能快速读到值。把不需要检索的字段设成参与检索,索引体积与写入开销一起上升,查询能力却未必提升;把需要检索的字段设成仅展示,用户就会遇到"内容在页面上,但怎么都搜不到"。

召回策略本身的价格差别也很明显。单词项匹配最省,取一个列表即可。短语匹配要求位置信息,要把多个词项的列表按位置对齐,开销高出一截。前缀与模糊匹配要在词典里遍历更多候选词项,候选一多,参与运算的文档列表也多,交并的工作量随之上升。

多字段查询的逻辑组合直接改变候选集合的大小。用"必须"连接会缩小集合,用"应该"连接会扩大集合并影响后续打分,用"排除"连接是先算进来再剔除。组合写复杂之后,集合大小与计算量都不好预估,最常见的后果是条件过严导致零结果,而过松导致候选集合膨胀、排序压力被推到下一层。

在把这类检索负载落到机器上时,一万网络在整合自营节点与 BGP 多线资源过程中,通常把索引常驻部分的容量与重建窗口并入配置清单一起核:这一段表面上是算法问题,落到机器上决定的是要留多少内存给索引、重建期间要预留多少余量。

这一段还有一种常见误判:把过滤条件写成了检索条件。分类、状态、时间范围这些字段,用户不会把它们当作关键词输入,它们的作用是缩小范围;把它们的取值当词项去匹配,既浪费了倒排,又让打分被无关词项干扰。判断标准很朴素:用户会不会主动把这个值敲进搜索框。会,就参与检索;不会,就只做过滤。

第三段:排序权重与时间衰减,怎么调才不靠拍脑袋

排序通常分两层。底层是统计打分,用词频与逆文档频率这一类思路给出基础相关性:一个词在这篇文档里出现得多、在整个文档集合里出现得少,它对这篇文档的贡献就大。TF-IDF 与 BM25 属于这一类思路,各家实现的公式与参数不同,这里不展开具体形式。

上层是规则叠加。字段权重让标题命中的文档高于正文命中的;业务权重让某些来源或某些类型的内容整体靠前;时间衰减让新内容不至于被历史内容压住;热度把被点击多的文档往上提。这些规则注入的是业务意图,不是相关性本身,它们与底层统计打分是相加关系,不是替代关系。

这一层最容易出现的问题是权重退化成一堆没有解释的数字。每加一项权重,都应当能回答两个问题:它想让哪一类文档往上走,它因此会把哪一类文档压下去。回答不了这两个问题,调整就只能是试,而试出来的结果在换一批数据之后通常不再成立。

时间衰减是最典型的例子。它的取法取决于内容性质:时效性强的内容,衰减应当明显,否则旧内容会长期占据前排;长期有效的文档,衰减应当平缓甚至不加,否则被反复检索的知识类内容会被新但质量一般的内容挤下去。同一个衰减强度套在两类内容上,必然有一边吃亏,而吃亏的那一侧往往不会有人来反馈。

让调参脱离拍脑袋的办法只有一条:准备一组标注过的"查询—期望结果"样本,每改一次权重就重跑同一组样本,看期望结果落在第几位、前若干条的命中情况如何变化。样本量不必很大,但必须是同一组,每次都要跑。有了这条基线,"感觉变好了"才有可以对照的地方,也能在改坏之后立刻发现。

还要接受一个事实:排序层能解决的问题,上限由召回层给定。候选集合里没有的文档,再怎么加权也出不来;候选集合里全是噪音,再怎么调也只能是矮子里拔将军。因此当样本显示期望结果压根没进集合时,继续调权重不产生任何收益。

查询改写:同义词、缩写、错字与拼音的维护成本

查询改写是把用户的输入变成系统更容易处理的形式,常用手段有三类。同义词与近义表达把一种说法映射到另一种;缩写与别名把简称、别名、型号简写映射到正式名称;错字与拼音容错把打错的、用拼音代替的输入映射到正确词形。

改写可以放在查询侧,也可以放在索引侧。查询侧是在拿到用户输入之后,按词典把它展开成多个词项再去检索;索引侧是在建索引时就把扩展词一并写进去,文档因此携带了它原文里没有的词项。

两种位置的成本结构不同。查询侧改动即时生效,词典可以频繁调整,代价是每次查询都要做一次展开,热门词展开后候选集合变大,召回变广的同时精度会下降。索引侧查询更省,代价是词典一改就要重索引受影响的文档,改动生效慢,而且索引体积会随之变大。

更常见的做法是分层放置。稳定的、不常变动的映射,比如行业通用别名、正式名与简称,放索引侧,一次写入长期受益;易变的、需要立刻见效的映射,比如临时活动名、刚上市的型号,放查询侧,改完马上能用。错字与拼音容错通常也放查询侧,因为这类映射的维护频率高,不值得每次都触发重索引。

改写同样要防止过度。同义词展开得越多,召回越广,精度越低;一个词被映射到不相关的义项上,比不映射更难被发现——零结果是显眼的,会立刻被投诉,不相关的结果是沉默的,只在用户放弃搜索时才体现出来。给每条映射保留来源记录与下线开关,是这一段必要的成本。

呈现层:高亮、摘要与深翻页的隐藏代价

高亮需要在结果里标出用户搜的词,这要求索引里保留原始文本,或者至少保留词项的位置偏移。只存词项不存位置的字段能参与打分,做不了高亮;连原文都不存的字段,摘要也取不出来。所以"用于展示的字段要不要存原文"必须在建索引时就回答,事后补只能重索引。

摘要通常是取原文中包含命中词的片段,并让命中词落在片段中间。它看似只是显示问题,实际影响用户对结果的信任:标题里没有搜的词、摘要里也没有,用户会认为这一条不相关,即便它排在第一位。呈现层因此不是纯粹的装饰,它参与用户对相关性的判断。

深翻页是这一段代价最陡的一项。偏移量很大的翻页需要先算出并跳过前面所有结果,代价随偏移量增长;翻到很后面时,用户付出的等待与系统付出的计算都不划算。常见的替代办法是用游标记录上一页的位置,或者先对结果做条件过滤再翻页,把翻页深度压在可接受范围内。

索引与更新也在这里收口。常驻内存的部分影响查询延迟;段合并与索引重建会带来磁盘与瞬时资源开销;数据写入到可被检索到之间存在可见性延迟,刷新间隔越短,写入吞吐的负担越重。增量更新适合日常节奏,全量重建适合结构或词典发生较大变化的窗口,两者各有适用的时机。具体的间隔与窗口取决于数据规模与业务对新内容的容忍度,不存在通用数值。

环节 常见症状 主要变量 改这里的见效程度 付出的代价
分词与词典 正文里确实存在的词搜不到;换个说法就能搜到;零结果集中在长词与新品名 切分粒度、词典覆盖面、索引侧与查询侧是否用同一套切分规则 高,直接决定文档能不能被找出来,是零结果类问题的第一现场 改词典通常要重索引;词典需要长期人力维护;切细之后噪音同步增加
召回策略与布尔组合 条件一多就零结果;短语搜不到但拆成单个词能搜到;结果忽多忽少 字段角色设定、是否要求位置信息、必须/应该/排除的组合方式 中高,能同时改集合大小与精度,但见效范围受字段设计约束 短语与模糊匹配更贵;字段设错会导致索引体积上升或能力缺失
排序权重与时间衰减 文档找得到但排在第十页;前几条总是不相干;新内容或旧内容总有一边吃亏 词频与逆文档频率类的基础打分、字段权重、业务权重、衰减强度 中,只在候选集合内起作用,召回漏掉的文档无法通过它挽回 权重缺乏解释会变成碰运气;必须配套回归样本才能判断改好还是改坏
查询改写(同义词与纠错) 用户说法与文档用词不一致;简称搜不到、打错字搜不到、用拼音搜不到 改写位置(查询侧或索引侧)、词典规模、展开后的候选数量 中,对说法不一致类问题见效快,对错别字与拼音容错直接有效 查询侧每次查询都要展开;索引侧改词典要重索引;过度展开会稀释精度
高亮摘要与深翻页 结果排对了但用户认为不相关;高亮里没有搜的词;翻到后面明显变慢 是否存原文与位置偏移、摘要取片段的方式、翻页用偏移量还是游标 低到中,不改结果集合,只影响用户对既有结果的判断与等待感受 存原文与位置会增大索引;深翻页代价随偏移量增长;改动往往要重索引

本文只讨论面向用户的中文文本检索,不涉及日志检索平台与向量召回

需要把边界说清楚。本文谈的是面向用户的中文文本检索:用户输入词或短语,系统在文档集合里找出相关内容并展示出来,场景包括电商站内搜索、文档与知识库检索、内容站点的站内查找、后台订单与客户的模糊查询。讨论的重心是质量问题的定位顺序与改动代价,不是引擎选型。

刻意排除的有两块。一是日志检索平台:它面对的是机器生成的结构化与半结构化数据,查询形态以过滤、聚合、时间范围扫描为主,用户是运维与开发人员,相关性的含义与站内搜索完全不同,把它拉进来比较会偏离这里的问题。二是向量召回:本文只在结尾处一句带过,不展开实现细节。

还有一块不展开:不同检索引擎的具体参数与内部实现。词典格式、打分公式的具体形式、段合并的触发条件、刷新间隔的默认值,各家差异很大,本文一律不给出具体数值。读者要做的是在自己的数据上验证,别人的结论与默认配置都不能直接照搬。

六步定位与回归样本

把前面的判断收成一份可以照着做的流程。六步的核心不是改,而是先确定改哪里。

第一步,收集真实的失败查询。三个来源最有效:零结果的查询日志、有结果但点击率明显偏低的查询、客服与运营转来的工单。凭印象列的查询往往集中在少数热门词,覆盖不到真正的长尾,而长尾恰恰是召回问题的高发区。

第二步,给每条查询标注期望结果。标注内容包括:期望出现在前面的文档、可以接受的名次区间、这条查询来自哪个场景。名次区间比单一目标更实用,因为多数业务能容忍"前三",不能容忍"第三页"。

第三步,逐条判定问题段落。对每条样本依次问三个问题:期望文档有没有进候选集合;进之后排在第几位;排得靠前时高亮与摘要是否正常。三个答案分别对应召回、排序、呈现。这一步要实际去查,不要凭结果页猜。

第四步,归类后看占比。把样本按段落归类,哪一段占比最高就从哪一段动手。如果召回类占了大头,先调排序权重几乎没有收益;如果排序类占了大头,改词典就属于投入错了地方。这一步决定了后面所有工作的方向。

第五步,一次只改一段,改完重跑样本。同时改分词与权重,结果变好时无法归因,结果变差时也无法定位。改动的单位越小,样本给出的反馈越干净。

第六步,记录与回滚。每次改动记下改了什么、样本结果如何变化、上线时间。样本显示没有改善的改动应当回滚,而不是继续叠加新的权重去抵消。长期积累的记录本身就是资产:它能告诉你这一类业务的问题通常落在哪一段。

回归样本的构成不必复杂,但要固定。它包含查询词、期望文档、可接受名次区间、问题段落归类四项即可,规模可以从几十条起步,覆盖不同查询类型——单词、短语、简称、含错字的输入、跨字段的查询。每次调参后跑同一组,观察召回情况与前若干条的命中变化,而不是凭感觉判断。

关于分词、召回与排序的七个疑问

"搜不到"和"排在后面",哪一种更常见也更该先解决?从影响上看,"搜不到"更该先解决。排在后面的文档至少还在结果集合里,用户翻几页或者通过筛选条件还能找到;召回漏掉的文档是彻底不存在的,用户只会得到零结果或者一堆替代品,而且没有任何后续手段能补救。实际操作上也必须先解决它:召回没进集合的文档,排序层的任何调整都作用于集合内部,无法把它拉进来。定位时先拿样本去问"期望文档在不在候选集合里",这一步能省掉大量无效的权重调整。

分词调细是不是就一定更准?不是。切细带来的是召回变广,准确的方向是"更不容易漏",代价是"更容易带进噪音"。搜一个完整词,粒度细的系统可能把只含有其中一部分的文档一起召回,前排被稀释,看起来就是结果变多了但更不相关。反过来切粗则是更准但更容易漏,长词的前半段搜不到整词。粒度本身没有优劣,要看业务更怕哪一种:电商里漏掉一个商品等于丢单,噪音可以用排序压一压;文档检索里噪音会直接毁掉信任,宁可漏也要准。确定的做法是先定业务取向,再定粒度,并且保证索引侧与查询侧用同一套规则。

同义词与纠错放在查询侧还是索引侧?取决于这条映射的变动频率。稳定的映射放索引侧,一次写入长期受益,查询时不额外展开,代价是索引体积变大、改词典要重索引。易变的映射放查询侧,改完立刻生效,代价是每次查询都要展开一次、热门词展开后候选集合膨胀。错字与拼音容错一般放查询侧,因为这类映射维护频繁,不值得每次都触发重索引。实操里更常见的是分层:行业通用别名与简称放索引侧,临时活动名与新型号放查询侧。无论放在哪一侧,都要给映射保留来源记录与下线开关,否则出错的映射很难被及时发现。

高亮为什么需要额外的字段设计?因为高亮依赖两类信息,而它们都不是默认就有的:一是原始文本,用于截取片段;二是词项在原文中的位置偏移,用于定位标红的区间。只存词项不存位置的字段能参与打分,做不了高亮;连原文都不存的字段,摘要也取不出来。因此"用于展示的字段要不要存原文与位置"是一个建索引时就必须回答的问题,事后补只能重索引。代价是索引体积上升,收益是结果页能让人一眼看出为什么命中。这个取舍没有标准答案,取决于结果页要不要展示摘要与高亮。

深翻页为什么会变慢,怎么替代?偏移量式翻页需要先确定并跳过前面所有结果,偏移量越大,要跳过的部分越多,代价随偏移量增长;翻到很后面时,计算量与等待时间都不划算。替代方式有两种常见做法:一是用游标记录上一页末尾的位置,下一页从这个位置继续取,翻页代价基本恒定;二是先搜索后过滤,把条件尽量前置,让真正需要翻页的深度压在可接受范围内。此外还要考虑一个产品层面的判断:绝大多数业务里,用户翻到很深的位置本身就说明前几页没给出答案,与其优化深翻页,不如回去看召回与排序。

排序权重怎么调才不靠拍脑袋?两条一起做才有效。一是每条权重都要能解释:它想让哪一类文档往上走,会因此把哪一类文档压下去,回答不了就先不要加。二是准备一组标注过的"查询—期望结果"样本,每次调完重跑同一组,看期望结果落在第几位、前若干条的命中情况如何变化。样本不必大,但必须固定,且每次都跑同一组,否则无法区分这次变化是改动带来的还是数据波动。此外一次只改一处,改两处就无法归因。做到这三条,权重调整才从试错变成有对照的实验。

索引多久重建一次、能不能只做增量?没有通用数值,取法取决于数据规模与业务对新内容的容忍度。增量更新适合日常节奏,写入后立即或很快可见,代价是段会越来越多,需要靠段合并维持查询效率,而合并本身带来磁盘与瞬时资源开销。全量重建适合词典或字段结构发生较大变化的窗口,比如切分规则改了、字段角色调整了、扩展词表大规模更新——这类改动不重建就不会生效。判断依据是改动是否影响已存文档的表示形式:只增删文档,用增量;改了文档如何被表示,才需要重建。刷新间隔与写入吞吐之间存在取舍,间隔越短可见性越好、写入负担越重,具体取值要在自己的数据上验证。

文本检索之外,混合检索与向量召回本文没有展开

检索领域还有一大块本文刻意没有触碰:把基于词项的稀疏检索与基于向量的稠密检索结合起来的混合检索。它要解决的问题与本文不同——不是"词项对不上",而是"意思对得上但用词完全不同",比如用户描述一个需求而不使用文档中出现的词。这条路线的加入会改变整篇的账:它需要额外的模型推理与向量索引,代价结构与本文讨论的三段都不相同,因此需要单独讨论。

另外几块也一并留在本文之外:多语言与跨语言检索、拼写纠正之外的查询意图识别、搜索建议与自动补全的排序、个性化与用户画像对结果的影响、结果的多样性控制(避免前排被同一来源占满)。它们都建立在三段划分之上,但各自引入了新的变量,混在定位流程里讲会让判断失去焦点。

一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。在为搜索类负载配机器时,可把索引内存、重建窗口与查询并发并入服务器租用与机柜托管的配置清单一起核。

回到中文搜索:把三段问题分开定位再动手

回到开篇那张截图:搜不到,是因为查询侧与索引侧切出来的词项对不上,或者该字段根本没被设成参与检索;搜到了但排在第十页,是因为候选集合里挤进了太多噪音,而期望的那篇被压在下面。两句话听起来像同一个抱怨,动手的地方完全不同。

值得记住的次序是:先看召回,再看排序,最后看呈现。召回决定文档在不在集合里,排序决定它在集合里的位置,呈现决定用户是否相信这个位置。顺序反过来,就会在不会改变结果的那一段上反复投入。

具体到操作,可以把它压缩成一张三行的表写进项目文档:这一段负责什么、症状长什么样、改它要付出什么代价。每次接到"搜索不准"的反馈,先拿样本去问三个问题,把答案填进表里再决定动哪一段。三个答案落定,大半的争议也就落定了,剩下的是代价选择,而代价是可以在讨论桌上明算的账。

表里的具体数值与配置,一律以自身数据与验证为准。不同检索引擎的实现差异很大,官方说明与自己跑出来的样本结果才是依据,别处的结论只能作为参考线索。


上一篇:实时地理围栏为什么会把机器压垮:Tile38 里对象类型、查询方式与回调各自吃什么资源

下一篇:2026 OpenCost 与 Kubecost 服务器租用配置手册:K8s 成本分摊采集量与留存怎么算 + 避坑全解