ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自进化搜索内核设计:舍弃向量数据库的稀疏检索与反馈闭环实践

自进化搜索内核设计:舍弃向量数据库的稀疏检索与反馈闭环实践

搜索这个领域,平时看着挺简单,真自己从零搭一套,坑多到数不过来。我这边维护的内部搜索内核Sirchmunk,最近终于跑到了稳定可用的状态,想把这套设计思路拆出来聊聊。先说结论:这个项目从一开始就没有用向量数据库,全程走稀疏检索加在线反馈闭环,靠一套叫做“自进化”的机制持续调优。它对标的问题是:团队内部文档、工单、代码片段这些中长尾内容越来越多,传统Lucene系检索在语义匹配上吃力,但直接上一套向量方案又觉得成本高、运维重、结果不可解释。这篇文章不聊概念,只讲架构和取舍,适合正在设计搜索系统、或者想优化现有检索效果的同学参考。

1. 设计拆解:为什么把“向量数据库”排除在方案外

很多搜索项目在立项时会惯性思维地认为,效果不好就是没上向量。实际上在Sirchmunk的选型讨论中,我们列过一版很现实的需求清单,反而不太支持向量方案。这个决策过程值得先讲清楚,因为后面所有结构设计都建立在这个取舍之上。

1.1 搜索场景的真实约束

先说数据形态。项目要搜的东西很广:运维操作手册、会议纪要、代码注释、历史工单,甚至偶尔有扫描件OCR出来的文本。这些数据的特点是长度不均、质量参差、主题分散。如果做向量索引,首先要过embedding模型,但内容里大量是缩写、内部代号、命令片段,通用模型对这些粒度的语义理解其实并不稳定。

再说延迟预算。内网搜索虽然并发不高,但内部用户对等待非常敏感。习惯了主流搜索引擎的毫秒级反馈,如果一次查询要几百毫秒甚至一秒以上,基本就被判定为不好用。纯向量检索在粗排阶段还好说,但一旦要加过滤条件、做聚合统计、按时间或部门范围圈定结果,向量方案的过滤能力往往需要额外对齐,流程变长。

还有一个被经常忽略的点:结果可解释性。搜索最终要面对用户,推荐结果如果是个黑盒,出了badcase几乎没法定位。向量检索给出“语义上相似”的结论,但用户问“为什么搜到了它”,我们无话可说;倒排索引至少能告诉用户“这篇文章里出现了你输入的两个关键词”。在内部系统里,这种解释能力比花哨的语义匹配更重要。

1.2 稀疏检索并不是过时方案

有一种倾向是把稀疏检索当成老古董。实际上在Sirchmunk的设计里,核心召回完全靠倒排索引,配合BM25打分,只是在排序阶段叠加了更多上下文特征。对于精确词命中、FAQ问答、工单检索这类场景,稀疏匹配不仅稳定,而且计算简单可量化。

稀疏检索最被低估的优势是可以精确定位。比如用户输入“Redis连接超时排查”,倒排索引可以做到“Redis”、“连接”、“超时”、“排查”四个词全部命中的文档优先,这是向量检索很难做到的。向量检索擅长的是“含义相近但用词不同”,比如“缓存挂了”和“Redis不可用”之间的联想;但现实是,很多内部文档标题就会直接用用户可能输入的关键词,精确匹配已经解决大部分问题。

真正的病根从来不是“倒排索引能不能召回”,而是“召回后排序能力弱不强”。过去只有TF-IDF或BM25时,排序确实粗糙,满足不了长尾需求。但现在可以在倒排召回的基础上叠加大量实时特征,用轻量模型做重排,这本质上已经不是传统意义的“关键词搜索”了。Sirchmunk走的就是这条路:召回靠稀疏,排序靠特征加模型,兼顾准与快。

1.3 对“自进化”的理解方式

说到自进化,网上容易渲染成玄学,好像系统会自己长出能力。实际工程里的自进化,在我看来是一套受控的闭环:系统观察用户行为,形成信号;信号回流到词典、索引权重、排序参数;新参数经过验证后生效;整个过程自动迭代。核心不在某个神秘算法,而在闭环管道设计得是否扎实。

Sirchmunk的自进化主要作用在三个地方:词关系扩充、排序特征调整、结果注入与置顶。这三个地方分别对应召回变宽、排序变准、干预变少。每一步都依赖日志反馈来驱动,而不是拍脑袋。这点在后面章节会详细拆。

