☰
自进化检索架构:不靠向量数据库,用倒排索引与词图实现语义搜索
2026/10/22 5:34:23 网站建设 项目流程

1. 从一次不堪重负的资源账单说起:我为什么放弃了“Embedding优先”方案

去年年中,我们团队接了一个站内知识库搜索的活。最初版本和大多数人想的一样:文档切块、调Embedding模型、灌进向量数据库、按相似度召回。原型阶段一切都很好,Top10看起来像模像样,演示给业务方看的时候对方也很满意。但等到数据量从几万条文档涨到几十万条,情况就开始不对了。

首先是向量检索的资金盘子越来越难看。Embedding模型推理要吃GPU,向量库集群要占内存,每加一批文档就要重算一遍向量;更麻烦的是,用户查询里频繁出现文档里压根没写过的口语化表达,比如业务系统里的“挂账”在文档里写作“未结算”,“审批流”在文档里写作“审核流程”。检索系统再怎么相似度计算,也掩盖不了一个事实:我们把大量成本花在了“词面完全不同”的语义鸿沟上,却没能建立起词与词之间的动态关联。那段时间我反复在日志里看见低质召回结果,用户翻到第三页就放弃,搜索框的后续点击量一直在掉。

也就是在这个背景下,我开始重新审视一个老问题:搜索引擎到底需不需要向量数据库来支撑语义检索?带着这个疑问,我陆陆续续做了一个叫Sirchmunk的检索架构原型。它不依赖任何向量数据库,核心思路是把“语义关系”交给一套持续自我修正的词关系结构和索引权重来完成,也就是标题里说的“自进化”。这套架构的初衷很简单:用倒排索引和轻量级词图承担召回任务,让系统根据用户查询行为动态修正词形、搭配和排序权重,从而在没有GPU和专用向量集群的条件下,获得接近传统向量检索方案的体验。

这篇文章是第一篇,重点讲整体架构设计和设计取舍。我不打算堆一大堆概念,而是想先介绍这个项目“为什么这样做”“数据流长什么样”“自进化到底进化了什么”,让打算自己动手做类似搜索系统的朋友能有一个清晰的整体框架。后续文章再展开索引细节、查询改写、评估方法等具体模块。

适合读这篇文章的人,我认为有三类:一是像我一样在预算或运维能力上扛不住完整向量检索链路的小团队;二是手里有大量非结构化文档、搜索质量长期靠人工调词的开发者;三是对搜索架构感兴趣,想了解“不靠向量召回”这条路到底能走多远的技术爱好者。

2. Sirchmunk的架构总览:绕过向量索引,搜索引擎依然可以“懂”语义

2.1 两条路线的本质差异:特征向量 vs 检索信号

先摆一个我的基本判断:传统向量检索解决语义问题的方式,是把“语义相似度”转为“向量空间距离”,这在理论上很漂亮,但工程上要付出三笔固定成本:

  1. 离线Embedding成本:每篇文档都要过模型,模型更新后还要全量重算。
  2. 在线查询成本:用户查询也要Embedding,需要保持模型版本一致。
  3. 相似度计算成本:即使有ANN索引,高召回率下免不了要扫描大量候选集。

Sirchmunk选择的是另一条路:把语义关系拆成可增量维护的检索信号。这里的“检索信号”包括:

  • 词条与词条的共现关系(哪些词经常在同类文档里一起出现)
  • 词形变体关系(“付款”与“支付”、“审批”与“审核”的用户改写习惯)
  • 文档之间的交叉引用强度(哪些文档经常被同一批查询命中)
  • 历史查询的点击反馈分布

这些信号本质上是一张持续更新的<词-词>、<词-文档>关系网络。它不追求完美的语义理解,但胜在四项天然优势:无需GPU、增量更新成本低、可解释性强、冷启动速度快。

2.2 四个核心组件:摄取、索引、查询、进化循环

Sirchmunk的整体架构可以拆成四个部分,每个部分的职责很清晰:

