1. 为什么我最终放弃了向量数据库:一个搜索系统的现实选择做搜索引擎的人,这两年大概都被问过一个灵魂问题:你上向量数据库了吗?说实话,最早拿到这个项目需求时,我也按“标准答案”走过——接一个嵌入模型,把文档全部向量化,然后搞个近似最近邻检索,看起来又潮又稳。但真正在数据量、实时性和成本三重压力下跑过一段时间后,我彻底改变了思路。Sirchmunk这个项目,最初的定位是一个面向垂直领域的内容检索系统。这个垂直领域的特点是:文档数量不是互联网级别,但查询模式极其复杂——用户会用各种口语化的说法去搜索同一类东西,而且新内容每天都在进来。用向量数据库一开始确实很爽,嵌入、召回、排序,流程顺畅。可问题也随之而来:千亿级参数模型跑出来的向量不是白来的,每个文档都要过一遍嵌入模型,每天增量更新还要重新计算;商业化向量数据库的按量计费在百万文档级别还勉强能忍,但到了千万级、上亿级,成本就是一笔让人头疼的开销。更重要的是,向量检索是个黑盒,用户搜不到东西时,你根本没法解释到底哪个环节出了问题。真正让我下决心推倒重来的,是一个很简单的场景。系统里存了一批行业报告,用户想搜“今年第二季度的市场份额变化”。传统的词法检索可以轻松命中“市场份额”“第二季度”这些关键词,但向量检索只按语义相似度找邻居,如果不做特别处理,匹配到的文档可能语义相近但根本不是用户要的那一篇。调试一个这样的个例就要花掉半天时间,因为我得去查向量距离是怎么算的,嵌入模型为什么把这几个文档拉到了一起。这太痛苦了。于是我开始思考一个看起来有点“复古”的问题:有没有一种方案,不需要向量数据库,也能实现语义级别的搜索,同时还能让系统越用越聪明?这个问题的答案,就是Sirchmunk今天这版架构的核心——用传统全文索引导出的结构化语义特征,加上一套基于用户行为的自反馈循环,让系统在运行中持续进化。这篇文章我会把整个架构设计拆开讲清楚,包括每个部件为什么存在、数据怎么流转、进化机制怎么落地,以及这套方案踩过的坑和已经明确的边界。如果你也在做中小规模的搜索系统,或者被向量数据库的成本和黑盒问题困扰,这版思路应该能给你一些直接的参考。即便你已经在用向量检索,这套架构里的反馈闭环设计也可以单独拿出来借鉴。2. Sirchmunk的架构总览:四层模型与两条数据链在详细展开之前,我先给出一张全局蓝图——注意这里没有流程图,我用文字把关系描述清楚,你可以在脑内建模。2.1 四个层次各自的职责我把整个系统拆成四个大的层次,每一层只做自己那一摊事,层级之间通过明确的数据接口通信。第一层是采集与清洗层。它负责从多个内容源拉取原始文档,然后做去重、格式统一、正文抽取、语言检测这些基础工作。输出是一个干净的结构化文档对象,字段包括标题、正文、发布时间、来源、标签等。这套系统里的“文档”比你想象的要“瘦”,我们不保留网页脚本、导航之类的噪音,只要对检索有意义的正文信息。第二层是存储与索引层。原始文档存档在对象存储里,同时写入一个核心的全文索引——我用的是Lucene体系。索引在这里不只是用来做关键词匹配的,它同时是后面几张“衍生表”的物化基础。除了索引,这一层还维护了一张实体关系图和一个标签倒排表。实体关系图记录的是文档中提到的人、机构、产品、概念之间的关联关系;标签倒排表则是一个“标签指向哪些文档”的映射表。这两个东西都不是靠外部图数据库,而是直接建立在索引文件和排序表之上,查询时合并结果。第三层是检索与排序层。所有面向用户的查询都经过这一层处理。它有四个子模块:查询解析模块、召回模块、重排序模块、日志记录模块。查询解析模块负责把用户输入的自然语言切割成可检索的词元、短语和标签意图;召回模块先跑经典的关键词检索,再并行做标签匹配和实体邻域扩展;重排序模块把多路召回的结果用一套加权公式揉在一起;日志记录模块把每次查询的完整上下文记录下来——包括解析结果、召回集、排序结果、用户最终点击了什么。第四层是进化层。这是Sirchmunk和普通搜索引擎最不一样的地方。它定期扫描行为日志,从中挖掘新的标签词、高频查询模式、效果不佳的排序规则,然后自动更新标签倒排表、实体关系权重和查询重写规则。这层没有做成在线实时计算,而是一个批量进化的离线工作流,因为实时计算对基础设施要求太高,而且很多反馈信号本身就有噪音,等积累到一定数量再做统计更有意义。2.2 两条数据链:检索链和进化链整个系统的数据流其实只有两条链。第一条叫检索链,是用户查询进来的路径。Query进入查询解析模块,产出词元和标签;召回模块在索引和标签表里各拉一批候选;重排序模块融合多路结果并输出Top列表;列表渲染给用户的同时,用户操作行为被写成日志。这条链的延迟要求很高,核心检索部分必须控制在几十毫秒以内,所以我尽量不在实时路径上做重量级计算。第二条叫进化链,是反馈驱动的路径。行为日志批量汇入进化层;进化层做统计分析,生成或更新标签词典、查询重写规则、实体关系权重;这些产物被写回索引层,供下一次检索链使用。进化链是异步、离线、批量的,它不追求实时性,只追求“每一次更新都比上一次更准”。为什么要把进化做成离线的?这里有一个工程上的现实:在线自进化意味着每次检索都要等待一个额外的在线学习动作,这在延迟上和稳定性上都是灾难。离线批量更新虽然“慢半拍”,但可以充分利用夜间空闲的计算资源,而且一次批量更新可以同时解决上千条查询模式的优化,性价比远高于在线逐条更新。3. 不依赖向量数据库,靠什么做出语义效果这一节回答一个核心质疑:你说了半天不用向量数据库,那语义匹配怎么办?总不能用“精准匹配关键词”糊弄人吧?当然不是。Sirchmunk的语义能力来自三个非向量部件:标签倒排表、实体关系图、查询重写规则。3.1 标签倒排表:把“语义”显式化为可检索的结构向量检索做的本质上是把“语义相似”映射成“向量近邻”。Sirchmunk不搞向量,那它把语义映射成了什么?映射成了“标签”。每一个文档在入库时,会通过一个规则引擎打上若干标签。规则引擎的输入是文档的标题、正文和初始元信息,输出是一组标签词及对应的置信度。举例来说,一篇标题为“2025年Q2智能手机市场分析报告”的文档,规则引擎会打这些标签:智能手机、市场分析、季度报告、2025Q2、行业数据。这些标签被写入标签倒排表——这是一个形如“标签词→文档ID列表”的映射表。检索时,如果用户的查询解析出一组标签,系统直接在表里做交集和并集运算,效率比向量近似检索还快。但问题来了:规则引擎凭什么知道该打什么标签?答案是用一个可维护的标签词典。词典初始版本由业务分析师手工构建——把领域内的核心概念、同义词、上下位词全部列出来。之后,进化层会持续往词典里补充新词条,这就是自进化的入口之一。这套机制的优势在于可解释。任何一个文档被打上某个标签,你都能追溯到规则引擎里的具体条件;用户搜不到东西时,你可以直接看标签表里有没有对应的标签。向量系统的黑盒问题在这里完全不存在。3.2 实体关系图:让“关联”成为可计算的信息标签能解决“这篇文档讲什么”的问题,但解决不了“这个实体跟哪些实体有关”的问题。举个例子:用户在历史检索里经常把“A产品”和“B技术”放在同一个查询里搜,说明这两者在用户认知里是关联的。这条关联信息如果只存在于日志里,进不了检索环节,那就没法帮助后续查询。Sirchmunk在索引层维护了一张轻量的实体关系图。这张图的节点是实体(品牌名、技术名、人物名等),边是共现关系,边权由共现频次和用户行为共同决定。它不是“存储”在一张图数据库里,而是以三列结构存在索引文件里:源实体、目标实体、权重值。检索时,如果查询命中一个实体节点,系统会找到它的邻居实体,再用邻居实体去倒查相关文档,作为召回路的一个扩展分支。这里我特别想强调一个工程细节:实体关系图这张“图”不是给用户看的,它是给召回模块看的。用户搜“A产品”,召回模块除了直接命中A产品相关文档外,还会沿着实体关系图找到与A产品关联度高的“B技术”,把B技术的文档也拉回来,放在候选集里。重排序环节会判断这些扩展结果到底值不值得放到最终列表里。3.3 查询重写规则:让用户问法不再是一个难题用户搜索同一个东西,表达方式千差万别。“Q2智能手机出货量”“二季度手机卖了多少台”本质上是一个意思,但词法层面完全不同。传统搜索引擎靠同义词扩展词典,Sirchmunk的进化层做的是更高级的事情——它不断从行为日志里学习“哪些查询模式是同一个意图,该怎么重写成标准查询”。重写规则以“模式→标准查询”的形式存在。模式侧记录的是查询词元的组合、词序、间隔;标准查询侧是经过验证的、能召回好结果的规范化格式。规则库可以由人工初始化,进化层会在后续运行中自动挖掘新的模式,或者修正已有规则的置信度。重写模块作用的位置在查询解析之后、召回之前。如果系统检测到用户查询匹配了某条模式,就用标准查询替换原查询继续走检索流程。这里的关键设计是“不替换原始查询,只替换检索用的查询”——用户看到的还是自己输入的原话,但系统内部检索的是改写后的标准查询。这样做的好处是规则错了也只会影响结果排序,而不会让用户觉得系统“乱改了我的话”。这三件套合在一起,效果上其实覆盖了向量检索能做的大部分场景,但每一步都是可解释、可干预的。更关键的一点是,它们的更新机制完全建立在系统自己的运行数据上——这正好接上了下一节讲的自进化。4. 自进化机制:系统如何越用越聪明我见过很多号称“智能”的系统,所谓的智能其实只是初始化时灌了很多规则,上线后就不再更新。Sirchmunk的自进化,是指系统的检索能力会随着持续运行显著提升——它不是“假装聪明”,而是真正在“变聪明”。4.1 三个进化环路:快、中、慢自进化不是在同一个节奏上跑一个巨大的更新任务,而是拆成三个节奏完全不同的环路:第一个是快环,更新周期是分钟级。它只做一件事情:实时更新实体关系图的边权。用户点击某个搜索结果,系统会在日志里记录“查询实体”和“被点击文档的实体”之间的关联次数。这些次数实时累加,直接反映在实体关系图的权重上。这个环路的计算量很小,甚至可以放在Web服务进程的内存里定期刷盘。效果是:当用户反复通过某条关联路径找到目标文档时,下一次这条路径的优先级会立刻上升。第二个是中环,更新周期是小时级。它处理的是标签词典的增量更新。系统每小时扫描一次新增的行为日志,统计高频查询词。如果一个查询词在日志中反复出现,但标签词典里还没有它,系统会把它标记为“候选标签”。候选标签要经历一个验证过程:看看这个标签能否从日志里找到足够的点击验证,如果能,就正式加入标签词典,并自动回填到历史文档上。这样系统就学到了一个新的“语义锚点”。第三个是慢环,更新周期是日级。它处理的是查询重写规则的生成与优化。每天夜间,系统跑一遍全量行为日志分析,聚类出高频查询模式,对每个模式计算当前检索效果。如果某个模式的点击率远低于平均水平,说明现有规则处理不了它,系统就尝试生成一条新的重写规则;如果某条规则改写后的查询点击率比改写前低,规则就会被降权或标记待修订。这三条环路并不是从第一天上线就全部有数据可跑,快环需要实体关系有初始边,中环需要行为日志累积,慢环需要足够多的查询样本来做统计判断。所以Sirchmunk的上线过程是“冷启动”的:先用人工初始化的词典和规则跑起来,然后逐步放开三个环路,让它自己变聪明。4.2 反馈信号的质量控制:不能把噪音当信号自进化系统的最大风险是“学习到了错误的东西”。用户点了一个结果,不代表这个结果就是最优的;用户搜了一个词,不代表这个词就该进词典。如果系统不加筛选地消化所有行为数据,很快会被噪音带偏。我设计了一个简单的信号质量门槛,每个反馈事件都要经过三道筛选:第一道是行为完整性检查。点击事件必须包含查询、候选列表、耗时、最终点击位置这些信息。少任何一项就不入库——因为少了一个字段,后续统计时就有偏差。比如没有记录候选列表的话,你没法判断用户点击的是“最优结果”还是“唯一结果”。第二道是行为有效性检查。用户在结果页上停留少于3秒就返回,或者查询后立即翻页但不点击,这类行为被视为无效信号。它们可能意味着用户没找到想要的东西,直接拿去做“这个查询应该召回什么”的学习样本,会产生误导。第三道是标签一致性检查。只有当同一个查询模式在行为日志里反复出现,并且对应的点击结果具有高度的标签重合度时,系统才认为这个模式值得生成一条新的重写规则或标签。单次行为的置信度太低,必须靠次数来累积置信度。4.3 人机协作为进化护航纯自动进化可能存在一个盲区——系统没有“品位”,它只会统计性地优化点击率,但点击率高不代表结果质量高。比如用户想搜“手机”,如果首页全是促销广告且用户愿意点,系统可能会把广告结果排得更靠前——这对搜索体验是灾难性的。为了解决这个问题,我给进化层留了一个“人工确认岗”。所有由中环和慢环生成的新标签、新规则,默认状态是“实验性”。它们在系统里可以参与排序,但权重打折。每周由运营人员巡检一次候选池,确认后的规则转正为“线上规则”,权重恢复正常。这个环节看似“不自动化”,其实是保留一条安全底线。如果完全自动化而不管内容质量,自进化就会走向“过度拟合点击率”的深渊。这个设计我特别想推荐给要做自进化系统的同行:永远不要把一个完全无人干预的闭环放上线。你可以把人工确认的周期拉得很长,甚至一个月看一次,但一定要有。5. 从Query到Top5:一次检索的完整旅程讲了这么多部件,现在把它们串起来,看一次真实查询在Sirchmunk里是怎么走的。这节我按时间顺序逐步拆解,每一步都给出对应的数据处理。5.1 第一步:查询解析用户输入“今年Q2手机出货量国产”进入系统。查询解析模块先做词法切分,得到原始词元:今年、Q2、手机、出货量、国产。然后做归一化:Q2被映射为标准季度表达“2025Q2”,国产被映射为标签“国产手机厂商”。这一步还会调用查询重写规则的匹配,看看有没有现成的模式能把这个查询改写成标准格式。如果匹配到一条“时间词+产品词+指标词+属性词”的通用模式,系统会把检索查询改写为“2025Q2 智能手机 出货量 国产手机厂商”。注意,这里的改写结果不会展示给用户,只进入检索阶段。5.2 第二步:多路召回改写后的查询并行走三路召回:第一路是全文索引召回。用Lucene对核心词元做布尔检索,返回按BM25相关性排序的文档集。这一路负责“精准命中”——文档包含尽可能多的查询词。第二路是标签匹配召回。查询中的“智能手机”“出货量”“国产手机厂商”都是标签词,系统直接在标签倒排表里取交集,得到同时命中这些标签的文档集。这路负责“语义命中”——文档可能没有完整出现所有关键词,但标签上完全对得上。第三路是实体扩展召回。查询解析时识别出实体“手机”,实体关系图返回它的核心邻居。加入第二季度的出货数据、主要品牌表现等关联文档。这路负责“关联发现”——用户没有明说,但大概率相关的文档。三路结果合并成一个候选集,每篇候选文档都有一个或多个来源标记,对应它被哪条路径召回的。这些标记在重排序时很有用。5.3 第三步:融合重排序候选集里可能有几百篇文档,但最终只展示5个位置。重排序模块用一套加权公式给每篇文档打分:总分 = 0.5 × BM25得分 + 0.3 × 标签匹配得分 + 0.2 × 实体扩展得分 + 规则加成前两项都是标准计算。标签匹配得分的算法比较直接:查询标签数与文档标签数的重合比例,重合越多分越高。实体扩展得分看的是文档与查询实体的关联路径强度——如果一篇文档和查询实体之间的边权很高,说明它被用户验证过是关联的。规则加成是一个可扩展位。当某个查询模式匹配到一条实验性重写规则时,加成值为正或负,影响排序结果。这套公式的好处是每个系数都能单独调,而且在日志里可以看到每一路的具体得分,排查问题非常简单。5.4 第四步:日志记录与闭环Top5结果返回给用户后,系统记录这次查询的完整日志:原始查询、改写后的查询、三路召回各自的候选列表和得分、最终排序列表。用户看到结果后,可能点击了第二位或第五位,也可能谁都没点直接改搜索词。所有这些行为都会被快环和中环消化,成为下一次自进化的原料。有个细节我想强调:日志记录一定要做全量记录,不要做抽样。搜索系统的行为日志是所有进化分析的唯一数据源。抽样看似省了存储,其实丢掉的往往是长尾行为——而长尾行为恰恰是自进化最有价值的输入。这一节整体走下来,你会注意到每一路召回的结果都是可复现、可解释的。用户问为什么搜不到,你可以把三路召回的候选都拉出来,看清楚是哪一路缺席了。这是向量检索方案给不了你的调试体验。6. 每日进化循环:系统在夜间如何自我迭代Sirchmunk的进化并不是随时都在发生,日常的“自我迭代”集中在一个每天触发的离线工作流里。我给这个流程起的代号是“夜间循环”。整个流程按顺序做四件事。6.1 日志聚合与查询模式挖掘夜间第一步,把当天所有的查询日志按查询文本聚合,剔除单个用户刷出来的低频查询,得到高频查询集合。然后用N-gram重叠算法做模式聚类——把看似不同但实际相似度很高的查询聚成一个模式。比如“手机出货量”“手机销量数据”“手机出货情况分析”很可能被聚在一起,因为这些查询共享的核心词元是“手机”加一个出货/销量类指标词。聚类产出的是“查询模式”和“该模式的样本查询列表”。这些模式会拿到著名的“点击率验证”——系统统计每个模式内历史查询的点击率平均值,作为这个模式的基准表现。6.2 标签与规则生成拿到查询模式之后,系统开始尝试生成新标签和新规则。对于每个模式,系统从点击过的文档标题里抽取高频词,如果某个词在这个模式的点击文档中反复出现,而在非点击文档中很少出现,那就把它作为候选标签。候选标签需要过一个“可解释性检查”——简单来说,如果一个标签没有对应的中文名词解释或同义词列表,就不被认为是可信的。这一步的目的是防止系统学到完全脱离人类认知的“伪标签”。同时,规则生成器会对比“模式A的原始查询结果点击率”和“模式A改写后的查询结果点击率”。如果改写的效果显著更好,就生成一条以该模式为条件的新规则;如果改写效果不佳,就把已有规则标记为需要人工复核。Interaction with对话过程中的输出在这里特别值得注意:夜间循环永远不要做“大爆炸式更新”。每次只变更一部分标签和规则,保留大部分稳定状态。因为如果一夜之间更新了几百条规则,第二天搜索结果体验可能会剧烈波动。6.3 实体关系图边权更新第三件事相对简单:把白天快环累积的实体关联频次写入长期表,重新计算每条边的权重。计算方式不是单纯累加,而是带上了衰减因子——早期积累的关联数据权重递减,最近的数据权重递增。这样做的好处是实体关系图能及时捕捉趋势变化:某个产品在近期的检索热度被持续验证,它的关联权重就会持续上升;一个半年没人碰过的关联会自动沉底,不再出现在扩展召回里。这个衰减机制我强烈建议做自进化的朋友都加上。如果没有衰减,早期数据会像“僵尸权重”一样长期霸占系统,导致系统无法适应内容或用户习惯的变化。6.4 人工复核机制夜间循环跑完后,所有新生成的标签和规则不会直接上线,而是进入一个待复核区。我设定的默认规则是:标签词典的更新需要人工确认,查询重写规则更新需要人工确认,实体关系权重更新则完全自动——因为实体权重只是幅度变化,不涉及新语义或新逻辑,风险可控。人工复核的界面很简单,就是一个待审列表,每行给出候选标签的关键信息:标签词、出现次数、点击率、覆盖文档数、样例查询。运营人员花15分钟扫一遍,通过或驳回。这套流程虽然看起来“不够自动化”,但保证了系统的语义进化方向不会偏离人类预期。整个夜间循环的执行时间,在百万级文档的规模上大约是20到30分钟。我没有把它做得更快,因为它并不在实时检索链路上,没有必要和时间较劲。稳定性比速度重要。7. 这套架构的真正边界:哪些场景不适合把Sirchmunk吹得再好也不合适,它有很多明确的局限和边界。我在这篇文章的结尾部分,想把这些边界讲清楚,免得有人拿它去做不该做的事情。7.1 不适合超大规模或高度动态的搜索场景如果搜索的文档规模达到上亿级别,且每天有千万级的新文档流入,我建议你还是老老实实用向量数据库或者商业搜索引擎。Sirchmunk的标签倒排表和实体关系图在千亿级关联下会产生很大的存储和计算压力,而且离线重建索引的周期会变得越来越长,最后无法满足内容时效性。它的舒适区间是百万到千万级的垂直领域内容集:行业报告、产品库、知识库、文档中心。这个规模下,传统索引的性能几乎可以用“碾压”来形容,而且成本优势非常明显。7.2 不适合完全依赖语义且表达高度模糊的场景如果用户的查询几乎不包含任何关键词——比如用户只输入“那个东西”,单靠标签和实体关系是做不了语义消歧的。这类场景需要真正的多模态语义理解和上下文推理,向量模型仍然是最好的工具。Sirchmunk能在“语义丰富但表达多样”的搜索场景里做到良好的效果,但做不到“无中生有”。我到现在还记得第一次给一个同事演示这套系统时,他随口问了一句:“我把上周看到的那个东西找出来。”系统回车返回的结果当然是乱七八糟。他哈哈一笑说“你看还是得靠人啊”。这句话反而让我放心——说明系统不会在它不理解的查询上硬编一些虚假的“智能感”。7.3 历史经验总结Sirchmunk造出来的最大收获,不是证明了“向量数据库不是必须的”,而是证明了一个朴素道理:搜索系统能力的上限,取决于你有没有一套可靠的手段,把用户和文档之间的关联知识持续提取出来并反馈到检索链路里。技术选型只是手段,反馈闭环才是灵魂。如果你正在设计一个类似的系统,我的建议是:先不要纠结要不要上向量数据库,先回答三个问题。第一,我的行为日志是否完整可用了?第二,我的反馈信号是否有办法做质量控制?第三,我有没有能力定期人工复核系统的自学结果?这三个问题没有答案之前,任何架构设计都是空中楼阁。我自己在后续的迭代中,已经在尝试把Sirchmunk的标签倒排表和向量召回做混合——保留向量召回作为一路补充信号,但主力仍然放在可解释的结构化索引上。这个混合版本的实验数据目前很令人鼓舞,等跑得更稳一点,我会写深度解析(二)详细展开。
1. 为什么我最终放弃了向量数据库:一个搜索系统的现实选择做搜索引擎的人,这两年大概都被问过一个灵魂问题:你上向量数据库了吗?说实话,最早拿到这个项目需求时,我也按“标准答案”走过——接一个嵌入模型,把文档全部向量化,然后搞个近似最近邻检索,看起来又潮又稳。但真正在数据量、实时性和成本三重压力下跑过一段时间后,我彻底改变了思路。Sirchmunk这个项目,最初的定位是一个面向垂直领域的内容检索系统。这个垂直领域的特点是:文档数量不是互联网级别,但查询模式极其复杂——用户会用各种口语化的说法去搜索同一类东西,而且新内容每天都在进来。用向量数据库一开始确实很爽,嵌入、召回、排序,流程顺畅。可问题也随之而来:千亿级参数模型跑出来的向量不是白来的,每个文档都要过一遍嵌入模型,每天增量更新还要重新计算;商业化向量数据库的按量计费在百万文档级别还勉强能忍,但到了千万级、上亿级,成本就是一笔让人头疼的开销。更重要的是,向量检索是个黑盒,用户搜不到东西时,你根本没法解释到底哪个环节出了问题。真正让我下决心推倒重来的,是一个很简单的场景。系统里存了一批行业报告,用户想搜“今年第二季度的市场份额变化”。传统的词法检索可以轻松命中“市场份额”“第二季度”这些关键词,但向量检索只按语义相似度找邻居,如果不做特别处理,匹配到的文档可能语义相近但根本不是用户要的那一篇。调试一个这样的个例就要花掉半天时间,因为我得去查向量距离是怎么算的,嵌入模型为什么把这几个文档拉到了一起。这太痛苦了。于是我开始思考一个看起来有点“复古”的问题:有没有一种方案,不需要向量数据库,也能实现语义级别的搜索,同时还能让系统越用越聪明?这个问题的答案,就是Sirchmunk今天这版架构的核心——用传统全文索引导出的结构化语义特征,加上一套基于用户行为的自反馈循环,让系统在运行中持续进化。这篇文章我会把整个架构设计拆开讲清楚,包括每个部件为什么存在、数据怎么流转、进化机制怎么落地,以及这套方案踩过的坑和已经明确的边界。如果你也在做中小规模的搜索系统,或者被向量数据库的成本和黑盒问题困扰,这版思路应该能给你一些直接的参考。即便你已经在用向量检索,这套架构里的反馈闭环设计也可以单独拿出来借鉴。2. Sirchmunk的架构总览:四层模型与两条数据链在详细展开之前,我先给出一张全局蓝图——注意这里没有流程图,我用文字把关系描述清楚,你可以在脑内建模。2.1 四个层次各自的职责我把整个系统拆成四个大的层次,每一层只做自己那一摊事,层级之间通过明确的数据接口通信。第一层是采集与清洗层。它负责从多个内容源拉取原始文档,然后做去重、格式统一、正文抽取、语言检测这些基础工作。输出是一个干净的结构化文档对象,字段包括标题、正文、发布时间、来源、标签等。这套系统里的“文档”比你想象的要“瘦”,我们不保留网页脚本、导航之类的噪音,只要对检索有意义的正文信息。第二层是存储与索引层。原始文档存档在对象存储里,同时写入一个核心的全文索引——我用的是Lucene体系。索引在这里不只是用来做关键词匹配的,它同时是后面几张“衍生表”的物化基础。除了索引,这一层还维护了一张实体关系图和一个标签倒排表。实体关系图记录的是文档中提到的人、机构、产品、概念之间的关联关系;标签倒排表则是一个“标签指向哪些文档”的映射表。这两个东西都不是靠外部图数据库,而是直接建立在索引文件和排序表之上,查询时合并结果。第三层是检索与排序层。所有面向用户的查询都经过这一层处理。它有四个子模块:查询解析模块、召回模块、重排序模块、日志记录模块。查询解析模块负责把用户输入的自然语言切割成可检索的词元、短语和标签意图;召回模块先跑经典的关键词检索,再并行做标签匹配和实体邻域扩展;重排序模块把多路召回的结果用一套加权公式揉在一起;日志记录模块把每次查询的完整上下文记录下来——包括解析结果、召回集、排序结果、用户最终点击了什么。第四层是进化层。这是Sirchmunk和普通搜索引擎最不一样的地方。它定期扫描行为日志,从中挖掘新的标签词、高频查询模式、效果不佳的排序规则,然后自动更新标签倒排表、实体关系权重和查询重写规则。这层没有做成在线实时计算,而是一个批量进化的离线工作流,因为实时计算对基础设施要求太高,而且很多反馈信号本身就有噪音,等积累到一定数量再做统计更有意义。2.2 两条数据链:检索链和进化链整个系统的数据流其实只有两条链。第一条叫检索链,是用户查询进来的路径。Query进入查询解析模块,产出词元和标签;召回模块在索引和标签表里各拉一批候选;重排序模块融合多路结果并输出Top列表;列表渲染给用户的同时,用户操作行为被写成日志。这条链的延迟要求很高,核心检索部分必须控制在几十毫秒以内,所以我尽量不在实时路径上做重量级计算。第二条叫进化链,是反馈驱动的路径。行为日志批量汇入进化层;进化层做统计分析,生成或更新标签词典、查询重写规则、实体关系权重;这些产物被写回索引层,供下一次检索链使用。进化链是异步、离线、批量的,它不追求实时性,只追求“每一次更新都比上一次更准”。为什么要把进化做成离线的?这里有一个工程上的现实:在线自进化意味着每次检索都要等待一个额外的在线学习动作,这在延迟上和稳定性上都是灾难。离线批量更新虽然“慢半拍”,但可以充分利用夜间空闲的计算资源,而且一次批量更新可以同时解决上千条查询模式的优化,性价比远高于在线逐条更新。3. 不依赖向量数据库,靠什么做出语义效果这一节回答一个核心质疑:你说了半天不用向量数据库,那语义匹配怎么办?总不能用“精准匹配关键词”糊弄人吧?当然不是。Sirchmunk的语义能力来自三个非向量部件:标签倒排表、实体关系图、查询重写规则。3.1 标签倒排表:把“语义”显式化为可检索的结构向量检索做的本质上是把“语义相似”映射成“向量近邻”。Sirchmunk不搞向量,那它把语义映射成了什么?映射成了“标签”。每一个文档在入库时,会通过一个规则引擎打上若干标签。规则引擎的输入是文档的标题、正文和初始元信息,输出是一组标签词及对应的置信度。举例来说,一篇标题为“2025年Q2智能手机市场分析报告”的文档,规则引擎会打这些标签:智能手机、市场分析、季度报告、2025Q2、行业数据。这些标签被写入标签倒排表——这是一个形如“标签词→文档ID列表”的映射表。检索时,如果用户的查询解析出一组标签,系统直接在表里做交集和并集运算,效率比向量近似检索还快。但问题来了:规则引擎凭什么知道该打什么标签?答案是用一个可维护的标签词典。词典初始版本由业务分析师手工构建——把领域内的核心概念、同义词、上下位词全部列出来。之后,进化层会持续往词典里补充新词条,这就是自进化的入口之一。这套机制的优势在于可解释。任何一个文档被打上某个标签,你都能追溯到规则引擎里的具体条件;用户搜不到东西时,你可以直接看标签表里有没有对应的标签。向量系统的黑盒问题在这里完全不存在。3.2 实体关系图:让“关联”成为可计算的信息标签能解决“这篇文档讲什么”的问题,但解决不了“这个实体跟哪些实体有关”的问题。举个例子:用户在历史检索里经常把“A产品”和“B技术”放在同一个查询里搜,说明这两者在用户认知里是关联的。这条关联信息如果只存在于日志里,进不了检索环节,那就没法帮助后续查询。Sirchmunk在索引层维护了一张轻量的实体关系图。这张图的节点是实体(品牌名、技术名、人物名等),边是共现关系,边权由共现频次和用户行为共同决定。它不是“存储”在一张图数据库里,而是以三列结构存在索引文件里:源实体、目标实体、权重值。检索时,如果查询命中一个实体节点,系统会找到它的邻居实体,再用邻居实体去倒查相关文档,作为召回路的一个扩展分支。这里我特别想强调一个工程细节:实体关系图这张“图”不是给用户看的,它是给召回模块看的。用户搜“A产品”,召回模块除了直接命中A产品相关文档外,还会沿着实体关系图找到与A产品关联度高的“B技术”,把B技术的文档也拉回来,放在候选集里。重排序环节会判断这些扩展结果到底值不值得放到最终列表里。3.3 查询重写规则:让用户问法不再是一个难题用户搜索同一个东西,表达方式千差万别。“Q2智能手机出货量”“二季度手机卖了多少台”本质上是一个意思,但词法层面完全不同。传统搜索引擎靠同义词扩展词典,Sirchmunk的进化层做的是更高级的事情——它不断从行为日志里学习“哪些查询模式是同一个意图,该怎么重写成标准查询”。重写规则以“模式→标准查询”的形式存在。模式侧记录的是查询词元的组合、词序、间隔;标准查询侧是经过验证的、能召回好结果的规范化格式。规则库可以由人工初始化,进化层会在后续运行中自动挖掘新的模式,或者修正已有规则的置信度。重写模块作用的位置在查询解析之后、召回之前。如果系统检测到用户查询匹配了某条模式,就用标准查询替换原查询继续走检索流程。这里的关键设计是“不替换原始查询,只替换检索用的查询”——用户看到的还是自己输入的原话,但系统内部检索的是改写后的标准查询。这样做的好处是规则错了也只会影响结果排序,而不会让用户觉得系统“乱改了我的话”。这三件套合在一起,效果上其实覆盖了向量检索能做的大部分场景,但每一步都是可解释、可干预的。更关键的一点是,它们的更新机制完全建立在系统自己的运行数据上——这正好接上了下一节讲的自进化。4. 自进化机制:系统如何越用越聪明我见过很多号称“智能”的系统,所谓的智能其实只是初始化时灌了很多规则,上线后就不再更新。Sirchmunk的自进化,是指系统的检索能力会随着持续运行显著提升——它不是“假装聪明”,而是真正在“变聪明”。4.1 三个进化环路:快、中、慢自进化不是在同一个节奏上跑一个巨大的更新任务,而是拆成三个节奏完全不同的环路:第一个是快环,更新周期是分钟级。它只做一件事情:实时更新实体关系图的边权。用户点击某个搜索结果,系统会在日志里记录“查询实体”和“被点击文档的实体”之间的关联次数。这些次数实时累加,直接反映在实体关系图的权重上。这个环路的计算量很小,甚至可以放在Web服务进程的内存里定期刷盘。效果是:当用户反复通过某条关联路径找到目标文档时,下一次这条路径的优先级会立刻上升。第二个是中环,更新周期是小时级。它处理的是标签词典的增量更新。系统每小时扫描一次新增的行为日志,统计高频查询词。如果一个查询词在日志中反复出现,但标签词典里还没有它,系统会把它标记为“候选标签”。候选标签要经历一个验证过程:看看这个标签能否从日志里找到足够的点击验证,如果能,就正式加入标签词典,并自动回填到历史文档上。这样系统就学到了一个新的“语义锚点”。第三个是慢环,更新周期是日级。它处理的是查询重写规则的生成与优化。每天夜间,系统跑一遍全量行为日志分析,聚类出高频查询模式,对每个模式计算当前检索效果。如果某个模式的点击率远低于平均水平,说明现有规则处理不了它,系统就尝试生成一条新的重写规则;如果某条规则改写后的查询点击率比改写前低,规则就会被降权或标记待修订。这三条环路并不是从第一天上线就全部有数据可跑,快环需要实体关系有初始边,中环需要行为日志累积,慢环需要足够多的查询样本来做统计判断。所以Sirchmunk的上线过程是“冷启动”的:先用人工初始化的词典和规则跑起来,然后逐步放开三个环路,让它自己变聪明。4.2 反馈信号的质量控制:不能把噪音当信号自进化系统的最大风险是“学习到了错误的东西”。用户点了一个结果,不代表这个结果就是最优的;用户搜了一个词,不代表这个词就该进词典。如果系统不加筛选地消化所有行为数据,很快会被噪音带偏。我设计了一个简单的信号质量门槛,每个反馈事件都要经过三道筛选:第一道是行为完整性检查。点击事件必须包含查询、候选列表、耗时、最终点击位置这些信息。少任何一项就不入库——因为少了一个字段,后续统计时就有偏差。比如没有记录候选列表的话,你没法判断用户点击的是“最优结果”还是“唯一结果”。第二道是行为有效性检查。用户在结果页上停留少于3秒就返回,或者查询后立即翻页但不点击,这类行为被视为无效信号。它们可能意味着用户没找到想要的东西,直接拿去做“这个查询应该召回什么”的学习样本,会产生误导。第三道是标签一致性检查。只有当同一个查询模式在行为日志里反复出现,并且对应的点击结果具有高度的标签重合度时,系统才认为这个模式值得生成一条新的重写规则或标签。单次行为的置信度太低,必须靠次数来累积置信度。4.3 人机协作为进化护航纯自动进化可能存在一个盲区——系统没有“品位”,它只会统计性地优化点击率,但点击率高不代表结果质量高。比如用户想搜“手机”,如果首页全是促销广告且用户愿意点,系统可能会把广告结果排得更靠前——这对搜索体验是灾难性的。为了解决这个问题,我给进化层留了一个“人工确认岗”。所有由中环和慢环生成的新标签、新规则,默认状态是“实验性”。它们在系统里可以参与排序,但权重打折。每周由运营人员巡检一次候选池,确认后的规则转正为“线上规则”,权重恢复正常。这个环节看似“不自动化”,其实是保留一条安全底线。如果完全自动化而不管内容质量,自进化就会走向“过度拟合点击率”的深渊。这个设计我特别想推荐给要做自进化系统的同行:永远不要把一个完全无人干预的闭环放上线。你可以把人工确认的周期拉得很长,甚至一个月看一次,但一定要有。5. 从Query到Top5:一次检索的完整旅程讲了这么多部件,现在把它们串起来,看一次真实查询在Sirchmunk里是怎么走的。这节我按时间顺序逐步拆解,每一步都给出对应的数据处理。5.1 第一步:查询解析用户输入“今年Q2手机出货量国产”进入系统。查询解析模块先做词法切分,得到原始词元:今年、Q2、手机、出货量、国产。然后做归一化:Q2被映射为标准季度表达“2025Q2”,国产被映射为标签“国产手机厂商”。这一步还会调用查询重写规则的匹配,看看有没有现成的模式能把这个查询改写成标准格式。如果匹配到一条“时间词+产品词+指标词+属性词”的通用模式,系统会把检索查询改写为“2025Q2 智能手机 出货量 国产手机厂商”。注意,这里的改写结果不会展示给用户,只进入检索阶段。5.2 第二步:多路召回改写后的查询并行走三路召回:第一路是全文索引召回。用Lucene对核心词元做布尔检索,返回按BM25相关性排序的文档集。这一路负责“精准命中”——文档包含尽可能多的查询词。第二路是标签匹配召回。查询中的“智能手机”“出货量”“国产手机厂商”都是标签词,系统直接在标签倒排表里取交集,得到同时命中这些标签的文档集。这路负责“语义命中”——文档可能没有完整出现所有关键词,但标签上完全对得上。第三路是实体扩展召回。查询解析时识别出实体“手机”,实体关系图返回它的核心邻居。加入第二季度的出货数据、主要品牌表现等关联文档。这路负责“关联发现”——用户没有明说,但大概率相关的文档。三路结果合并成一个候选集,每篇候选文档都有一个或多个来源标记,对应它被哪条路径召回的。这些标记在重排序时很有用。5.3 第三步:融合重排序候选集里可能有几百篇文档,但最终只展示5个位置。重排序模块用一套加权公式给每篇文档打分:总分 = 0.5 × BM25得分 + 0.3 × 标签匹配得分 + 0.2 × 实体扩展得分 + 规则加成前两项都是标准计算。标签匹配得分的算法比较直接:查询标签数与文档标签数的重合比例,重合越多分越高。实体扩展得分看的是文档与查询实体的关联路径强度——如果一篇文档和查询实体之间的边权很高,说明它被用户验证过是关联的。规则加成是一个可扩展位。当某个查询模式匹配到一条实验性重写规则时,加成值为正或负,影响排序结果。这套公式的好处是每个系数都能单独调,而且在日志里可以看到每一路的具体得分,排查问题非常简单。5.4 第四步:日志记录与闭环Top5结果返回给用户后,系统记录这次查询的完整日志:原始查询、改写后的查询、三路召回各自的候选列表和得分、最终排序列表。用户看到结果后,可能点击了第二位或第五位,也可能谁都没点直接改搜索词。所有这些行为都会被快环和中环消化,成为下一次自进化的原料。有个细节我想强调:日志记录一定要做全量记录,不要做抽样。搜索系统的行为日志是所有进化分析的唯一数据源。抽样看似省了存储,其实丢掉的往往是长尾行为——而长尾行为恰恰是自进化最有价值的输入。这一节整体走下来,你会注意到每一路召回的结果都是可复现、可解释的。用户问为什么搜不到,你可以把三路召回的候选都拉出来,看清楚是哪一路缺席了。这是向量检索方案给不了你的调试体验。6. 每日进化循环:系统在夜间如何自我迭代Sirchmunk的进化并不是随时都在发生,日常的“自我迭代”集中在一个每天触发的离线工作流里。我给这个流程起的代号是“夜间循环”。整个流程按顺序做四件事。6.1 日志聚合与查询模式挖掘夜间第一步,把当天所有的查询日志按查询文本聚合,剔除单个用户刷出来的低频查询,得到高频查询集合。然后用N-gram重叠算法做模式聚类——把看似不同但实际相似度很高的查询聚成一个模式。比如“手机出货量”“手机销量数据”“手机出货情况分析”很可能被聚在一起,因为这些查询共享的核心词元是“手机”加一个出货/销量类指标词。聚类产出的是“查询模式”和“该模式的样本查询列表”。这些模式会拿到著名的“点击率验证”——系统统计每个模式内历史查询的点击率平均值,作为这个模式的基准表现。6.2 标签与规则生成拿到查询模式之后,系统开始尝试生成新标签和新规则。对于每个模式,系统从点击过的文档标题里抽取高频词,如果某个词在这个模式的点击文档中反复出现,而在非点击文档中很少出现,那就把它作为候选标签。候选标签需要过一个“可解释性检查”——简单来说,如果一个标签没有对应的中文名词解释或同义词列表,就不被认为是可信的。这一步的目的是防止系统学到完全脱离人类认知的“伪标签”。同时,规则生成器会对比“模式A的原始查询结果点击率”和“模式A改写后的查询结果点击率”。如果改写的效果显著更好,就生成一条以该模式为条件的新规则;如果改写效果不佳,就把已有规则标记为需要人工复核。Interaction with对话过程中的输出在这里特别值得注意:夜间循环永远不要做“大爆炸式更新”。每次只变更一部分标签和规则,保留大部分稳定状态。因为如果一夜之间更新了几百条规则,第二天搜索结果体验可能会剧烈波动。6.3 实体关系图边权更新第三件事相对简单:把白天快环累积的实体关联频次写入长期表,重新计算每条边的权重。计算方式不是单纯累加,而是带上了衰减因子——早期积累的关联数据权重递减,最近的数据权重递增。这样做的好处是实体关系图能及时捕捉趋势变化:某个产品在近期的检索热度被持续验证,它的关联权重就会持续上升;一个半年没人碰过的关联会自动沉底,不再出现在扩展召回里。这个衰减机制我强烈建议做自进化的朋友都加上。如果没有衰减,早期数据会像“僵尸权重”一样长期霸占系统,导致系统无法适应内容或用户习惯的变化。6.4 人工复核机制夜间循环跑完后,所有新生成的标签和规则不会直接上线,而是进入一个待复核区。我设定的默认规则是:标签词典的更新需要人工确认,查询重写规则更新需要人工确认,实体关系权重更新则完全自动——因为实体权重只是幅度变化,不涉及新语义或新逻辑,风险可控。人工复核的界面很简单,就是一个待审列表,每行给出候选标签的关键信息:标签词、出现次数、点击率、覆盖文档数、样例查询。运营人员花15分钟扫一遍,通过或驳回。这套流程虽然看起来“不够自动化”,但保证了系统的语义进化方向不会偏离人类预期。整个夜间循环的执行时间,在百万级文档的规模上大约是20到30分钟。我没有把它做得更快,因为它并不在实时检索链路上,没有必要和时间较劲。稳定性比速度重要。7. 这套架构的真正边界:哪些场景不适合把Sirchmunk吹得再好也不合适,它有很多明确的局限和边界。我在这篇文章的结尾部分,想把这些边界讲清楚,免得有人拿它去做不该做的事情。7.1 不适合超大规模或高度动态的搜索场景如果搜索的文档规模达到上亿级别,且每天有千万级的新文档流入,我建议你还是老老实实用向量数据库或者商业搜索引擎。Sirchmunk的标签倒排表和实体关系图在千亿级关联下会产生很大的存储和计算压力,而且离线重建索引的周期会变得越来越长,最后无法满足内容时效性。它的舒适区间是百万到千万级的垂直领域内容集:行业报告、产品库、知识库、文档中心。这个规模下,传统索引的性能几乎可以用“碾压”来形容,而且成本优势非常明显。7.2 不适合完全依赖语义且表达高度模糊的场景如果用户的查询几乎不包含任何关键词——比如用户只输入“那个东西”,单靠标签和实体关系是做不了语义消歧的。这类场景需要真正的多模态语义理解和上下文推理,向量模型仍然是最好的工具。Sirchmunk能在“语义丰富但表达多样”的搜索场景里做到良好的效果,但做不到“无中生有”。我到现在还记得第一次给一个同事演示这套系统时,他随口问了一句:“我把上周看到的那个东西找出来。”系统回车返回的结果当然是乱七八糟。他哈哈一笑说“你看还是得靠人啊”。这句话反而让我放心——说明系统不会在它不理解的查询上硬编一些虚假的“智能感”。7.3 历史经验总结Sirchmunk造出来的最大收获,不是证明了“向量数据库不是必须的”,而是证明了一个朴素道理:搜索系统能力的上限,取决于你有没有一套可靠的手段,把用户和文档之间的关联知识持续提取出来并反馈到检索链路里。技术选型只是手段,反馈闭环才是灵魂。如果你正在设计一个类似的系统,我的建议是:先不要纠结要不要上向量数据库,先回答三个问题。第一,我的行为日志是否完整可用了?第二,我的反馈信号是否有办法做质量控制?第三,我有没有能力定期人工复核系统的自学结果?这三个问题没有答案之前,任何架构设计都是空中楼阁。我自己在后续的迭代中,已经在尝试把Sirchmunk的标签倒排表和向量召回做混合——保留向量召回作为一路补充信号,但主力仍然放在可解释的结构化索引上。这个混合版本的实验数据目前很令人鼓舞,等跑得更稳一点,我会写深度解析(二)详细展开。