在聊索引扫描的前置过滤器时, 先举个例子.
家里装过自来水过滤器的朋友一定知道, 过滤器分前置过滤器和后置过滤器, 前置过滤器一般装在入户的位置, 主要是过滤大颗粒杂质, 保障所有其他出水口不容易被堵塞. 后置过滤器主要用于过滤细粒度杂质, 例如厨房可能回安装直饮水过滤器.
如果把前置过滤器拿掉, 可能有好处, 也可能带来坏处, 和水质有关.
水质差, 大颗粒多, 拿掉肯定不好.
水质好, 拿掉可能有好处. 过滤器毕竟是影响水流量量多, 拿掉前置过滤器水通量可能变大.

下面我们来看看索引扫描的前置过滤和后置过滤.
很多人只知道索引扫描的后置过滤, 那么我们就来看看什么是索引扫描的后置过滤.
这条SQL的索引是items_pkey, 索引扫描到符合条件的记录后, 回表, 判断tuple可见性, 并且对可见tuple获得embedding字段的值, 再输入到后置过滤器得到满足vector_norm(embedding) < 0.5的tuple.
EXPLAIN (COSTS FALSE)
SELECT * FROM items WHERE id <= 5000 AND vector_norm(embedding) < 0.5;
QUERY PLAN
--------------------------------------------------------------
Index Scan using items_pkey on items
Index Cond: (id <= 5000)
Filter: (vector_norm(embedding) < '0.5'::double precision)
什么是索引前置过滤呢? 只有索引扫描过程中需要多步计算时可能会用到前置过滤. 例如为了减少内存使用、加速裁剪等, 采用了rabitq量化和残差量化(rvq)分层结构的向量索引vectorchord, 这类索引扫描通常分多个步骤, 初筛、剪枝/精排等最后输出最终结果.
剪枝/精排可能涉及较大的向量计算运算量, 如果向量结果中很多tuple是不符合非索引条件(其他where条件)的tuple, 那么就可以考虑使用索引扫描的前置过滤器. 通常可能会被称为lazy vector compute(就是真正要计算向量时才计算).
例如这条SQL
EXPLAIN (COSTS FALSE)
SELECT * FROM items WHERE id % 97 = 0 ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
QUERY PLAN
-----------------------------------------------------
Limit
-> Index Scan using items_embedding_idx on items
Order By: (embedding <-> '[0,0,0]'::vector)
Filter: ((id % '97'::bigint) = 0)
索引扫描前置过滤器可能是这样的:

注意它依旧是索引扫描的行为, 并不是通过全表扫描去获取非向量字段的值.
为什么上面这条SQL要采用前置过滤器呢?
因为大量tuple不满足WHERE id % 97 = 0条件, 并且"索引计算(剪枝/精排)"这个步骤的运算量极大, 消耗大量cpu.
那么是不是所有的索引都支持前置过滤功能呢? 非也, 其实这个功能来自vectorchord插件.
下面的内容翻译自: https://docs.vectorchord.ai/vectorchord/usage/prefilter.html
在索引扫描过滤中,索引首先执行扫描,然后 PostgreSQL 检查是否满足过滤条件。“过滤器”指的是 PostgreSQL 查询计划中索引扫描节点上的过滤器。这通常由SQL 语句中的WHERE子句构成。过滤器不需要具有特定的形式。
EXPLAIN (COSTS FALSE)
SELECT * FROM items WHERE id <= 5000 AND vector_norm(embedding) < 0.5;
QUERY PLAN
--------------------------------------------------------------
Index Scan using items_pkey on items
Index Cond: (id <= 5000)
Filter: (vector_norm(embedding) < '0.5'::double precision)
然而,对向量搜索来说, 这并不总是最有效的方法。考虑以下查询。
EXPLAIN (COSTS FALSE)
SELECT * FROM items WHERE id % 97 = 0 ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
QUERY PLAN
-----------------------------------------------------
Limit
-> Index Scan using items_embedding_idx on items
Order By: (embedding <-> '[0,0,0]'::vector)
Filter: ((id % '97'::bigint) = 0)
假设ID分布均匀, 由于id % 97 = 0这个条件的存在, 检索10条满足过滤条件的向量,索引扫描大约要放大97倍, 也就是索引需要扫描970行。这表明索引执行了大量的冗余计算。如果索引可以利用过滤条件进行剪枝,计算量将显著减少。向量索引提供了一个 GUC 参数vchordrq.prefilter,允许根据过滤条件对搜索空间进行剪枝。
SET vchordrq.prefilter = on;
预过滤使向量索引能够根据过滤器执行搜索。这会精简搜索空间,而较小的搜索空间可以提高搜索效率。然而,检查过滤条件是否满足也会带来开销。因此,仅当过滤条件严格(剔除大量行)且计算成本低廉(计算成本远低于计算向量距离)时才建议使用预过滤。为了帮助理解,我们提供了两个错误的使用示例:
id % 97 > 0:这个过滤器不太耗cpu, 但是大多数是满足条件的. 用在前置过滤是不可取的.
email ~ '^([a-zA-Z]+)*$':这个过滤器是正则匹配, 可能比较耗费cpu(但是相比向量计算的cpu也许还好), 但是满足条件的tuple非常少. 所以用于前置过滤是可取的.
适合前置过滤的是:
根据我们的实验结果,不同选择性下的 QPS 加速如下:
1% 时加速 200%10% 时加速 5%
ps: vchordg不支持此功能。仅vchordrq支持预过滤.
参数
vchordrq.prefilter
vchordrq.prefilter GUC 参数允许在计算距离之前先进行条件评估。例如,在查询SELECT * FROM items WHERE id % 2 = 0 ORDER BY embedding <-> '[3,1,2]' LIMIT 5中,索引通常会先计算所有有用的embedding <-> '[3,1,2]'向量距离,然后将行传递给 PostgreSQL,后者会过滤掉符合条件id % 2 != 0的行。此参数允许索引在计算距离之前预先评估条件并丢弃不匹配的行,从而提高查询效率。
前置过滤节省了向量计算的CPU开销, 确没有节省其他条件计算带来的cpu开销和回表带来的IO.
其实还可以继续优化.
如果非索引条件是必选条件, 可能存在的更好方法是
https://deepwiki.com/search/set-vchordrqprefilter-on-true_8b1a7b21-b2b0-41b8-b7e2-0e4f9eabc495
https://docs.vectorchord.ai/vectorchord/usage/prefilter.html
《重新发现PostgreSQL之美 - 8 轨迹业务IO杀手克星index include(覆盖索引)》
《PostgreSQL index include - 类聚簇表与应用(append only, IoT时空轨迹, 离散多行扫描与返回)》