本节摘要:纯向量搜索只回答"像不像",而真实业务几乎总要在"像"之外再加"符合条件"。Qdrant 的高级查询把向量相似度与载荷过滤拧在一起,让你能查"相似且便宜""相似且是新品"这类复合问题。本节讲清过滤与向量搜索结合的两条路径——先过滤再搜、以及把过滤嵌入 HNSW 图遍历的混合查询——再拆解载荷索引、布尔逻辑和范围、地理、全文几类条件的用法。
阅读完本节,你应当能够:
前面两节解决了"数据怎么存""相似怎么找",但一落到业务里,纯相似搜索常常不够用。用户要的不是"和这双鞋最像的十双鞋",而是"和这双鞋像、价格在一百到两百之间、有现货、品牌在 A 或 B 里的五双"。这里同时出现了两类约束:一类是软的相似度,一类是硬的结构化条件。软约束靠向量,硬约束靠载荷。
如果系统只支持向量搜索,我们就得先把相似结果全捞出来,再在应用层自己筛价格、筛库存。这有俩问题:一是捞出来的结果里可能绝大多数都不满足条件,白算一堆距离;二是为了凑够 K 个合格结果,你往往得先捞 K 的几倍甚至几十倍,召回和延迟都难看。Qdrant 的高级查询机制,就是把这些"硬条件"下沉到数据库内部,让过滤和向量搜索在同一趟查询里协同完成。
打个比方:纯向量搜索像只按"眼缘"给你推荐人,高级查询则像一位懂你硬指标的媒人——先卡掉年龄、城市、收入这些硬条件,再在剩下的人里挑"感觉对的"。媒人省下的不是一点半点。
再具体一点:假设你有一千万条商品,其中符合"价格低于一百"的只有五万条。如果先搜再过滤,你按相似度捞前一百名,很可能这一百名里价格全都超一百,过滤之后颗粒无收;你得把 K 拉大几十倍才可能凑够,延迟和召回一起失控。反过来,先过滤到五万条再搜,候选集缩小两百倍,又快又准。这就是为什么"先过滤"或"过滤嵌入"几乎总是优于"先搜后滤"。
把过滤和向量搜索结合,直觉上有两条路。
第一条是先过滤再搜:先用载荷索引筛出满足条件的点,得到一个缩小的候选集,再在这个子集里跑向量搜索。优点是候选集小了,向量距离计算量骤减;缺点是如果过滤条件太宽,子集还是很大,如果过滤条件太窄(比如全库只剩几十个点),HNSW 的图结构在这么小的子集里反而施展不开。
第二条是先搜再过滤:先跑向量搜索捞出一批最近邻,再对结果做载荷过滤。问题很明显——可能过滤完之后,真正合格的候选点根本不在这一批里,召回率掉得很厉害,尤其当过滤条件筛掉了大量点的时候。
Qdrant 的聪明之处在于,它在 HNSW 的图遍历内部就加入了过滤检查:贪心探索邻居时,每走一步都先看这个邻居的载荷是否满足条件,不满足就直接跳过,不纳入候选。这样搜索路径始终贴着"既相似又合格"的点走,既不像"先过滤再搜"那样在极端窄条件下瘫痪,也不像"先搜再过滤"那样在宽条件白算。这套机制叫"可过滤的 HNSW",是 Qdrant 混合检索的核心。
下面这张图把两条路径摆在一起对照,重点看右边的混合查询如何把过滤"嵌"进图的每一步。