组件职责关键产出
摄取管线解析文档、抽取元数据、构建倒排索引文档块、词频表、倒排列表
索引仓库存储词条关系、文档元数据、历史反馈词共现图、动态词典、热区记录
查询引擎解析查询、扩展词形、召回候选、重排结果TopN文档列表
进化调度器基于查询日志与反馈信号批量更新索引权重词形改写表、加权规则、新词条目

组件之间没有复杂的消息总线,就是常规的本地任务队列加定时调度。整条链路可以跑在一台16核32G的物理机上,这在我之前的向量检索方案里是不可想象的——同样的数据量外加一台GPU实例都不够从容。

2.3 一次查询的完整旅程:从关键词到排序结果

为了让思路更具体,我描述一次真实查询的流程。假设用户输入“怎么处理未结算的挂账”:

  1. 查询引擎先做基础分词和清洗,得到关键词集合:["怎么", "处理", "未结算", "挂账"],同时过滤掉“怎么”这类停用词。
  2. 查询引擎去查询“动态词典”,发现“未结算”存在一个词形改写关联“未结算 → 待付款/未付款/未收款项”,“挂账”关联“挂账 → 应收账款/往来款/欠款”。于是原始查询被扩展成一组同义/近义表达。
  3. 用扩展后的词条去倒排索引里取回候选文档集合,这一步只是“OR”语义,候选数量可能很多。
  4. 进入热区重排阶段:Sirchmunk会给近期高频共现词对、用户点击密集的文档段加权,目的不是改变召回集合,而是让更可能被用户认可的文档排到前面。
  5. 返回TopN,并把自己的“查询词条-文档排序”记录写入日志,供进化调度器后续使用。

整个过程没有任何向量计算,最重的操作也就是查词典扩展、倒排求并、按加权分数排序。带来的一个明显好处是:单次平均查询时间基本稳定在50毫秒以内,不再随数据量上升出现明显劣化。

3. 自进化到底在进化什么:不是模型参数,而是索引规则

3.1 进化的信息源:查询日志和点击行为

“自进化”这个词听起来玄乎,但在Sirchmunk里它指的事情非常具体——系统定期从查询日志与反馈行为中抽取规则,再把这些规则写回词典和索引权重,从而让下一次查询更快、更准。真正驱动进化的信号有四类:

  • 查询词条与点击文档标题的共现关系;
  • 同一查询会话内用户的连续改写行为;
  • 用户跳过前几位结果、点击靠后结果的行为分布;
  • 文档被圈选复制或长时间停留的阅读行为(这类信号很弱,但能辅助判断)。

这些信号结合已有的词共现图,就能形成“哪个词应该关联哪个词”“哪篇文档在什么查询下应该更靠前”的修正方向。

3.2 词形改写的自我增长:从查询失败里学习新词

词形改写表的初始来源是人工词库和同义词词典,但真正让它丰富起来的,是从搜索失败案例里自动“长出”的新词关系。举个例子:用户查“报销时限”,文档里写的是“报销有效期”,刚开始倒排索引命中很少,用户翻了几页没找到,换了关键词“有效期多久”,最终点开了目标文档。Sirchmunk在后台捕获到这次“查询词→最终点击文档标题词”的转化链,就把“报销时限”与“报销有效期”的关联强度上调。经过几十次类似行为,词形改写表里就会稳定出现“报销时限 → 报销有效期/有效时间/报销截至日期”的关联。

这个过程和用户手动维护同义词词典相比,最大的区别在两点:

  • 动态性:词关系随用户习惯变化,不需要等到人工发现再更新;
  • 频次门槛:只有达到最低共现次数的词对才会进入规则表,减少噪声干扰。

3.3 反馈回路设计:避免进化变成“偏执”