2. 索引与检索:无向量方案的核心工程

既然不用向量库,索引与检索模块就是整个系统的心脏。这里说几个关键工程点,都是实践中真正踩过坑才确认下来的设计。

2.1 索引结构:词典加倒排列表加辅助元数据

Sirchmunk的索引物理结构可以简单理解成三层:词典层记录词项到倒排列表的映射,倒排列表存docID、词频、位置信息,辅助元数据存字段级权重和文档属性。

词典层用双数组Trie加哈希表结合。Trie负责前缀查找和提示词补全,哈希表负责精确词典查询。倒排列表没有用纯数组存储,而是在内存里用跳表加差值编码,千万级文档量下,既能支持快速跳跃定位,又能压内存占用。位置信息这个字段容易被人省略,但Sirchmunk保留了它,因为短语查询和邻近查询都依赖位置计算。比如用户搜“订单失败 重试”,文档里“失败”和“重试”出现在同一段落才算强相关,没有位置信息只能妥协成AND匹配,效果差不少。

文档元数据单独放在一套列式结构中,方便排序阶段按需取用。比如我们需要按“最近更新时间”、“作者部门”、“文档热度”做过滤或特征计算,列式存储可以快速读取指定字段,不需要展开整篇文档。很多自研搜索引擎初期会忽略这层设计,结果排序阶段要临时查数据库,延迟一下子就上去了。

2.2 召回策略:从词匹配到短语与字段加权

召回策略直接决定了“能不能找到”。单纯对用户输入的每个词做OR匹配,长尾场景噪音会很大;做成AND匹配,漏召回又太严重。Sirchmunk的召回逻辑分三层。

第一层是核心必达:所有查询词必须命中的文档集合,要求严格AND匹配,这部分是精确答案的候选池。第二层是宽松层:去掉其中一个低频词后命中的文档集合,主要解决用户多打了一个词、或者词序颠倒的问题。第三层是扩展层:通过同义词和近义词扩展后的词项参与匹配,这块由自进化模块动态维护。

字段加权在召回阶段就起作用。标题命中权重远高于正文,标签字段命中权重高于摘要。这不是排序阶段再做的事,因为权重会直接影响初筛时候选集的大小。比如标题命中的文档哪怕频次只有一次,也比正文频次五次更有机会进候选集。所以索引构建时,不同字段的词项会写入各自独立的倒排列表,查询时按字段加权合并打分。

短语召回在这套架构里是独立处理的。查询解析器会先识别短语片段,比如带引号的内容、连续的英文词组或ID串。短语作为一个整体词项去匹配索引里预建的phrase倒排列表。预建短语索引是在文档入库时用N-Gram滑窗抽出来的,代价是索引稍大,但查询时短语精度非常高,工单号、版本号这类检索尤其受益。

2.3 排序模型:特征设计加打分函数

倒排召回之后,Sirchmunk的排序分两个阶段。第一阶段是快速粗排,用BM25加少量静态特征过滤,目标是快速砍掉明显无关的文档,把候选集压缩到几百个。第二阶段是精排,用GBDT模型对几百个候选逐条打分。

精排用到的特征分为三类。文本特征:BM25分值、词项覆盖率、短语命中数、标题命中权重、核心词距离等。行为特征:文档历史点击率、平均停留时长、收藏数、转发数,这些来自反馈数据。时效特征:文档新鲜度,指数衰减函数处理。

模型训练用LambdaMART,损失函数直接优化NDCG,对排序任务来说比普通回归准确得多。初期没有训练数据时,先用规则打分函数做兜底,等反馈日志攒够十万级会话再切模型。这是整套架构里为数不多必须等数据的地方,急不来。

2.4 中文场景的切词与过滤

如果只处理英文,分词简单许多。但Sirchmunk面对的中文内容必须做专门的切词。我们采用“词典分词加统计分词结合”的策略:通用词典负责日常词,领域词典负责产品名、系统名、内部代号。统计分词用条件随机场训练一个序列标注模型,解决未登录词。