过滤快不快,取决于载荷字段有没有建索引。常见的几类索引,对应几类条件:
| 载荷字段类型 | 索引类型 | 支持的查询 |
|---|---|---|
| 字符串 枚举 | 关键字索引 | 精确匹配 集合包含 |
| 整数 浮点 日期 | 数值范围索引 | 大于 小于 区间 |
| 布尔 | 布尔索引 | 真 假 |
| 经纬度 | 地理索引 | 矩形框 半径范围 |
过滤条件不是孤立的,Qdrant 用三个子句把多个条件组合成树:must 表示"必须满足"(与),should 表示"满足其一"(或),must_not 表示"必须排除"(非)。三者的嵌套能表达相当复杂的业务规则。
下面这段概念代码演示了一个带复合条件的搜索。
from qdrant_client import QdrantClient, models client = QdrantClient(":memory:") query_vector = [0.10, 0.55, 0.27] results = client.search( collection_name="products", query_vector=query_vector, limit=5, query_filter=models.Filter( must=[ models.FieldCondition(key="category", match=models.MatchValue(value="shoes")), models.FieldCondition(key="price", range=models.Range(gte=100, lte=200)), ], should=[ models.FieldCondition(key="brand", match=models.MatchValue(value="A")), models.FieldCondition(key="brand", match=models.MatchValue(value="B")), ], must_not=[ models.FieldCondition(key="in_stock", match=models.MatchValue(value=False)), ], ), )
除了布尔过滤,Qdrant 还提供几类"专用"条件:范围查询覆盖数值和日期时间;地理查询支持按矩形框或半径圈选点,是位置服务的刚需;全文搜索给字符串字段做轻量关键词匹配,虽不及专业搜索引擎,但对付元数据里的简单模糊匹配足够。此外,多向量查询能把多个查询向量融合成一个综合查询,分组去重能按某个字段把结果分组、每组只取得分最高的若干条,避免推荐结果扎堆同类。
地理索引让经纬度字段支持两种查询:矩形框找出落在给定经纬度范围内的点,半径查询找出离某个中心点一定距离内的点。外卖、打车、附近门店这类"位置驱动"的业务离不开它。地理坐标要按固定格式存,索引也要显式声明为地理类型,否则它只会被当成普通数值对待,圈选功能就用不起来。
有时一个查询背后有多个向量——比如用户同时输入几个关键词,或一张图配一句话。多向量查询把多个查询向量聚合成一个(取平均、加权,或更复杂的融合方式),再拿去搜索;也可以并行搜多次再把结果融合。它提升的是"查询表达能力",让系统能同时接收多个语义信号,而不是逼用户把意图压缩成单一句子。
推荐场景里,如果不做处理,搜索结果很可能前十名全是同一品牌的相似商品。分组功能按某个载荷字段(比如品牌或商品 ID)把结果分组,每组只返回得分最高的若干条,强制结果覆盖更多类目。去重则用于清理那些语义高度重复的条目。两者都是"结果多样性"的手段,代价是牺牲一点点纯相似度排序。分组键的选择直接决定多样性效果——按品牌分组能避免品牌扎堆,按品类分组则让结果跨品类覆盖。
结果集很大时,一次性返回会撑爆内存和网络。滚动接口按游标分批取,每次拿一批、记下位置,下次接着取。它比用偏移量翻页更稳——偏移量在数据变动时会重复或漏条,游标则锚定一个稳定位置。导出数据、离线分析、无限滚动列表,都该用滚动而不是偏移量。游标本身不依赖结果总数,这是它翻页稳定的根本原因。
问:过滤条件能不能作用在向量本身上?答:不能,过滤只作用在载荷上;向量负责相似度,载荷负责条件,两者分工明确。问:全文搜索能替代专业搜索引擎吗?答:不能,它只做元数据里的轻量关键词匹配,复杂的相关性排序、同义词扩展仍建议交给专业检索系统。问:一次查询能同时用多个索引吗?答:能,向量索引和多个载荷索引在同一趟查询里协同,这正是混合检索的本意。
过滤条件宽、筛掉比例低时,"先过滤再搜"和"过滤嵌入 HNSW"差别不大;条件很窄时,纯"先过滤再搜"可能把候选集缩得太小,HNSW 的图结构发挥不出来。Qdrant 的过滤嵌入 HNSW 在多数场景下是更稳的默认,因为它自适应地在图遍历里边搜边滤。真正要警惕的是"先搜再过滤"——它几乎总在宽条件或高 K 时掉召回,除非你的过滤条件极松、几乎不筛人。
和上一节载荷索引的结论一致:只给真正高频出现在过滤条件里的字段建索引。一个典型电商集合里,价格、类别、库存、上架时间值得建;而那些偶尔才用的字段,宁可让它走扫描。
结果集大时,用滚动接口按游标分批取,比用偏移量翻页稳得多——后者在数据变动时容易重复或漏条。这个习惯在导出数据、做离线分析时尤其重要。
⚠️ 常见坑:把复杂的范围条件写成一长串 should,结果发现查询越来越慢。OR 类条件(should)会把候选集放大,越多的"或"越接近全表扫描。能收窄的约束优先放 must,把 should 的数量压到最少,是让混合查询保持低延迟的关键。
💡 关键直觉:过滤的价值不是"让结果更少",而是"让相似度计算只花在值得算的点上"。把硬条件下沉到数据库内部、嵌进图遍历,本质是省掉那些注定被丢弃的点上的距离计算。理解这一点,你就知道为什么"先过滤再搜"和"过滤嵌入 HNSW"能显著快于"先搜再过滤"。
下一节我们转向"稳"字——数据存储与管理,看看段、预写日志、快照和副本是怎么让前面这些"快"的能力在断电和宕机面前依然站得住的。