自进化系统最怕的其实是“正反馈失控”——一个错误规则被反复强化,导致排序越来越偏。我在Sirchmunk里加了几个约束:

  • 衰减因子:每条进化规则的权重会随时间衰减,如果后续行为不支持该规则,它的影响力自动降低;
  • 置信度门槛:一个词对(A→B)必须至少在N个独立查询会话中出现,并且点击文档的重合度超过阈值,才允许进入持久规则表;
  • 分桶验证:新规则先在低流量查询分桶上线,观察点击率变化,再决定是否全量生效。

这个设计有点像我以前做推荐系统时的“EE策略”——探索新的词关系,但只在证据充分时利用它。

4. 不依赖向量库的语义近似:倒排索引 + 词共现图 + 热区重排

4.1 为什么倒排索引够用:大多数查询是“关键词命中”问题

很多人听到“语义搜索”就下意识觉得必须用向量,但我想提醒一个统计事实:在我接触的站内搜索场景里,超过80%的用户查询词都能直接命中或近义命中文档中的词条。真正的难点不是“理解自然语言”,而是“词面不同但意思相近”的词汇覆盖问题。而词汇覆盖问题,恰恰可以用动态词典+词形扩展来解决。

Sirchmunk没有复杂的语义模型,它的召回核心还是经典的倒排索引:词条 → 倒排列表。但和普通倒排索引不同的是,它的词条来自“动态词典”,包含原始词和进化得到的词形变体。因此在召回阶段,系统实际上是在“多套词面”上同时做匹配。

4.2 词共现图:让组合条件参与排序

倒排索引只能告诉你“哪些文档包含某个词”,但很难表达“这些词在一起出现”的重要性。Sirchmunk用一个轻量级词共现图来弥补:图中每个节点是词条,边权重表示两个词在同一文档或同一查询中被共同命中的频繁程度。

这个图的作用有三:

  • 查询扩展时,从共现图中找出与查询词关联度高的近邻词,用于扩充候选集合;
  • 排序时,如果文档A同时包含“报销”和“有效期”,而另一篇文档B只出现“报销”,A的基础分更高;
  • 当用户查询词本身没命中任何文档时,可以用共现图做“跳转式召回”,找到关联词再进行第二轮匹配。

4.3 热区重排:把近期用户偏好灌入排序

热区这个概念是我从缓存设计里借鉴过来的。Sirchmunk里维护了一个“热区矩阵”:记录“查询词组合 → 高频点击文档集合”的近期统计。每次查询排序时,如果当前查询命中了热区矩阵中的某个词组合,就把对应文档集的权重整体上调。

效果层面,这个机制对两类查询特别明显:

  • 热点业务查询:比如某个时间段突然集中出现的“怎么下载审批单”,热区会记录用户最终点击的文档,并不断强化排序位置;
  • 长尾变体查询:比如“报销最长时限”“报销最晚时间”“报销截止日期”这类表达混乱但指向同一批文档的查询,热区会把这几个词组合映射到同一批文档。

热区矩阵本质上是一种短期记忆,它让Sirchmunk在保留长期规则的同时,还能快速适应用户近期的关注点变化。

5. 落地一个原型需要准备哪些零件:数据结构与关键选型

5.1 数据模型:文档、词条、关系表

要落地这套架构,你需要几张非常朴素的关系表(或JSON文件)。我列一个最小集合,方便你照着建模:

  • doc_meta:文档ID、标题、正文摘要、URL、更新时间、被阅读时长总和、点击次数。
  • term_dict:词条ID、词条原文、词条变体集合、出现文档数、最后更新时间。
  • inverted_index:词条ID、文档ID、词频、位置列表(可选)。这等价于传统倒排索引。
  • term_cooccur:词条A、词条B、共现次数、最近共现时间戳。这张表构成词共现图。
  • query_log_raw:查询ID、查询词条序列、用户最终点击文档、检索返回候选列表、事件时间。
  • evolution_rules:规则类型、源词条、目标词条/文档、置信度、生效状态、最后生效时间。

