5.1 查询类型与执行流程 进入性能工程的第一步是看清执行路径。本节承接第五章开篇的"修理链":先把常见查询类型分清楚——它们的执行成本天差地别;再沿着一次检索在引擎内部的完整流程走一遍,用时序图和漏斗图把延迟的坐标系立起来。旧版教程的"查询类型与语义""查询规划与执行流程"两节在此合并,因为"有哪些查询"与"查询怎么跑"本是同一枚硬币的两面。 四类查询,四种成本 纯近邻查询是最基础的形态:给向量、要 TopK、不带附加条件,成本约等于索引的搜索开销。阈值查询要求返回距离小于某阈值的所有结果,条数不定——它把"取前 K 条"变成了"取到不够好为止",执行器无法预知扫描量,延迟方差大,监控时要单列。
进入性能工程的第一步是看清执行路径。本节承接第五章开篇的"修理链":先把常见查询类型分清楚——它们的执行成本天差地别;再沿着一次检索在引擎内部的完整流程走一遍,用时序图和漏斗图把延迟的坐标系立起来。旧版教程的"查询类型与语义""查询规划与执行流程"两节在此合并,因为"有哪些查询"与"查询怎么跑"本是同一枚硬币的两面。
纯近邻查询是最基础的形态:给向量、要 TopK、不带附加条件,成本约等于索引的搜索开销。阈值查询要求返回距离小于某阈值的所有结果,条数不定——它把"取前 K 条"变成了"取到不够好为止",执行器无法预知扫描量,延迟方差大,监控时要单列。带过滤的近邻查询是生产环境的主力:在 TopK 之上叠加标量条件(类目、租户、时间窗、状态),它的成本高度依赖过滤策略,下文单讲。混合检索同时跑向量与关键词两路,再用融合算法归并,成本约等于两路之和加融合开销,专治嵌入模型在专有名词、型号代码上的盲区。

以带过滤的近邻查询为例,完整流程依次是:接入层鉴权与限流;协调器解析请求、制定计划,确定扇出到哪些分片;各分片执行搜索;归并全局 TopK;必要时做后处理。两个环节最值得深究。一是过滤策略:先搜后滤(搜出 TopK 再核对条件)在过滤条件宽松时高效,条件严格时会"滤到最后凑不够 K 条";先滤后搜(先按标量条件圈出子集再搜)结果正确,但子集大时接近全量扫描。主流系统的解法是过滤感知的索引——在图遍历或倒排探测过程中边搜边滤,让过滤几乎不额外付费,选型时(第六章)值得把"强过滤下的性能曲线"作为硬性验证项。二是归并:各分片返回局部 TopK 后全局归并,分片越多归并开销越大,且全局延迟受最慢分片拖累——这是 4.2 结论在查询侧的回响。
下面是这趟旅程的时序账:
# 一次带过滤检索的标准写法(伪代码) result = collection.search( vector=embed("防水的轻便折叠伞"), # 查询向量 top_k=50, # 故意超量取回,见 5.2 filter={ "category": {"in": ["雨具", "户外配件"]}, "status": {"eq": "on_sale"}, "price": {"lt": 120}, }, output_fields=["title", "price"], # 只要业务字段,别拖回整行 ) # 返回按距离升序的五十条;后续重排层再裁到最终展示的十条
会话里有两个刻意的细节:top_k 取五十而不是最终要展示的十条,为的是给过滤与重排留出损耗余量——先搜后滤策略下尤其重要;output_fields 限定返回字段,避免把载荷整体拖回抬高网络与序列化开销。这两处小习惯在生产代码里能省掉大量"莫名其妙的慢"。
执行流程图最大的实用价值,是给延迟分解提供分段依据。在客户端与各环节埋点,把一次查询的耗时拆成嵌入编码、网络往返、引擎执行、结果回传四段,"慢"就从一个抱怨变成了四个可归因的数字:
import time def timed_search(collection, embed, text, top_k=50): t0 = time.perf_counter() vec = embed(text) # 段一:嵌入编码 t1 = time.perf_counter() res = collection.search(vector=vec, top_k=top_k) # 段二加三:网络加执行 t2 = time.perf_counter() return { "embed_ms": (t1 - t0) * 1000, "engine_ms": (t2 - t1) * 1000, # 含网络往返,需在服务端细分 "result_n": len(res), } # 连续采样数百条后分段取 P95:通常会发现嵌入段被严重低估
实践里最常见的发现是段一被低估:嵌入编码在文本较长或服务过载时轻松吃掉几十毫秒,而团队一直在索引参数上打转。分段计时的纪律是服务端与客户端各埋一层——客户端看端到端,服务端看引擎内部(解析、搜索、归并的分段),两层一对,慢在哪一段立刻现形。这个脚本配合 5.5 的基准框架,就是性能诊疗的听诊器。
这是过滤策略最经典的坑,解法按代价递增有四档。第一档,超量取回:把搜索的 K 放大数倍再过滤截断,条件不太严时成本可忽略,是默认首选。第二档,动态放大:检测到过滤后结果不足时自动翻倍重试,对突发严条件有弹性,代价是长尾延迟抬升。第三档,过滤感知搜索:切换到边搜边滤的执行模式,让索引跳过不满足条件的候选,多数现代产品已内置,要在选型时验证严条件下的表现。第四档,倒排预筛:把强条件交给标量倒排先圈出子集,向量检索只在子集内进行,条件极严时反而是最快路径。四档不是互斥的,成熟的系统会按过滤选择率自动在档位间切换——评估产品时问一句"过滤策略是怎么选的",能听出实现深度。
语义上可以(设了阈值就返回所有达标项),工程上最好分开。阈值查询的返回条数不可预估,延迟方差大,与 TopK 查询共享连接池与超时配置时会互相干扰——一批深阈值查询能把队列里的普通查询一起拖慢。分开的方法可以是独立接口、独立超时预算,或至少在监控里把两类查询的延迟分开统计。见过太多"P99 突然烂掉"的事故,最后发现是某条全库阈值查询在队列里排队。
查询路径看清了,下一节处理路径的最后一环:结果拿回来之后,怎么把它变得更好。