向量库标量过滤与向量检索联合下推落地

日期:2026-08-16

一、项目背景

我们的检索服务建在向量库上,给一个行业知识库做语义搜索。业务那边几乎所有的查询都不是纯向量查询,基本都带着过滤条件,比如只要某个类目、某个时间之后的、某个价格区间的文档。最早我们的做法是先向量召回 top 500,再在内存里按这些条件过滤,觉得这样简单直观,研发也好理解。

跑了两个月问题就来了。带强过滤的查询,比如只要最近七天的文档,召回的 500 条里可能只有十几条满足,等于白算了一大堆,延迟还高。更糟的是,过滤太狠的时候,满足条件的文档根本没进 top 500,结果召回为空,用户搜什么都是空的,体验直接崩。那段时间客服收到不少投诉,说搜索变傻了,其实就是召回被过滤吃掉了。

二、落地场景

这个知识库覆盖几十个业务类目,文档带类目、创建时间、来源系统、权限标签这些标量字段。用户的真实查询往往是「类目等于 A 且时间大于某天 且 语义相似 top K」。我们要让向量检索和标量过滤一起生效,而不是先召回再砍。

落地上我们把标量字段单独建了索引,查询时把过滤条件下推到向量检索的执行计划里,让检索在遍历或者图遍历的过程中就顺手把不满足条件的候选丢掉,最后返回的就是已经过滤过的近邻结果。权限标签这个字段我们还做了特殊对待,因为不同用户能看到的范围不同,过滤必须强制下推,不能靠应用层事后过滤,否则有越权风险。

三、关键技术挑战与解决思路

第一个难点是过滤下推的正确性。向量索引尤其是基于图的那种,遍历过程不是全扫描,下推过滤之后要保证不漏掉真正该命中的。我们做了对比测试,把下推结果和暴力全量过滤的结果逐条对,确认在合理过滤率内结果一致,才敢切到生产。测试覆盖了几千条不同过滤率的组合,差一点我们就没敢上。

高过滤率下召回衰减是个真问题。当过滤条件很严,比如只保留千分之一的文档,图索引能走到的节点太少,召回会掉。我们的应对是把索引参数调成更密的连接,并且在过滤率超过阈值时自动降级到带过滤的暴力检索,用算力换召回,保证结果不空。这个降级开关我们默认开着,实测下来绝大多数查询走下推,只有极少数极端过滤才触发暴力,性能影响可控。

索引参数要权衡。连接数调密了召回稳,但内存和构建时间都涨。我们按类目数据量做了分级配置,大类目用更密的索引,小类目用默认,整体内存可控。构建时间也做了分批,避免一次性重建把服务卡死。

这个服务上线后我们加了一层查询质量监控,对每条带过滤的查询记录下推是否命中、走了下推还是降级、延迟多少,按类目和过滤率维度定时出报表。运营能直观看到哪些类目的过滤率偏高、是不是该调索引参数了。这个监控帮我们抓到过一次索引参数被误改导致召回掉的问题,在用户感知之前就修回去了。索引重建我们也做成了灰度,先在小类目验证新参数再全量推,避免一次改动影响全部查询。整体下来,带过滤检索从原来最容易被投诉的功能,变成了最稳的一块,这一点超出了我们当初的预期。

四、效果数据

带过滤查询的平均延迟从原来的三百多毫秒降到了八十毫秒以内,因为不用再召回一大堆然后丢掉。无效召回,也就是被过滤掉的比例,之前接近七成,现在几乎为零。过滤后召回率我们持续监测,在常用过滤率范围内和暴力基准对齐,没有出现搜不到的情况。权限标签下推之后,越权访问的隐患也顺手堵上了。

案例片段(已脱敏): 一段带过滤的查询配置(json 示意): {"vector": [...], "top_k": 10, "filter": {"category": "A", "created_after": "2026-01-01", "acl": "user_123"}, "pushdown": true, "fallback_bruteforce_rate": 0.001} 一次压测:过滤率 0.3% 的查询,原方案召回 0 条且耗时 340ms,下推方案召回 10 条且耗时 72ms;过滤率 5% 时两者召回一致,延迟由 210ms 降到 65ms。极端过滤率 0.05% 时自动降级暴力,召回保持满,耗时 120ms。

五、可复用经验总结

带过滤的向量检索,先召回再过滤是最容易踩的坑,我们一开始就是这么干的,看着简单,实际又慢又漏。把标量条件下推进索引,是这件事正确的打开方式。

高过滤率下的召回衰减要提前想好退路。我们设了自动降级到暴力检索的阈值,虽然单次贵一点,但保证用户搜得到东西,比返回空结果强太多。这个降级开关建议默认开启,别等线上爆了再补。

索引参数别全场一套。我们按类目规模分级配置之后,内存和延迟都更合理,这个思路在别的带过滤的检索场景也能直接用。还有权限字段一定要强制下推,别指望应用层兜底,那是最容易出越权的地方。