这样的模型设计有几个好处:结构透明,任何一步出问题都能回溯;增量更新方便,新规则本质上就是往这几张表里插入或更新记录;迁移成本低,后面如果要引入向量检索,也能把这套模型作为向量方案之外的精排层并存。

5.2 内置搜索引擎组件选择:还是得用成熟基础件

Sirchmunk没有从零写一个搜索引擎,它在底层用了几个成熟的基础组件来避免重复造轮子:

  • 倒排索引部分采用了某基于Lucene的嵌入式搜索引擎作为底层存储与基础检索库。我主要使用它的分词、倒排存储和BM25基础排序能力,高级打分则交给上层规则;
  • 词图和热区矩阵用轻量级内存数据库或嵌入式KV存储维护。数据量不大,单机完全能撑住;
  • 语言层用的是Java或Go这类适合快速写IO逻辑的语言。我这边用Java,主要是因为和底层搜索引擎API的衔接顺畅;
  • 进化调度器用一个定时任务框架,每天凌晨跑增量更新,不占用高峰时段计算资源。

需要说明的是,这些底层的选择不是唯一答案。如果你更熟悉Python,完全可以用Python版底层的嵌入式搜索引擎组件搭配相同的数据模型。关键是理解每个部件的职责边界。

5.3 索引更新策略:增量优先,定期的全量重建兜底

索引更新是搜索系统最容易出性能问题的环节。Sirchmunk采取的策略是“增量优先+定期合并”:

  • 新文档进来后,只对增量部分构建倒排索引,插入到已有索引段;
  • 词共现图和热区矩阵每次只在内存里更新,并不立即落盘;
  • 每个周期(比如一天)跑一次段合并,压缩索引文件,同时把内存中的词图和热区统计写入持久层;
  • 遇到词典结构大变(比如导入了一批人工同义词),才触发全量索引重建,重建期间是用旧的索引副本提供查询服务。

这个策略借鉴了传统搜索引擎中“段合并”的思想,但用在了轻量级场景里。好处是查询延迟稳定,不会因为一次全量更新导致大停顿。

5.4 重构模块信号控制:防止一条错误规则带偏全局

关于进化调度,我再多说一个细节:信号要分优先级,不能用单一行为做决定。我在项目里把信号优先级分为三层:

  1. 强信号:用户点击,且停留时间超过阈值(表示真的读到了内容);
  2. 中信号:用户查询后主动复制了摘要内容;
  3. 弱信号:用户翻页后改变查询词,或重新输入相近查询。

强信号可以直接推动规则生成或权重上调;中信号只能作为辅助证据;弱信号主要用来发现“查询词需要扩展”,而不用来直接强化文档排序。这么做的原因很简单:点击行为可能由“标题党”引起,但停留时间和内容复制往往更能反映实际满足度。

6. 用一组对照实验看效果:Sirchmunk和基线方案的差距

6.1 评测维度与数据集说明

为了确认这套架构不是自我感觉良好,我构造了一个对照实验。数据集来自对某知识库的脱敏文档集,共约8万篇技术文档和规章制度文本;查询样本来自历史搜索日志中抽样出的2000条真实查询。评测指标用:

  • 召回率@10:排在前10的结果中,出现有效相关文档的比例;
  • 平均排序位置:相关文档被排到的平均名次,越小越好;
  • 查询延迟:单次查询从输入到返回的耗时。

基线方案是“BM25 + 人工维护同义词词典”,这也是大多数团队在没有向量库时的常见选择。对比方案是“Sirchmunk自进化模式”。

6.2 结果一览:胜在长尾词和冷门表达

指标BM25+人工同义词Sirchmunk自进化提升幅度
召回率@1063.8%74.6%+10.8个百分点
平均排序位置3.92.8改善28%
查询延迟 P9988毫秒52毫秒降低41%

我对这个结果是持谨慎乐观态度的。召回率提升主要来自词形改写表的自动扩展——人工维护同义词不可能覆盖那么多口语化变体,而自进化机制能从查询日志里自动学习。延迟的下降也不是因为Sirchmunk的算法更“高级”,而是因为它没必要把时间花在向量距离计算和ANN参数调优上。