有个细节是过滤词表的动态性。传统实现会把“的、了、吗、呀”这类停用词直接丢掉,但内部文档里很多核心语义恰恰藏在短词里。比如“上云”里的“上”,“下线”里的“下”。所以Sirchmunk没有用固定停用词表,而是用“词频加文档频率”联合判定:高频且区分度低的词自动降权,但不会直接删除。这个策略比静态停用词表效果好不少。

3. 自进化机制:反馈闭环怎么做

自进化是Sirchmunk区别于普通搜索框架的核心设计。很多人以为只要记录点击日志就算闭环,实际差了很远。真正要跑通闭环,需要把信号采集、归因、更新、验证四件事全部自动化。

3.1 三类反馈信号

第一类信号是显式反馈。用户点了“有用”或“没用”按钮,这是最干净的信号,但数量稀少,通常占全部行为的不到2%。任何推荐系统都不能只靠它,但也不能没有它。

第二类信号是隐式反馈。点击、停留时长、二次搜索比例、翻页深度,这些信号量大但噪音也大。点击不一定是满意,可能是标题党;停留时长长不一定是内容好,可能是没看懂。处理隐式信号的关键是设置阈值和置信度,比如点击位置在前三但停留不超过三秒,标记为低置信度正反馈,不参与模型更新。

第三类信号是外部反馈。比如新文档被高频引用、代码片段被反复粘贴,这类信号与具体搜索会话无关,但对文档权重有天然加成。Sirchmunk会把外部信号折算成静态分,直接进入精排特征。

3.2 在线日志与归因模块

日志采集不在业务代码里堆打点,而是统一走搜索中间件。查询词、命中的文档ID列表、展示顺序、用户点击序列、后续行为,全部汇聚到日志管道。这里的核心难点在归因:用户点了结果B,但B展示了三个,到底是因为排在第一的A太差还是因为B标题写得好,需要合理分割归因。

Sirchmunk的归因模块没有用复杂的因果推断,就是用“逆位置权重”法:用户在位置一的点击权重高,在位置五的点击权重低,同时考虑展示时间序列。这样能得到一个“如果结果排序变化,预期点击会怎样变化”的近似信号,用于训练排序模型基本够用。

3.3 词典与同义关系的自动演进

搜索系统最常见的badcase是“用户搜的词文档里没有,但文档内容明明相关”。Sirchmunk靠动态同义词来解决。初始词典是人工导入的几十组常见同义词,比如“宕机”等于“挂掉”等于“不可用”。但真实世界的词汇变化快,人工维护跟不上。

自动演进采用“点击停靠分析”:当两个不同查询词触发了同一条文档被点击,且查询词本身又不相似时,就会把这两个词的关系强度加一分。例如用户搜“证书过期”点了文档X,另一个人搜“SSL失效”也点了文档X,多次重复后系统自动建立起“证书过期”与“SSL失效”的相关关系。累积关系度超过阈值后,写入同义词词典,参与后续查询扩展。这个过程完全不需要人工参与,运行三个月后,词典自动膨胀了差不多三分之一。

3.4 排序模型周期性重训练与A/B验证

排序特征和模型权重不能每天变,否则用户会感觉结果飘忽不定。Sirchmunk采用固定进化窗口:每周日凌晨跑一次离线训练,用上一周日志生成新的排序模型,然后切到小流量验证。

验证采用分层A/B方式,5%流量的对照组用上一版模型,5%实验组用新模型,观察点击率、无结果率、二次搜索率、平均停留时长等指标。只有连续两周正向指标显著时才全量上线。这里最重要的一条经验是:不要因为单周指标好看就急着全量,噪音周期普遍存在。

4. 实操实现:模块划分与关键流程

模块划分决定了后续迭代是舒服还是痛苦。Sirchmunk把整个检索链拆成六块,每块职责单一,中间通过接口通信,边界非常清晰。

4.1 六个核心模块与数据流向

查询网关负责接受请求、鉴权、限流,把原始查询转成内部查询对象。分析模块做分词、词性标注、意图识别、查询改写。检索模块做倒排召回、短语匹配、候选集合并。排序模块加载特征和模型,完成粗排与精排。反馈采集器负责把搜索结果页面行为写入日志管道。进化引擎定期消费日志,产出新词典、新权重、新模型。

数据流向是单向的:查询进入网关后一路向下,结果返回前端;同时用户行为回流到日志管道,进化引擎处理后把产物推给索引与排序模块。这个结构好在哪里?任意模块可以独立升级,比如分析模块换一套分词器,不影响检索和排序;排序模型重训练,不需要动索引。解耦带来的直接好处是,每次改动都只需要在单模块内测试,风险大幅降低。