6.3 边界案例:自进化系统依然会有“翻车”场景

我必须坦白,这套架构不是银弹。在两类查询上,Sirchmunk的表现并不理想:

  • 高度抽象的概念查询:比如用户查“组织架构优化”,文档里通篇是“部门合并”“管理扁平化”,但并没有出现查询中的任何词。此时词形扩展和共现图都难以找到关联,倒排索引召回基本落空。
  • 语义反直觉的行业黑话:某些词汇在特定行业里有完全不同含义,比如“备付金”“头寸调整”等,如果历史查询日志里从没见过,系统就无法自动建立关系。

对这两类情况,我的建议是:要么人工补充强规则;要么在架构里预留一个“外部语义增强”接口,在召回为空或低置信时,才调用一个轻量模型兜底。这属于Plan B的范畴,下一篇文章会展开。

7. 什么时候该回头用向量检索:自进化方案的适用边界与Plan B

我之前反复强调“无需向量数据库”,但并不是说向量检索一无是处。准确说,Sirchmunk的定位是替代“常规文档检索场景下的向量方案”,而不是替代所有语义理解的复杂场景。

适合Sirchmunk的场景,我总结为以下三个特征:

  • 文档数量级在百万以下:再大的规模,单机倒排索引也能承载,没有必要为了海量召回而上向量;
  • 查询词与文档词之间存在可学习的对应关系:比如用户习惯用A词,文档习惯用B词,而且这种对应关系可以靠查询行为反复验证;
  • 团队缺少GPU或向量运维经验:希望能用一套简单、透明、可调试的方案快速上线。

不适合的场景也很明确:

  • 跨语言检索:中文和英文文档的匹配关系不是简单的词形变体可以解决的;
  • 图片、音视频等多模态检索:倒排索引只能处理文本特征;
  • 对“抽象表达”特别敏感的领域,如法律条文、学术论文的语义联想检索。

如果真要给一个Plan B方案,我的建议是在Sirchmunk架构外挂一个“语义召回兜底层”:当自进化查询结果的前N个文档的用户反馈率低于阈值时,系统自动降级到备用语义模型做一轮补充召回,再与原有结果做加权融合。这样既保留了主链路的轻量优势,又能在大词面差异场景下有保底能力。

8. 下一步准备做什么:从索引规则到查询意图预测

目前Sirchmunk还在持续迭代中。我下一步的重点有三个方向。

第一,把热区矩阵的“时间窗”粒度调细。现在热区统计是按天滚动的,但在实际日志里,有些业务词汇在一周内有明显衰减,有些则在月底突然冲高。我准备引入更细的时间衰减函数,让热区更能反映近期意图。

第二,改进查询改写表的生成逻辑。现在主要靠行为共现,下一步想在“行为共现”之外引入词条在文档主题分布中的一致性判断——如果两个词经常出现在同一批主题文档里,即便用户没有频繁改写过,也应建立弱关联。

第三,把评估流程工具化。现在跑一轮实验还要手动重启索引、导日志、算指标。我准备把这套流程固化成一条可复用的流水线,让每次规则更新都能自动跑出效果对照表。

坦白说,做Sirchmunk的过程里,我最受用的一个经验是:选型时不必迷信某个明星技术,先把数据规模、查询模式和团队运维能力算清楚,再决定要不要背上向量检索那套基础设施负担。很多场景下,一个自进化调参的经典倒排索引方案,可能比华丽的向量方案更实用、更省钱,也更可控。

如果你也想在自己项目里试一下类似思路,我建议从小处入手:先把你现有搜索的查询日志翻出来,统计一下高频查询词与实际点击文档标题词的差异,你会很快发现“词面鸿沟”到底有多大。把这个鸿沟量化出来,再考虑是否值得建一套自进化词表系统,你的投入产出比会清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询