4.2 索引更新的实时通道

索引不能是全量重建,Sirchmunk采用“主索引加增量段”的模式。新文档进来先写入增量段,内存里维护一个小时级的小索引,查询时合并主索引和所有增量段。每隔一段时间,增量段合并进主索引。这种架构与Lucene的段合并思路一致,但自己实现时要注意合并策略和删除文档的清理。

实时性上,文档入库到可以被搜到,默认延迟是三十秒以内。增量段写满一定阈值或达到时间上限就触发合并,合并过程中不阻塞查询,用的是写时复制加原子切换目录的方式。这个设计被验证得很稳,线上很少因为索引合并导致查询抖动。

4.3 自进化调度与版本管理

进化引擎不是一直在跑,而是有节奏地执行。调度器按天触发任务链:拉取日志到离线表,清洗数据,归因分析,产出候选同义词,训练排序模型,生成实验配置。

版本管理容易被忽略。Sirchmunk要求每次进化的产物都有完整版本记录,包括数据时间范围、模型参数、特征字典快照、同义词变更集。这样做的好处是,一旦新版本效果不佳,可以快速回滚到旧版本。回滚粒度要细化到单产物,比如只回滚词典,不动模型。

4.4 最小可运行实现方案

如果你不打算完整复刻Sirchmunk,想先用最小方案跑通闭环,可以参考这个顺序。第一步,搭一个带BM25的倒排索引查询服务,用现成的检索引擎库就能实现。第二步,在结果页埋点,记录查询词、结果顺序、点击事件,落日志。第三步,做离线的点击归因分析,产出词对相关度信号。第四步,将高置信度的词对写入同义词表,重启加载。这四步做完,你就已经拥有一个能自我进化的最小搜索系统。全程不需要运维向量数据库,也不需要大规模GPU训练。

5. 常见问题与排查实录

这部分是排障经验,很多情况是跑了一段时间之后才暴露出来的,值得单独记录。

5.1 明明有文档却搜不到

最常见的原因是分词不一致。文档入库时“OCR”被切成一个词,用户查询时“o c r”被拆成三个字母,两边对不上。排查思路是先看分析模块的输出,把查询词分词结果打出来,确认词典里是否存在这个词。如果确实没有,就会走未登录词策略,下次入库时自动学习。

另一个原因是过滤条件误伤。比如时间范围默认只查三个月,但用户找的是一年前的历史方案。这种问题定位很快,检查查询日志里的过滤参数即可。

5.2 找到了结果但排名靠后

典型的排序权重问题。Sirchmunk初期特征权重依赖人工调节,经常出现标题含精确关键词的文档排不到前面。后来引入模型训练后,这个问题大幅缓解。但有一个隐藏点:精排阶段没有纳入“标题完整匹配”这一强特征,导致模型学到的是碎片化信号。补充该特征后,排序准确度明显提升。

5.3 自进化改完词典,效果反而变差

这是自进化系统典型的过拟合风险。某段时间大量用户搜“登录失败”,第一次搜“登录失败”没结果,第二次搜“验证失败”点了文档,系统就建立了强关联。但两周后该问题修复,用户不再搜“登录失败”,这个关联却仍然是高权重,于是搜“登录失败”时总会返回过时文档。

解决办法有两条:一是同义词必须有时效衰减,太久没有共同点击就不再强化;二是新增词对先进入灰度词典,只影响小流量,观察一段时间再全量。

5.4 索引文件膨胀,磁盘告警

增量段合并策略不当时,会产生大量冗余段文件。常见治理办法是限制最大段数量,并定期做全量重合并。同时把删除文档的清理从“标记删除”改成“合并时真正物理清除”,可以显著降低存储占用。

Sirchmunk演进过程中踩过的最深的一个坑,是重排序模型上线后效果不如老版本,但指标差异一天看不出来,三天观察期内用户流失已经发生。所以自进化迭代必须要能快速回滚,而且一定要设定最低实验周期,不能只看短期波动。这套机制跑到现在,最大的感受是:搜索系统真正的好处来自稳定可控的演进节奏,而不是某个时段的大改大动。下次拆排序模型特征时,再单独聊细节。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进