跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

使用指南

使用 vchordrq 与 vchordg 构建索引、MaxSim 多向量检索、量化类型、范围查询与性能调优。

1 - 索引构建

VectorChord 的 vchordrq 索引类型会将向量划分为多个列表,并仅搜索距离查询向量最近的一部分列表。它兼具极快的构建速度与低内存占用,同时性能显著优于 hnsw 和 ivfflat。

VectorChord 的索引类型 vchordrq 会将向量划分为多个列表(list),并仅搜索距离查询向量最近的一部分列表。它兼具极快的构建速度与低内存占用,同时在性能上显著优于 hnsw 和 ivfflat。

我们首先创建一个名为 items 的表,其中包含一个类型为 vector(n) 的 embedding 列,然后向其中填充示例数据。

CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3));
INSERT INTO items (embedding) SELECT ARRAY[random(), random(), random()]::real[] FROM generate_series(1, 1000);

要创建 vchordrq 索引,可以使用以下 SQL 语句:

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops);

索引构建完成后,您就可以使用它执行向量搜索,检索距离目标向量最近的 1010 个近邻。

SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

您还可以根据需要为向量搜索查询添加过滤条件。

SELECT * FROM items WHERE id % 7 <> 0 ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

调优

当表中的行数少于 100,000100,000 时,通常不需要设置索引选项。

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops);

SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

然而,随着行数的增长,对向量空间进行分区变得十分必要,这可以通过索引选项进行配置。options 使用 TOML(Tom’s Obvious Minimal Language) 格式的字符串指定。更多信息请参阅 #索引选项。

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
[build.internal]
lists = [1000]
$$);

SET vchordrq.probes TO '10';
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

参数 lists 应根据表中的行数进行调优。下表提供了选择合适取值的指导建议。在执行查询时,请相应地选择 vchordrq.probes。

行数 NN 推荐分区数 LL lists 示例
N∈[0,105)N \in [0, 10^5) N/A []
N∈[105,2×106)N \in [10^5, 2 \times 10^6) L=N500L = \frac{N}{500} [2000]
N∈[2×106,5×107)N \in [2 \times 10^6, 5 \times 10^7) L∈[4N,8N]L \in [4 \sqrt{N}, 8 \sqrt{N}] [10000]
N∈[5×107,∞)N \in [5 \times 10^7, \infty) L∈[8N,16N]L \in [8 \sqrt{N}, 16\sqrt{N}] [80000]

构建索引的过程包含两个步骤:首先对向量空间进行分区,然后将行插入到索引中。第一步(划分向量空间)可以使用多线程进行加速。

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
[build.internal]
lists = [1000]
build_threads = 8
$$);

SET vchordrq.probes TO '10';
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

第二步(插入数据行)可以使用多进程并行执行。详情请参阅 PostgreSQL 调优。

对于大多数使用余弦相似度的数据集,启用 residual_quantization 和 build.internal.spherical_centroids 可以同时提升 QPS 和召回率(recall)。

CREATE INDEX ON items USING vchordrq (embedding vector_cosine_ops) WITH (options = $$
residual_quantization = true
[build.internal]
lists = [1000]
spherical_centroids = true
build_threads = 8
$$);

SET vchordrq.probes TO '10';
SELECT * FROM items ORDER BY embedding <=> '[3,1,2]' LIMIT 10;

为了提高构建速度,您可以选择将 build.pin 设置为 2,通过使用更多共享内存来加速构建过程。

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
residual_quantization = true
build.pin = 2
[build.internal]
lists = [1000]
spherical_centroids = true
build_threads = 8
$$);

对于大表而言,时间和内存开销主要由分区阶段主导。有关进一步的调优方法,请参阅分区调优。

您也可以参考外部构建,将分区阶段卸载到其他机器上执行。

参考

操作符类(vchordrq)

vchordrq

下表列出了 vchordrq 支持的所有可用操作符类(Operator Classes)。

操作符类 说明 操作符 1 操作符 2
vector_l2_ops 适用于 vector 类型与欧氏距离 <->(vector,vector) <<->>(vector,vector)
vector_ip_ops 适用于 vector 类型与负内积 <#>(vector,vector) <<#>>(vector,vector)
vector_cosine_ops 适用于 vector 类型与余弦距离 <=>(vector,vector) <<=>>(vector,vector)
halfvec_l2_ops 适用于 halfvec 类型与欧氏距离 <->(halfvec,halfvec) <<->>(halfvec,halfvec)
halfvec_ip_ops 适用于 halfvec 类型与负内积 <#>(halfvec,halfvec) <<#>>(halfvec,halfvec)
halfvec_cosine_ops 适用于 halfvec 类型与余弦距离 <=>(halfvec,halfvec) <<=>>(halfvec,halfvec)
rabitq8_l2_ops 适用于 rabitq8 类型与欧氏距离 <->(rabitq8,rabitq8) <<->>(rabitq8,rabitq8)
rabitq8_ip_ops 适用于 rabitq8 类型与负内积 <#>(rabitq8,rabitq8) <<#>>(rabitq8,rabitq8)
rabitq8_cosine_ops 适用于 rabitq8 类型与余弦距离 <=>(rabitq8,rabitq8) <<=>>(rabitq8,rabitq8)
rabitq4_l2_ops 适用于 rabitq4 类型与欧氏距离 <->(rabitq4,rabitq4) <<->>(rabitq4,rabitq4)
rabitq4_ip_ops 适用于 rabitq4 类型与负内积 <#>(rabitq4,rabitq4) <<#>>(rabitq4,rabitq4)
rabitq4_cosine_ops 适用于 rabitq4 类型与余弦距离 <=>(rabitq4,rabitq4) <<=>>(rabitq4,rabitq4)
vector_maxsim_ops 适用于 vector[] 类型与可扩展的多向量相似度 @#(vector[],vector[]) N/A
halfvec_maxsim_ops 适用于 halfvec[] 类型与可扩展的多向量相似度 @#(halfvec[],halfvec[]) N/A
rabitq8_maxsim_ops 适用于 rabitq8[] 类型与可扩展的多向量相似度 @#(rabitq8[],rabitq8[]) N/A
rabitq4_maxsim_ops 适用于 rabitq4[] 类型与可扩展的多向量相似度 @#(rabitq4[],rabitq4[]) N/A

<->、<#> 和 <=> 是由 pgvector 定义的操作符。

名称 说明
<-> 欧氏距离的平方
<#> 负点积
<=> 余弦距离

<<->>、<<#>>、<<=>> 是由 VectorChord 定义的操作符。有关 <<->>、<<#>>、<<=>> 的更多信息,请参阅相似度过滤。

@# 是由 VectorChord 定义的操作符。有关 @# 的更多信息,请参阅多向量检索。

rabitq8、rabitq4 是由 VectorChord 定义的数据类型。有关 rabitq8、rabitq4 的更多信息,请参阅量化类型。

MaxSim 相关的操作符类自 0.3.0 版本起可用。rabitq8 和 rabitq4 相关的操作符类自 1.1.0 版本起可用。

索引选项(vchordrq)

vchordrq

residual_quantization

  • 说明:该选项决定是否使用残差量化(Residual Quantization)。如果您不熟悉残差量化,可以阅读这篇博客了解更多信息。简而言之,残差量化是一种通过对向量的残差进行量化来提高向量搜索精度的技术。
  • 类型:布尔值(boolean)
  • 默认值:false
  • 示例:
    • residual_quantization = false 表示不使用残差量化。
    • residual_quantization = true 表示使用残差量化。
  • 注意:该选项不支持 rabitq8 和 rabitq4。

degree_of_parallelism

since v1.0.0
  • 说明:该选项是一个指定并行度的提示值。在大多数情况下,您无需更改它。如果您使用的 CPU 拥有超过 32 个线程,并且希望在 PostgreSQL 中利用更多线程,可以将其设置为线程数量以获得更好的性能。
  • 类型:整数(integer)
  • 默认值:32
  • 取值范围:[1, 256]
  • 示例:
    • degree_of_parallelism = 32 提示索引最多可能有 32 个进程并发访问该索引。
    • degree_of_parallelism = 64 提示索引最多可能有 64 个进程并发访问该索引。

build.pin

since v0.2.1
  • 说明:该选项决定构建索引时是否使用共享内存。对于大表,您可以选择启用此选项以加速构建过程。
  • 类型:整数与布尔值的联合类型(union of integer and boolean)
  • 默认值:
    • -1 since v1.0.0
    • false until v0.5.3
  • 取值范围:
    • {-1, 0, 1, 2, false, true} since v1.0.0
    • {false, true} until v0.5.3
  • 示例:
    • build.pin = 2 表示将索引的热数据部分全部缓存在内存中。
    • build.pin = 1 表示将索引热数据部分的一个子集缓存在内存中,消耗较少内存。
    • build.pin = 0 表示启用该功能但实际上不缓存任何内容。此选项仅用于调试目的。
    • build.pin = -1 表示禁用该功能。
    • build.pin = false 是 build.pin = -1 的旧版写法。
    • build.pin = true 是 build.pin = 1 的旧版写法。

默认构建选项

since v0.5.3

这是索引构建的默认设置。索引将不会进行分区。从语义上讲,build.default = {} 类似于 build.internal.lists = []。

内部构建选项(vchordrq)

vchordrq

build.internal.lists

  • 说明:该选项决定向量空间分区的层次结构。
  • 类型:整数列表(list of integers)
  • 默认值:
    • [] since v0.3.0
    • [1000] until v0.2.2: implicit behavior is not ideal
  • 示例:
    • build.internal.lists = [] 表示不对向量空间进行分区。
    • build.internal.lists = [4096] 表示将向量空间划分为 40964096 个单元(cell)。
    • build.internal.lists = [4096, 262144] 表示将向量空间划分为 40964096 个单元,并进一步将这些单元细分为 262144262144 个更小的单元。
  • 注意:索引会基于质心(centroid)将向量空间划分为多个 Voronoi 单元,通过迭代构建出一棵层次化的空间分区树。树中的每个叶子节点代表一个区域,并关联一个存储该区域内向量的列表。在插入时,向量会被放入其对应叶子节点的列表中。在查询时,索引通过排除叶子节点距离查询向量较远的列表来优化搜索,从而有效剪枝搜索空间。如果 lists 的长度为 11,则 lists 选项的值不应小于 4N4\sqrt{N},其中 NN 是表中的向量数量。

build.internal.spherical_centroids

  • 说明:该选项决定是否执行球面 K-means(spherical K-means)——即在每次迭代后对质心进行 L2 归一化,您可以参考此处的 spherical 选项。
  • 类型:布尔值(boolean)
  • 默认值:false
  • 示例:
    • build.internal.spherical_centroids = false 表示不执行球面 K-means。
    • build.internal.spherical_centroids = true 表示执行球面 K-means。
  • 注意:如果您的模型生成的嵌入向量使用余弦相似度作为度量标准,请将此项设置为 true。

build.internal.sampling_factor

since v0.2.0
  • 说明:该选项决定 K-means 算法为每个聚类采样的向量数量。该值越高,构建速度越慢,构建时的内存消耗越大,但搜索性能越好。
  • 类型:整数(integer)
  • 取值范围:[0, 1024]
  • 默认值:256
  • 示例:
    • build.internal.sampling_factor = 256 表示 K-means 算法采样 256C256 C 个向量,其中 CC 是 build.internal.lists 中的最大值。
    • build.internal.sampling_factor = 32 表示 K-means 算法采样 32C32 C 个向量,其中 CC 是 build.internal.lists 中的最大值。这会将 K-means 的时间和内存开销降低至默认值 256 时的约 18\frac{1}{8}。

build.internal.kmeans_iterations

since v0.2.2
  • 说明:该选项决定 K-means 算法的迭代次数。该值越高,构建速度越慢。
  • 类型:整数(integer)
  • 取值范围:[0, 1024]
  • 默认值:10
  • 示例:
    • build.internal.kmeans_iterations = 10 表示 K-means 算法执行 1010 次迭代。
    • build.internal.kmeans_iterations = 100 表示 K-means 算法执行 100100 次迭代。

build.internal.build_threads

  • 说明:该选项决定 K-means 算法使用的线程数。该值越高,构建速度越快,同时构建期间服务器的负载也越高。
  • 类型:整数(integer)
  • 取值范围:[1, 255]
  • 默认值:1
  • 示例:
    • build.internal.build_threads = 1 表示 K-means 算法使用 11 个线程。
    • build.internal.build_threads = 4 表示 K-means 算法使用 44 个线程。

build.internal.kmeans_algorithm

since v1.0.0
  • 说明:该选项决定所使用的 K-means 算法。
  • 类型:对象(object)
  • 示例:
    • build.internal.kmeans_algorithm.lloyd = {}:使用劳埃德算法(Lloyd’s algorithm)。这是默认值。
    • build.internal.kmeans_algorithm.hierarchical = {}:使用层次聚类(hierarchical clustering)。与劳埃德算法相比,这种方法速度要快得多,但可能会导致一定的精度损失。

build.internal.kmeans_dimension

since v1.0.0
  • 说明:该选项决定用于 K-means 输入和输出的维度。该特性通过重采样实现降维与升维,从而有效降低 K-means 的时间和内存消耗,但可能会导致一定的精度损失。
  • 类型:整数与空值的联合类型(union of integer and null)
  • 默认值:null
  • 示例:
    • 如果未设置此选项,则禁用该特性。
    • build.internal.kmeans_dimension = 100 表示 K-means 将处理 100100 维的向量。对于原始维度为 900900 的向量,这会将 K-means 的时间和内存开销降低至未启用该特性时的约 19\frac{1}{9}。

搜索参数(vchordrq)

vchordrq

vchordrq.enable_scan

since v0.5.0
  • 说明:启用或禁用查询规划器对 vchordrq 索引扫描的使用。该参数仅用于测试目的。
  • 类型:布尔值(boolean)
  • 默认值:on
  • 示例:
    • vchordrq.enable_scan = off 禁用查询规划器使用 vchordrq 索引扫描。
    • vchordrq.enable_scan = on 启用查询规划器使用 vchordrq 索引扫描。

vchordrq.probes

  • 说明:GUC 参数 vchordrq.probes 控制如何利用向量空间分区来辅助查询剪枝。探测数(probes)越多,搜索结果越精确,但速度也越慢。
  • 类型:整数列表(list of integers)
  • 默认值:
    • since v0.3.0
    • 10 until v0.2.2: the default value was 10 before `lists` defaulted to empty
    • 该参数支持回退参数(fallback)。
  • 示例:
    • SET vchordrq.probes = 1 表示仅使用 1 个探测分区。
    • SET vchordrq.probes = 10 表示使用 10 个探测分区。
  • 注意:默认值为空列表。该选项的长度必须与 lists 的长度相匹配。
    • 如果 lists = [],则 probes 必须为 。
    • 如果 lists = [11, 22],则 probes 可以是 2,4 或 4,8。其形状不能不匹配,例如不能是 、3、7,8,9 或 5,5,5,5。

vchordrq.epsilon

  • 说明:即使经过剪枝,检索出的候选向量数量依然可观。索引采用 RaBitQ 算法将向量量化为位向量(bit vector),其内存占用仅为单精度浮点向量的 132\frac{1}{32}。由于大部分计算基于整数完成,处理速度得以大幅提升。与传统量化算法不同,RaBitQ 不仅估算距离,还会估算距离的下界(lower bound)。索引会计算每个向量的距离下界,并根据查询需求动态调整需要重新计算精确距离的向量数量,从而在性能与精度之间取得平衡。GUC 参数 vchordrq.epsilon 控制距离下界估算的保守程度。该值越高,精度越高,但性能越差。对于大多数应用场景而言,默认值倾向于提供高于实际所需的精度,因此您可以尝试调低此参数以获得更好的性能。
  • 类型:实数(real)
  • 默认值:
  • 取值范围:[0.0, 4.0]
  • 示例:
    • SET vchordrq.epsilon = 0.1 表示您正在使用非常乐观的下界估算。这样设置是因为在您所需的精度下,您的数据集对下界估算不敏感。
    • SET vchordrq.epsilon = 4.0 表示您正在使用非常悲观的下界估算。这样设置是因为在您所需的精度下,您的数据集对下界估算较为敏感。

vchordrq.prewarm_dim

removed in v0.4.0
  • 说明:GUC 参数 vchordrq.prewarm_dim 用于为指定维度预计算 RaBitQ 投影矩阵。这有助于降低 PostgreSQL 集群启动后首次查询的延迟。
  • 类型:整数列表(list of integers)
  • 默认值:64,128,256,384,512,768,1024,1536
  • 示例:
    • ALTER SYSTEM SET vchordrq.prewarm_dim = '64,128' 表示将为维度 64 和 128 预计算投影矩阵。
  • 注意:
    • 此设置需要重启数据库才能生效。
    • 自 v0.4.0 起,新算法已使该选项不再需要。

vchordrq.max_scan_tuples

since v0.2.0
  • 说明:GUC 参数 vchordrq.max_scan_tuples 控制在一次向量搜索中最多可以扫描的元组(tuple)数量。在大多数情况下,您不需要设置此参数,因为 LIMIT 子句起到了类似的作用。然而,当存在 WHERE 子句时,LIMIT 子句在过滤之后生效,而此 GUC 参数在过滤之前生效。该参数旨在防止过滤选择率极低时出现性能下降。
  • 类型:整数(integer)
  • 默认值:-1
  • 取值范围:[-1, 2147483647]
  • 示例:
    • SET vchordrq.max_scan_tuples = 999 表示索引扫描最多返回 999999 个元组。
    • SET vchordrq.max_scan_tuples = -1 表示对元组数量没有限制。
  • 注意:当设置为 -1 时,此参数不起作用。

2 - 多向量检索

多向量检索(Multi-Vector Retrieval)是检索增强生成(RAG)系统中的一项高级技术,通过为每个文档使用多个向量来增强文档检索能力。
since v0.3.0

多向量检索(Multi-Vector Retrieval)是检索增强生成(RAG)系统中使用的一项高级技术,通过为每个文档关联多个向量来增强文档检索效果。

与单向量检索不同,使用 MaxSim 操作符的多向量检索能够捕捉细粒度的语义关系。

关于如何构建端到端的多向量检索应用,请参阅我们的博客了解更多细节。

快速上手

晚期交互(Late interaction)模型会将文档和查询分别嵌入为向量数组,并通过名为 MaxSim 的操作符计算相似度。MaxSim 操作符的定义为 ∑imax⁡jqi⋅dj\sum_i \max_j q_i \cdot d_j,即对于查询向量数组中的每个向量,在文档向量数组中找到最接近的向量,计算它们的点积,最后将所有结果求和。

在 VectorChord 中,该操作符的符号为 @#。左操作数是文档向量数组,右操作数是查询向量数组。原始的 MaxSim 操作符基于相似度,而 VectorChord 中的操作符则基于距离:VectorChord 的实现通过对相似度得分取反,将其解释为一种距离度量。

要为向量数组构建索引,首先创建一个名为 items 的表,其中包含一个类型为 vector(n)[] 的 embeddings 列,然后向表中填充生成的数据。

CREATE TABLE items (id bigserial PRIMARY KEY, embeddings vector(3)[]);

INSERT INTO items (embeddings) 
SELECT
    ARRAY[
        ARRAY[random(), random(), random()]::vector,
        ARRAY[random(), random(), random()]::vector,
        ARRAY[random(), random(), random()]::vector
    ]
FROM generate_series(1, 1000);

与其他向量操作符一样,使用 MaxSim 操作符的查询也可以通过索引进行加速。

CREATE INDEX ON items USING vchordrq (embeddings vector_maxsim_ops);

随后,您便可以使用该索引执行向量搜索。

SELECT * FROM items
ORDER BY embeddings @# 
    ARRAY[
        '[3,1,2]'::vector,
        '[2,2,2]'::vector
    ] LIMIT 5;

参考

vchordg 不支持此功能。

操作符类

请参阅:

搜索参数(vchordrq)

vchordrq

MaxSim 操作符的索引机制与其他向量操作符类似。当在向量数组列上构建索引时,数组中的每个向量都会被单独插入到索引数据结构中。在查询期间,索引会对查询数组中的每个向量分别执行一次独立的向量搜索。默认情况下,索引不会对结果进行重排序(rerank),而是使用 RaBitQ 的估算距离代替实际距离。随后,索引会将所有独立向量搜索的结果进行合并,以生成最终输出。

MaxSim 索引还使用了以下几个额外参数:

vchordrq.maxsim_refine

since v0.3.0
  • 说明:GUC 参数 vchordrq.maxsim_refine 会让索引对结果进行重排序,用实际距离替换 RaBitQ 的估算距离,直到前 k 个(top-k)最近向量的距离全部重新计算完毕。
  • 类型:整数(integer)
  • 取值范围:[0, 2147483647]
  • 默认值:
  • 示例:
    • SET vchordrq.maxsim_refine = 0 表示索引始终使用 RaBitQ 的估算距离。
    • SET vchordrq.maxsim_refine = 1024 表示索引会对结果进行重排序,用实际距离替换 RaBitQ 的估算距离,直到前 1024 个最近向量的距离全部重新计算完毕。

vchordrq.maxsim_threshold

since v0.3.0
  • 说明:GUC 参数 vchordrq.maxsim_threshold 启用对缺失值更激进的估算策略。启用该设置后,索引会在最底层列表中找到累积大小达到或超过 vchordrq.maxsim_threshold 的第一个聚类(cluster),并使用其与查询向量的距离来估算缺失值。
  • 类型:整数(integer)
  • 取值范围:[0, 2147483647]
  • 默认值:
  • 示例:
    • SET vchordrq.maxsim_threshold = 0 表示不启用更激进的估算策略。
    • SET vchordrq.maxsim_threshold = 1024 表示索引会在最底层列表中找到累积大小达到或超过 1024 的第一个聚类,并使用其与查询向量的距离来估算缺失值。

3 - 图索引

VectorChord 的 vchordg 索引类型是一种基于磁盘的图索引,具有低内存占用的特点。
since v0.5.0

VectorChord 的索引类型 vchordg 是一种基于磁盘的图索引,具有低内存占用的特点。

我们首先创建一个名为 items 的表,其中包含一个类型为 vector(n) 的 embedding 列,然后向其中填充示例数据。

要创建 vchordg 索引,可以使用以下 SQL 语句:

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops);
提示

该功能目前处于预览阶段。

调优

在构建索引时,通常需要对两个选项进行调优:m 和 ef_construction。m 是每个顶点的最大邻居数,ef_construction 是插入期间包含最近邻的动态列表大小。

在搜索时,需要对 ef_search 进行调优。ef_search 是搜索期间包含最近邻的动态列表大小。

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops) WITH (options = $$
m = 64
ef_construction = 128
$$);

SET vchordg.ef_search TO '128';
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10;

作为一种基于磁盘的索引,vchordg 通常只需要将量化后的向量保存在缓冲池(buffer pool)中即可维持良好性能。默认情况下,vchordg 会将一个 DD 维向量量化为 2D2D 比特。设行数为 NN,则该索引所需的总内存为 2DN2DN 比特。如果您的内存非常有限且正在使用超高维向量,可以考虑将向量量化为 DD 比特,此时索引所需的总内存为 DNDN 比特。

CREATE INDEX ON items USING vchordg (embedding vector_l2_ops) WITH (options = $$
bits = 1
m = 64
ef_construction = 128
$$);

索引构建可以通过使用多进程来加速。详情请参阅 PostgreSQL 调优。

参考

操作符类(vchordg)

vchordg

下表列出了 vchordg 支持的所有可用操作符类。

操作符类 说明 操作符 1 操作符 2
vector_l2_ops 适用于 vector 类型与欧氏距离 <->(vector,vector) <<->>(vector,vector)
vector_ip_ops 适用于 vector 类型与负内积 <#>(vector,vector) <<#>>(vector,vector)
vector_cosine_ops 适用于 vector 类型与余弦距离 <=>(vector,vector) <<=>>(vector,vector)
halfvec_l2_ops 适用于 halfvec 类型与欧氏距离 <->(halfvec,halfvec) <<->>(halfvec,halfvec)
halfvec_ip_ops 适用于 halfvec 类型与负内积 <#>(halfvec,halfvec) <<#>>(halfvec,halfvec)
halfvec_cosine_ops 适用于 halfvec 类型与余弦距离 <=>(halfvec,halfvec) <<=>>(halfvec,halfvec)
rabitq8_l2_ops 适用于 rabitq8 类型与欧氏距离 <->(rabitq8,rabitq8) <<->>(rabitq8,rabitq8)
rabitq8_ip_ops 适用于 rabitq8 类型与负内积 <#>(rabitq8,rabitq8) <<#>>(rabitq8,rabitq8)
rabitq8_cosine_ops 适用于 rabitq8 类型与余弦距离 <=>(rabitq8,rabitq8) <<=>>(rabitq8,rabitq8)
rabitq4_l2_ops 适用于 rabitq4 类型与欧氏距离 <->(rabitq4,rabitq4) <<->>(rabitq4,rabitq4)
rabitq4_ip_ops 适用于 rabitq4 类型与负内积 <#>(rabitq4,rabitq4) <<#>>(rabitq4,rabitq4)
rabitq4_cosine_ops 适用于 rabitq4 类型与余弦距离 <=>(rabitq4,rabitq4) <<=>>(rabitq4,rabitq4)

<<->>、<<#>>、<<=>> 是由 VectorChord 定义的操作符。有关 <<->>、<<#>>、<<=>> 的更多信息,请参阅相似度过滤。

rabitq8、rabitq4 是由 VectorChord 定义的数据类型。有关 rabitq8、rabitq4 的更多信息,请参阅量化类型。

前 6 个操作符类自 0.5.0 版本起可用,后 6 个操作符类自 1.1.0 版本起可用。

索引选项(vchordg)

vchordg

bits

since v0.5.0
  • 说明:RaBitQ 量化后比特数与维度的比值。bits = 2 可提供更好的召回率和 QPS,而 bits = 1 则消耗更少的内存。
  • 类型:整数(integer)
  • 默认值:2
  • 示例:
    • bits = 2 表示将 DD 维向量量化为 2D2D 比特。
    • bits = 1 表示将 DD 维向量量化为 DD 比特。

m

since v0.5.0
  • 说明:每个顶点的最大邻居数。m 越大,性能越好,但存储需求也越高。m 对应于 HNSW 中的 m0m_0 和 DiskANN 中的 mm。
  • 类型:整数(integer)
  • 默认值:32
  • 示例:
    • m = 32 表示每个顶点最多有 3232 个邻居。
    • m = 64 表示每个顶点最多有 6464 个邻居。

ef_construction

since v0.5.0
  • 说明:插入期间包含最近邻的动态列表大小。ef_construction 越大,搜索性能越好,但插入速度越慢。ef_construction 对应于 HNSW 中的 efconstruction\text{ef}_\text{construction} 和 DiskANN 中的 efC\text{ef}_C。
  • 类型:整数(integer)
  • 默认值:64
  • 示例:
    • ef_construction = 64 表示在插入期间包含最近邻的动态列表大小为 6464。
    • ef_construction = 128 表示在插入期间包含最近邻的动态列表大小为 128128。

alpha

since v0.5.0
  • 说明:剪枝期间所选用的 alpha 值。alpha 对应于 DiskANN 中的 α\alpha。该选项必须是一个升序列表,其中第一个元素为 1.0,最后一个元素小于 2.0。
  • 类型:浮点数列表(list of floats)
  • 默认值:[1.0, 1.2]
  • 示例:
    • alpha = [1.0, 1.2] 等价于在 DiskANN 中设置 alpha = 1.2。
    • alpha = [1.0] 等价于 HNSW 中的默认剪枝策略。
  • 注意:当距离度量为负内积时,此选项无效。

beam_construction

since v0.5.0
  • 说明:插入期间使用的束宽(beam width)。束宽是指一次同时访问的顶点数量。由于从 SSD 随机读取少量扇区的时间与读取单个扇区几乎相同,较大的束宽可以有效减少对 SSD 的往返访问次数,从而获得更好的性能。但由于它会增加计算量,因此对于完全放入内存的索引而言反而不利。
  • 类型:整数(integer)
  • 默认值:1
  • 示例:
    • beam_construction = 8 表示索引在插入期间一次同时访问 8 个顶点。
    • beam_construction = 1 表示索引在插入期间一次访问 1 个顶点。

搜索参数(vchordg)

vchordg

vchordg.enable_scan

since v0.5.0
  • 说明:启用或禁用查询规划器对 vchordg 索引扫描的使用。该参数仅用于测试目的。
  • 类型:布尔值(boolean)
  • 默认值:on
  • 示例:
    • vchordg.enable_scan = off 禁用查询规划器使用 vchordg 索引扫描。
    • vchordg.enable_scan = on 启用查询规划器使用 vchordg 索引扫描。

since v0.5.0
  • 说明:搜索期间包含最近邻的动态列表大小。vchordg.ef_search 越大,召回率越好,但 QPS 越低。vchordg.ef_search 对应于 HNSW 和 DiskANN 中的 ef\text{ef}。
  • 类型:整数(integer)
  • 默认值:
  • 取值范围:[1, 65535]
  • 示例:
    • SET vchordg.ef_search = 64 表示在搜索期间包含最近邻的动态列表大小为 6464。
    • SET vchordg.ef_search = 128 表示在搜索期间包含最近邻的动态列表大小为 128128。

since v0.5.0
  • 说明:搜索期间使用的束宽(beam width)。束宽是指一次同时访问的顶点数量。由于从 SSD 随机读取少量扇区的时间与读取单个扇区几乎相同,较大的束宽可以有效减少对 SSD 的往返访问次数,从而获得更好的性能。但由于它会增加计算量,因此对于完全放入内存的索引而言反而不利。
  • 类型:整数(integer)
  • 默认值:1
  • 取值范围:[1, 65535]
  • 示例:
    • SET vchordg.beam_search = 8 表示索引在搜索期间一次同时访问 8 个顶点。
    • SET vchordg.beam_search = 1 表示索引在搜索期间一次访问 1 个顶点。

vchordg.max_scan_tuples

since v0.5.0
  • 说明:GUC 参数 vchordg.max_scan_tuples 控制在一次向量搜索中最多可以扫描的元组数量。在大多数情况下,您不需要设置此参数,因为 LIMIT 子句起到了类似的作用。然而,当存在 WHERE 子句时,LIMIT 子句在过滤之后生效,而此 GUC 参数在过滤之前生效。该参数旨在防止过滤选择率极低时出现性能下降。
  • 类型:整数(integer)
  • 默认值:-1
  • 取值范围:[-1, 2147483647]
  • 示例:
    • SET vchordg.max_scan_tuples = 999 表示索引扫描最多返回 999999 个元组。
    • SET vchordg.max_scan_tuples = -1 表示对元组数量没有限制。
  • 注意:当设置为 -1 时,此参数不起作用。

4 - 量化类型

量化不仅可以应用于索引,还可以直接应用于向量本身。为此,VectorChord 提供了用于量化向量的数据类型。
since v1.1.0

量化不仅可以应用于索引,还可以直接应用于向量本身。为此,VectorChord 提供了专门用于量化向量的数据类型。

rabitq8 是量化向量的数据类型。在相同维度下,它的存储开销约为全精度向量(vector)的 14\frac{1}{4},半精度向量(halfvec)的 12\frac{1}{2}。该类型以其所使用的量化算法命名。1

CREATE TABLE items (id bigserial PRIMARY KEY, embedding rabitq8(3));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[0,0,0]'::vector));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[1,1,1]'::vector));
INSERT INTO items (embedding) VALUES (quantize_to_rabitq8('[2,2,2]'::vector));
--- ...
SELECT id FROM items ORDER BY embedding <-> quantize_to_rabitq8('[0.9,0.9,1.1]'::vector) LIMIT 100;

与普通向量类型一样,也可以在 rabitq8 类型的列上创建索引。

CREATE INDEX ON items USING vchordrq (embedding rabitq8_l2_ops);

通过使用 rabitq8,表大小和索引大小都将缩小为使用 vector 类型时的 14\frac{1}{4}。其代价是量化会明显折损性能与精度。因此,它适用于检索大量候选结果的场景。

此外,VectorChord 还提供了 rabitq4 类型。rabitq8 量化向量在每个维度上使用 88 比特,而 rabitq4 量化向量在每个维度上仅使用 44 比特。它专为在受限环境下优先追求低存储开销而非高精度的场景而设计。

参考

操作符类

请参阅:

数据类型

rabitq8

since v1.1.0
  • 说明:使用 uint8 进行内部存储的量化向量数据类型。基于扩展版 RaBitQ 算法。1
  • 文本表示:与 vector/halfvec 不同,rabitq8 的文本表示更为复杂。假设维度为 dd,它由括在圆括号中的 44 个浮点数数组与括在方括号中的 dd 个 uint8 整数数组拼接而成。例如 (2.999999, 160.63234, 647, 2.8498733)[255, 205, 187]。其内部解释无需手动处理,但您可以通过 dequantize_to_vector(rabitq8) / dequantize_to_halfvec(rabitq8) 获取它所代表的近似向量。

rabitq4

since v1.1.0
  • 说明:使用 uint4 进行内部存储的量化向量数据类型。基于扩展版 RaBitQ 算法。1
  • 文本表示:与 vector/halfvec 不同,rabitq4 的文本表示更为复杂。假设维度为 dd,它由括在圆括号中的 44 个浮点数数组与括在方括号中的 dd 个 uint4 整数数组拼接而成。例如 (2.999999, 9.420722, 38, 2.8498733)[15, 12, 11]。其内部解释无需手动处理,但您可以通过 dequantize_to_vector(rabitq4) / dequantize_to_halfvec(rabitq4) 获取它所代表的近似向量。

函数

quantize_to_rabitq8(vector)

since v1.1.0
  • 说明:将向量量化为 rabitq8。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。
  • 返回值:rabitq8,量化后的向量
  • 参数:
    • vector,待量化的向量
  • 示例:
    • SELECT quantize_to_rabitq8('[1,1,1]'::vector)
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (vector AS rabitq8) WITH FUNCTION quantize_to_rabitq8(vector) AS ASSIGNMENT 手动创建。

quantize_to_rabitq8(halfvec)

since v1.1.0
  • 说明:将半精度向量量化为 rabitq8。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。
  • 返回值:rabitq8,量化后的向量
  • 参数:
    • halfvec,待量化的向量
  • 示例:
    • SELECT quantize_to_rabitq8('[1,1,1]'::halfvec)
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (halfvec AS rabitq8) WITH FUNCTION quantize_to_rabitq8(halfvec) AS ASSIGNMENT 手动创建。

dequantize_to_vector(rabitq8)

since v1.1.1
  • 说明:将量化向量反量化为 vector。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。使用此函数无法精确重建原始向量。
  • 返回值:vector,反量化后的向量
  • 参数:
    • rabitq8,待反量化的量化向量
  • 示例:
    • SELECT dequantize_to_vector(quantize_to_rabitq8('[1,1,1]'::vector))
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (rabitq8 AS vector) WITH FUNCTION dequantize_to_vector(rabitq8) AS ASSIGNMENT 手动创建。

dequantize_to_halfvec(rabitq8)

since v1.1.1
  • 说明:将量化向量反量化为 halfvec。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。使用此函数无法精确重建原始向量。
  • 返回值:halfvec,反量化后的向量
  • 参数:
    • rabitq8,待反量化的量化向量
  • 示例:
    • SELECT dequantize_to_halfvec(quantize_to_rabitq8('[1,1,1]'::halfvec))
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (rabitq8 AS halfvec) WITH FUNCTION dequantize_to_halfvec(rabitq8) AS ASSIGNMENT 手动创建。

quantize_to_rabitq4(vector)

since v1.1.0
  • 说明:将向量量化为 rabitq4。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。
  • 返回值:rabitq4,量化后的向量
  • 参数:
    • vector,待量化的向量
  • 示例:
    • SELECT quantize_to_rabitq4('[1,1,1]'::vector)
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (vector AS rabitq4) WITH FUNCTION quantize_to_rabitq4(vector) AS ASSIGNMENT 手动创建。

quantize_to_rabitq4(halfvec)

since v1.1.0
  • 说明:将半精度向量量化为 rabitq4。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。
  • 返回值:rabitq4,量化后的向量
  • 参数:
    • halfvec,待量化的向量
  • 示例:
    • SELECT quantize_to_rabitq4('[1,1,1]'::halfvec)
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (halfvec AS rabitq4) WITH FUNCTION quantize_to_rabitq4(halfvec) AS ASSIGNMENT 手动创建。

dequantize_to_vector(rabitq4)

since v1.1.1
  • 说明:将量化向量反量化为 vector。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。使用此函数无法精确重建原始向量。
  • 返回值:vector,反量化后的向量
  • 参数:
    • rabitq4,待反量化的量化向量
  • 示例:
    • SELECT dequantize_to_vector(quantize_to_rabitq4('[1,1,1]'::vector))
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (rabitq4 AS vector) WITH FUNCTION dequantize_to_vector(rabitq4) AS ASSIGNMENT 手动创建。

dequantize_to_halfvec(rabitq4)

since v1.1.1
  • 说明:将量化向量反量化为 halfvec。请注意,在不同的 VectorChord 版本或不同的机器上,该函数对相同输入可能会产生略有差异的结果。使用此函数无法精确重建原始向量。
  • 返回值:halfvec,反量化后的向量
  • 参数:
    • rabitq4,待反量化的量化向量
  • 示例:
    • SELECT dequantize_to_halfvec(quantize_to_rabitq4('[1,1,1]'::halfvec))
  • 注意:该函数没有对应的默认类型转换(CAST)。如有需要,您可以使用 CREATE CAST (rabitq4 AS halfvec) WITH FUNCTION dequantize_to_halfvec(rabitq4) AS ASSIGNMENT 手动创建。

操作符

<->(rabitq8, rabitq8)

since v1.1.0
  • 说明:计算两个量化向量之间的 L2 距离。
  • 返回值:real,两个量化向量的 L2 距离
  • 参数:
    • rabitq8
    • rabitq8

<#>(rabitq8, rabitq8)

since v1.1.0
  • 说明:计算两个量化向量之间的负点积距离。
  • 返回值:real,两个量化向量的负点积距离
  • 参数:
    • rabitq8
    • rabitq8

<=>(rabitq8, rabitq8)

since v1.1.0
  • 说明:计算两个量化向量之间的余弦距离。
  • 返回值:real,两个量化向量的余弦距离
  • 参数:
    • rabitq8
    • rabitq8

<->(rabitq4, rabitq4)

since v1.1.0
  • 说明:计算两个量化向量之间的 L2 距离。
  • 返回值:real,两个量化向量的 L2 距离
  • 参数:
    • rabitq4
    • rabitq4

<#>(rabitq4, rabitq4)

since v1.1.0
  • 说明:计算两个量化向量之间的负点积距离。
  • 返回值:real,两个量化向量的负点积距离
  • 参数:
    • rabitq4
    • rabitq4

<=>(rabitq4, rabitq4)

since v1.1.0
  • 说明:计算两个量化向量之间的余弦距离。
  • 返回值:real,两个量化向量的余弦距离
  • 参数:
    • rabitq4
    • rabitq4

  1. Jianyang Gao, Yutong Gou, Yuexuan Xu, Yongyi Yang, Cheng Long, Raymond Chi-Wing Wong, “Practical and Asymptotically Optimal Quantization of High-Dimensional Vectors in Euclidean Space for Approximate Nearest Neighbor Search”, SIGMOD 2025, available at https://arxiv.org/abs/2409.09913. ↩︎ ↩︎ ↩︎

5 - 相似度过滤

通常我们使用 ORDER BY 和 LIMIT 子句执行向量搜索。在某些场景下,您可能希望使用 WHERE 子句过滤掉距离查询向量较远的向量。

通常,我们会使用 ORDER BY 和 LIMIT 子句来执行向量搜索。但在某些情况下,您可能希望使用 WHERE 子句过滤掉距离查询向量太远的向量。很自然地,您可能会写出如下 SQL:

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <-> '[0, 0, 0]' < 0.1 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: ((embedding <-> '[0,0,0]'::vector) < '0.1'::double precision)

返回的结果虽然符合预期,但这种写法的性能却很差。这是因为如果落在距离阈值内的向量少于 1010 个,PostgreSQL 会强制索引继续搜索以凑够 1010 条结果,从而遍历耗尽整个搜索空间。

为了避免这种情况,您可以使用特定语法将过滤条件直接下推(push down)到向量索引中,使索引一旦发现搜索区域超出指定距离便立即停止搜索。我们将这种可以下推到索引中的基于距离的过滤条件称为相似度过滤(similarity filter)。

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <<->> sphere('[0, 0, 0]'::vector, 0.1) ORDER BY embedding <-> '[0, 0, 0]' LIMIT 10;
                               QUERY PLAN                               
------------------------------------------------------------------------
 Limit
   ->  Index Scan using items_embedding_idx on items
         Index Cond: (embedding <<->> '("[0,0,0]",0.1)'::sphere_vector)
         Order By: (embedding <-> '[0,0,0]'::vector)

当且仅当两个向量之间的 L2 距离小于 0.1 时,embedding <<->> sphere('[0, 0, 0]'::vector, 0.1) 的求值结果为 true。对于负内积请使用 <<#>> 操作符,对于余弦距离请使用 <<=>> 操作符。

此外,如果您只指定 WHERE 子句而不使用 ORDER BY,索引依然能够被有效利用。

EXPLAIN (COSTS FALSE) 
SELECT * FROM items WHERE embedding <<->> sphere('[0, 0, 0]'::vector, 0.1);
                            QUERY PLAN                            
------------------------------------------------------------------
 Index Scan using items_embedding_idx on items
   Index Cond: (embedding <<->> '("[0,0,0]",0.1)'::sphere_vector)

参考

操作符类

请参阅:

数据类型

sphere_vector

  • 说明:球体(Sphere)。
  • 定义:(center vector, radius REAL)。

sphere_halfvec

  • 说明:球体(Sphere)。
  • 定义:(center halfvec, radius REAL)。

sphere_rabitq8

since v1.1.0
  • 说明:球体(Sphere)。
  • 定义:(center rabitq8, radius REAL)。

sphere_rabitq4

since v1.1.0
  • 说明:球体(Sphere)。
  • 定义:(center rabitq4, radius REAL)。

函数

sphere(vector, real)

  • 说明:通过球心和半径创建一个球体。
  • 定义:SELECT ROW($1, $2)。
  • 返回值:sphere_vector
  • 参数:
    • vector,球体的球心
    • real,球体的半径
  • 示例:
    • SELECT sphere('[0,0,0]'::vector, 1.0)

sphere(halfvec, real)

  • 说明:通过球心和半径创建一个球体。
  • 定义:SELECT ROW($1, $2)。
  • 返回值:sphere_halfvec
  • 参数:
    • halfvec,球体的球心
    • real,球体的半径
  • 示例:
    • SELECT sphere('[0,0,0]'::halfvec, 1.0)

sphere(rabitq8, real)

since v1.1.0
  • 说明:通过球心和半径创建一个球体。
  • 定义:SELECT ROW($1, $2)。
  • 返回值:sphere_rabitq8
  • 参数:
    • rabitq8,球体的球心
    • real,球体的半径
  • 示例:
    • SELECT sphere(quantize_to_rabitq8('[0,0,0]'::vector), 1.0)

sphere(rabitq4, real)

since v1.1.0
  • 说明:通过球心和半径创建一个球体。
  • 定义:SELECT ROW($1, $2)。
  • 返回值:sphere_rabitq4
  • 参数:
    • rabitq4,球体的球心
    • real,球体的半径
  • 示例:
    • SELECT sphere(quantize_to_rabitq4('[0,0,0]'::vector), 1.0)

操作符

<<->>(vector, sphere_vector)

  • 说明:使用 L2 度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • vector,待检测的向量
    • sphere_vector,球体

<<#>>(vector, sphere_vector)

  • 说明:使用负点积度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • vector,待检测的向量
    • sphere_vector,球体

<<=>>(vector, sphere_vector)

  • 说明:使用余弦度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • vector,待检测的向量
    • sphere_vector,球体

<<->>(halfvec, sphere_halfvec)

  • 说明:使用 L2 度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • halfvec,待检测的向量
    • sphere_halfvec,球体

<<#>>(halfvec, sphere_halfvec)

  • 说明:使用负点积度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • halfvec,待检测的向量
    • sphere_halfvec,球体

<<=>>(halfvec, sphere_halfvec)

  • 说明:使用余弦度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • halfvec,待检测的向量
    • sphere_halfvec,球体

<<->>(rabitq8, sphere_rabitq8)

since v1.1.0
  • 说明:使用 L2 度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq8,待检测的向量
    • sphere_rabitq8,球体

<<#>>(rabitq8, sphere_rabitq8)

since v1.1.0
  • 说明:使用负点积度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq8,待检测的向量
    • sphere_rabitq8,球体

<<=>>(rabitq8, sphere_rabitq8)

since v1.1.0
  • 说明:使用余弦度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq8,待检测的向量
    • sphere_rabitq8,球体

<<->>(rabitq4, sphere_rabitq4)

since v1.1.0
  • 说明:使用 L2 度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq4,待检测的向量
    • sphere_rabitq4,球体

<<#>>(rabitq4, sphere_rabitq4)

since v1.1.0
  • 说明:使用负点积度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq4,待检测的向量
    • sphere_rabitq4,球体

<<=>>(rabitq4, sphere_rabitq4)

since v1.1.0
  • 说明:使用余弦度量检测向量是否位于球体内部。
  • 返回值:布尔值(boolean)
  • 参数:
    • rabitq4,待检测的向量
    • sphere_rabitq4,球体

6 - PostgreSQL 调优

索引构建可以并行化执行。由于并行化会增加服务器负载,PostgreSQL 默认未启用它。请参考本文配置并行构建、I/O 与共享缓冲区。

索引构建可以并行化执行。由于并行化会增加服务器负载,PostgreSQL 默认并未启用。如需启用并行构建,请参考以下 SQL 配置:

-- 设置数据库集群可支持的最大后台进程数。
-- 建议将其设置为至少等于您期望的并行度加上 8。
-- 例如,如果您希望使用 16 个线程进行并行处理,请将该值至少设置为 24。
-- 根据您的实际工作负载,将该值设置得更高可能会更有帮助。
-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET max_worker_processes = 24;

-- 设置数据库集群可用于并行操作的最大工作进程数。
-- 建议将其设置为您期望的并行度减 1。
-- 例如,如果您希望使用 16 个线程进行并行处理,请将该值设置为 15。
SET max_parallel_workers = 15;

-- 设置单个维护命令(如 CREATE INDEX)可启动的最大并行工作进程数。
-- 建议将其设置为您期望的并行度减 1。
-- 例如,如果您希望使用 16 个线程进行并行处理,请将该值设置为 15。
SET max_parallel_maintenance_workers = 15;

并行工作进程的数量还取决于表本身的配置。默认情况下,该值由 PostgreSQL 自动计算决定。如果 PostgreSQL 判定不启用并行,那么并行索引构建就不会生效。您可以通过调整表的 parallel_workers 存储参数来覆盖 PostgreSQL 的默认行为,强制启用并行索引构建。

ALTER TABLE items set (parallel_workers = 15);

您还需要调整以下几个设置以提升 I/O 性能:

-- 设置 PostgreSQL 预期可同时执行的并发磁盘 I/O 操作数。
-- 使用 SSD 存储时,建议将其设置为 20。
SET effective_io_concurrency = 20;
SET maintenance_io_concurrency = 20;

我们观察到在某些情况下,工作异常的 JIT(即时编译)可能会对性能产生负面影响。如果您并不依赖它,可以选择将其禁用。

SET jit = off;

最后,您需要设置 PostgreSQL 共享缓冲区(shared buffers)的大小。

-- 设置数据库服务器用于共享内存缓冲区的内存量。
-- 根据您的工作负载,通常建议将该值设置为总内存的 25%~40%。
-- 对于向量数据库工作负载,将该值设置为总内存的 80% 往往更有优势。
-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET shared_buffers = '16GB';

异步 I/O

PostgreSQL 17 首次引入了流式 I/O(streaming I/O)API。在此基础上,PostgreSQL 18 引入了异步 I/O(Asynchronous I/O)。借助异步 I/O,PostgreSQL 可以同时发起多个读取操作,并在等待结果返回的同时继续处理其他任务。这能够显著提升需要从磁盘读取大量数据的查询性能。PostgreSQL 提供了多种异步 I/O 方式,您可以根据需要进行调整。

本节内容仅适用于 PostgreSQL 18。

io_uring

io_uring 方式使用 Linux 内核的 io_uring 接口,能够减少上下文切换的开销。该接口在 Linux 内核 5.1 及更高版本中可用。您可以通过 GUC 参数 io_method 启用此方式。

-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET io_method = 'io_uring';

由于 io_uring 会使用额外的文件描述符和锁定内存(locked memory),PostgreSQL 可能会触及系统的 RLIMIT_NOFILE 和 RLIMIT_MEMLOCK 限制。您可能需要在操作系统层面调高这些限制。另请参阅:

出于安全考虑,容器运行时、云服务商或系统管理员可能会禁用 io_uring。如果遇到此类情况,请向您的服务提供商咨询详情。另请参阅:

通常推荐使用 io_uring,但实际性能表现仍取决于您的具体工作负载。另请参阅:

本小节仅适用于 Linux 系统。

worker

这是 PostgreSQL 18 中的默认设置。PostgreSQL 通过将 I/O 请求发送到专门的进程池来避免阻塞后端进程。您可以通过 GUC 参数 io_method 显式启用此方式。

-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET io_method = 'worker';

-- 选择要使用的 I/O 工作进程数量。
-- 建议将其设置为您的并行度与参数上限值之间的较小者。
-- 例如,如果您的并行度为 16,请将该值设置为 16。
-- 该参数的最大值为 32,因此不能设置超过 32 的值。
-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET io_workers = 16;

sync

sync 方式的工作机制与 PostgreSQL 17 相同:读取操作是同步且阻塞的,并使用 posix_fadvise 向操作系统提供即将发生的读取提示。通常只有在调试时才会使用此方式。您可以通过 GUC 参数 io_method 启用此方式。

-- 注意:此设置需要重启数据库才能生效。
ALTER SYSTEM SET io_method = 'sync';

7 - 监控

您可以通过查询 pg_stat_progress_create_index 视图来监控 PostgreSQL 索引的构建进度。

您可以通过查询 pg_stat_progress_create_index 视图来监控 PostgreSQL 索引的构建进度。

SELECT command, phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%" FROM pg_stat_progress_create_index;

参考

构建阶段(vchordrq)

vchordrq

vchordrq 的索引构建过程包含 5 个步骤,每个步骤的阶段名称(phase)如下:

步骤 阶段消息 说明 耗时
1 initializing 开始构建索引 短
2 initializing index, by default build 该步骤仅用于默认构建(default build) 短
2 initializing index, by internal build 该步骤仅用于内部构建:对表进行采样并执行分区以获取质心 长
2 initializing index, by external build 该步骤仅用于外部构建:从指定表中加载质心 短
3 initializing index 初始化索引的数据结构与存储空间 短
4 inserting tuples from table to index 将所有向量分配并插入到各自对应的列表中 长
5 compacting tuples in index 紧缩优化向量索引结构以提升查询性能 中等

第 4 步 inserting tuples from table to index 占据了索引构建过程中的大部分时间。tuples_done、blocks_done 和 blocks_total 列可用于指示该步骤的当前进度。

构建阶段(vchordg)

vchordg

vchordg 的索引构建过程包含 2 个步骤,每个步骤的阶段名称(phase)如下:

步骤 阶段消息 说明 耗时
1 initializing 开始构建索引 短
2 inserting tuples from table to index 将所有向量插入到图结构中 长

第 2 步 inserting tuples from table to index 占据了索引构建过程中的大部分时间。tuples_done、blocks_done 和 blocks_total 列可用于指示该步骤的当前进度。

8 - 回退参数

搜索参数通常通过 GUC 进行配置。VectorChord 还提供了另一种方式,允许将这些参数直接绑定到索引上,作为索引存储参数的一部分保存。
since v1.1.0

搜索参数通常通过 GUC 进行配置。VectorChord 还提供了另一种方式,允许将这些参数直接绑定到索引上,而不是绑定到会话(session)或事务(transaction)上。换句话说,您可以将它们作为索引存储参数(storage parameters)的一部分保存下来。

在构建索引时,您可以使用以下语法设置 probes:

CREATE INDEX items_embedding_idx ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.internal.lists = [1000]
$$, probes = '10');

现在,您无需设置 GUC 即可直接执行搜索:

--- probes = '10'
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;

如果索引已经构建完成,您可以像这样修改 probes:

ALTER INDEX items_embedding_idx SET (probes = '100');

您仍然可以通过 GUC 设置搜索参数。当设置了 GUC 时,GUC 的优先级高于索引存储参数。这使您能够在不锁定索引的情况下,在当前会话或事务中临时覆盖这些设置。

SET vchordrq.probes TO '100';
--- probes = '100'
SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;

这种语法在以下场景中非常有用:

  • 当存在多个索引且需要不同参数时。
  • 当您不希望设置 GUC 时。
  • 当您希望离线配置搜索参数时。

只要设置了 GUC(哪怕是在系统级别或数据库级别设置的),它都会覆盖索引上的存储参数设置。因此,为了避免难以排查的问题,我们建议仅使用一种方式来设置搜索参数:要么统一通过 GUC 设置,要么统一通过索引存储参数设置。

并非所有参数都支持此功能。有关支持此功能的全部参数,请参阅参考。

参考

索引存储参数(vchordrq)

vchordrq

probes

since v1.1.0
  • 说明:vchordrq.probes 的回退值。
  • 类型:整数列表(list of integers)
  • 默认值:

epsilon

since v1.1.0
  • 说明:vchordrq.epsilon 的回退值。
  • 类型:实数(real)
  • 默认值:1.9
  • 取值范围:[0.0, 4.0]

maxsim_refine

since v1.1.0
  • 说明:vchordrq.maxsim_refine 的回退值。
  • 类型:整数(integer)
  • 默认值:0
  • 取值范围:[0, 2147483647]

maxsim_threshold

since v1.1.0
  • 说明:vchordrq.maxsim_threshold 的回退值。
  • 类型:整数(integer)
  • 默认值:0
  • 取值范围:[0, 2147483647]

索引存储参数(vchordg)

vchordg

since v1.1.0
  • 说明:vchordg.ef_search 的回退值。
  • 类型:整数(integer)
  • 默认值:64
  • 取值范围:[1, 65535]

9 - 评估召回率

在向量搜索中,召回率(recall)是指索引返回的近似最近邻中真实最近邻所占的比例。VectorChord 提供了内置的召回率评估与查询采样工具。
since v0.5.0

在向量搜索中,召回率(recall)是指索引返回的近似最近邻中真实最近邻所占的比例。例如,如果索引检索出 100100 个近似最近邻,其中有 9797 个属于真实最近邻,那么召回率就是 97100=0.97\frac{97}{100} = 0.97。

提示

该功能目前处于预览阶段。

召回率评估

since v0.5.0

召回率衡量的是搜索返回的相关结果占全部应返回真实结果的比例。在使用索引时,您通常希望了解不同参数配置所对应的召回率和 QPS。vchordrq_evaluate_query_recall 函数正是为评估召回率而设计的:它接收一条返回行标识符的查询语句,并返回对应的召回率。

vchordrq_evaluate_query_recall 函数内部依赖 ctid 将查询结果与基准真相(ground truth)进行比对。因此,传入的 query 参数必须以 SELECT ctid 开头。

SET vchordrq.probes = '100';
SET vchordrq.epsilon = 1.0;
SELECT vchordrq_evaluate_query_recall(query => $$
  SELECT ctid FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10
$$);

默认情况下,该函数通过执行另一次高精度索引扫描来生成估算的基准真相,并据此评估召回率。如果您希望通过全表扫描(table scan)生成精确的基准真相来评估召回率,请将函数参数 exact_search 设置为 true。

SET vchordrq.probes = '100';
SET vchordrq.epsilon = 1.0;
SELECT vchordrq_evaluate_query_recall(query => $$
  SELECT ctid FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10
$$, exact_search => true);

查询采样

since v0.5.2

为了方便评估生产或测试中的查询召回率,VectorChord 内置了查询采样方法。

SET vchordrq.query_sampling_enable = on;
SET vchordrq.query_sampling_max_records = 1000;
SET vchordrq.query_sampling_rate = 1;

假设您已经执行了一条示例向量搜索:SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 10。

在查询执行并被采样后,您可以检查捕获到的查询要素,包括模式名(schema)、索引名、表名、列名、操作符以及目标向量。

SELECT * from vchordrq_sampled_queries('items_embedding_idx');
-- schema_name | index_name          | table_name | column_name | operator |    value
---------------+---------------------+------------+-------------+----------+--------------
-- public      | items_embedding_idx | items      | embedding   | <->      | [0.5,0.25,1]

要测量已记录查询的平均召回率,您可以构造目标查询语句并调用评估函数:

SELECT AVG(recall_value) FROM (
    SELECT vchordrq_evaluate_query_recall(
            format(
                'SELECT ctid FROM %I.%I ORDER BY %I OPERATOR(%s) %L LIMIT 10',
                lq.schema_name,
                lq.table_name,
                lq.column_name,
                lq.operator,
                lq.value
            )
    ) AS recall_value
    FROM vchordrq_sampled_queries('items_embedding_idx') AS lq
) AS eval_results;

如果您正在使用包含主库(primary)和备库(standby)的流复制(replication)功能,请注意:采样到的查询不会通过复制或备份进行同步。相反,每台服务器都会根据自己实际处理的查询独立维护各自的采样查询集合。

参考

vchordg 和多向量检索不支持此功能。

搜索参数(vchordrq)

vchordrq

vchordrq.query_sampling_enable

since v0.5.2
  • 说明:启用或禁用 vchordrq 索引扫描的查询采样。
  • 类型:布尔值(boolean)
  • 默认值:off

vchordrq.query_sampling_max_records

since v0.5.2
  • 说明:控制每个索引最多记录的查询数量。该设置仅在 vchordrq.query_sampling_enable 开启时生效。
  • 类型:整数(integer)
  • 默认值:0
  • 取值范围:[0, 10000]
  • 示例:
    • vchordrq.query_sampling_max_records = 0 将为每个索引保留 0 条查询。
    • vchordrq.query_sampling_max_records = 1000 将为每个索引最多保留最近 1000 条查询。

vchordrq.query_sampling_rate

since v0.5.2
  • 说明:控制记录查询的采样率。该设置仅在 vchordrq.query_sampling_enable 开启时生效。
  • 类型:实数(real)
  • 默认值:0
  • 取值范围:[0, 1]
  • 示例:
    • vchordrq.query_sampling_rate = 0 不记录任何查询。
    • vchordrq.query_sampling_rate = 0.01 记录约 1% 的查询。
    • vchordrq.query_sampling_rate = 1 记录每一条查询(仅用于测试目的)。

函数(vchordrq)

vchordrq

vchordrq_sampled_queries

since v0.5.2
  • 说明:检索指定 vchordrq 索引已记录的查询集合。
  • 返回值:table
    • schema_name: text,查询中表的模式名
    • index_name: text,查询中使用的索引名称
    • table_name: text,查询中的表名
    • column_name: text,参与索引扫描的列名
    • operator: text,查询中使用的距离操作符
    • value: text,查询中目标向量的文本表示
  • 参数:
    • regclass,vchordrq 索引的对象标识符
  • 示例:
    • SELECT vchordrq_sampled_queries('items_embedding_idx');

vchordrq_sampled_values

since v0.5.2
  • 说明:检索指定 vchordrq 索引已记录查询中的目标向量。
  • 返回值:text,查询中目标向量的文本表示。
  • 参数:
    • regclass,vchordrq 索引的对象标识符
  • 示例:
    • SELECT vchordrq_sampled_values('items_embedding_idx');
  • 注意:该函数仅用于调试目的。

vchordrq_evaluate_query_recall

since v0.5.0
  • 说明:评估给定 vchordrq 查询的召回率。
  • 返回值:real
  • 参数:
    • query: text,待评估召回率的查询语句
    • exact_search: boolean,是否使用精确基准真相代替估算的基准真相
    • accu_probes: text,用于生成估算基准真相的 vchordrq.probes
    • accu_epsilon: real,用于生成估算基准真相的 vchordrq.epsilon
  • 示例:
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$);
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$, exact_search => true);
    • SELECT vchordrq_evaluate_query_recall(query => $$ SELECT ctid FROM t ORDER BY val <-> '[0.5, 0.25, 1.0]' LIMIT 10 $$, accu_probes => '9999', accu_epsilon => 4.0);

视图(vchordrq)

vchordrq

vchordrq_sampled_queries

since v0.5.2
  • 说明:检索所有 vchordrq 索引已记录的查询集合。
  • 示例:
    • SELECT * from vchordrq_sampled_queries;
  • 注意:该视图仅用于调试目的。

10 - 预热

PostgreSQL 启动后,表和索引只有在首次被访问时才会从存储加载到缓冲池中。使用专用预热函数可以将索引的热数据部分提前加载到内存中。

PostgreSQL 启动后,表和索引只有在首次被访问时才会从存储加载到缓冲池(buffer pool)中。随着查询的不断执行,索引中被频繁访问的部分会自然地缓存在缓冲池里。然而,在执行最初的几批查询时,由于缓冲池为空,查询速度往往较慢。

PostgreSQL 扩展 pg_prewarm 提供了 pg_prewarm 函数,可以在一定程度上解决这一问题。在 PostgreSQL 启动后,您只需向该函数传入索引名称,它就会将索引从存储加载到缓冲池中。

SELECT pg_prewarm('items_pkey');

不过,这种方法只有在缓冲池远大于索引大小时才有效。对于向量索引而言,索引大小往往远超内存容量。向量索引在设计上将索引数据分为两部分:体量较小的热数据部分(hot portion)和体量庞大的冷数据部分(cold portion)。向量索引只需要将热数据部分保留在内存中即可获得理想的性能。因此,VectorChord 提供了专用的预热函数,仅将索引的热数据部分加载到内存中。

SELECT vchordrq_prewarm('items_embedding_idx');

即使索引大小远大于内存容量,该函数依然能够很好地发挥作用。

参考

函数(vchordrq)

vchordrq

vchordrq_prewarm

  • 说明:该函数通过将 vchordrq 索引的热数据加载到缓冲池中来预热索引。
  • 返回值:text
  • 参数:
    • regclass,vchordrq 索引的对象标识符
  • 示例:
    • SELECT vchordrq_prewarm('items_embedding_idx')

函数(vchordg)

vchordg

vchordg_prewarm

since v0.5.0
  • 说明:该函数通过将 vchordg 索引的热数据加载到缓冲池中来预热索引。
  • 返回值:text
  • 参数:
    • regclass,vchordg 索引的对象标识符
  • 示例:
    • SELECT vchordg_prewarm('items_embedding_idx')

11 - 预过滤

在带过滤条件的索引扫描中,预过滤(Prefilter)允许向量索引在计算向量距离之前先评估过滤条件,从而剪枝搜索空间并提升查询效率。
since v0.4.0

在带过滤条件的索引扫描中,通常索引会先执行扫描,然后由 PostgreSQL 检查返回行是否满足过滤条件。在本页中,“过滤条件(filter)”是指 PostgreSQL 查询计划中索引扫描节点上的 Filter,它通常由 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)

为了检索出 1010 个满足过滤条件的向量,索引大约返回了 970970 行数据。这意味着索引执行了大量冗余计算。如果索引能够利用过滤条件进行剪枝,计算量将会大幅减少。为此,向量索引提供了 GUC 参数 vchordrq.prefilter,允许根据过滤条件对搜索空间进行剪枝。

SET vchordrq.prefilter = on;

预过滤(Prefilter)使向量索引能够在搜索过程中结合过滤条件进行判断,从而剪枝搜索空间;更小的搜索空间意味着更高的搜索效率。不过,检查过滤条件是否满足本身也会引入额外开销。因此,仅当过滤条件严格(能够排除大量行)且轻量(计算开销远低于计算向量距离)时,才推荐使用预过滤。为了便于理解,这里给出两个不适合使用预过滤的反例:

  • id % 97 > 0:该过滤条件过于宽松
  • email ~ '^([a-zA-Z]+)*$':该过滤条件计算开销过高

根据我们的实验结果,在不同选择率(selectivity)下启用预过滤带来的 QPS 加速比如下:

  • 在 1% 选择率下,QPS 提升 200%
  • 在 10% 选择率下,QPS 提升 5%
Prefilter on LAION-5m

参考

vchordg 不支持此功能。

搜索参数(vchordrq)

vchordrq

vchordrq.prefilter

since v0.4.0
  • 说明:GUC 参数 vchordrq.prefilter 允许在计算距离之前先对过滤条件进行求值。例如,在查询 SELECT * FROM items WHERE id % 2 = 0 ORDER BY embedding <-> '[3,1,2]' LIMIT 5 中,正常情况下索引会先计算所有候选行的 embedding <-> '[3,1,2]' 距离,然后将行传递给 PostgreSQL,由 PostgreSQL 过滤掉 id % 2 != 0 的行。启用该参数后,索引可以在计算距离之前预先评估过滤条件并丢弃不匹配的行,从而提高查询效率。
  • 类型:布尔值(boolean)
  • 默认值:false

12 - 预取

预取(Prefetch)能够大幅加速涉及 I/O 操作的查询。它根据索引的实际需求,主动将缓冲区预读到操作系统页缓存或 PostgreSQL 的缓冲池中。
since v0.4.0

预取(Prefetch)能够大幅加速涉及 I/O 操作的查询。它的工作原理是根据索引的实际访问需求,主动将缓冲区预读到操作系统页缓存(page cache)或 PostgreSQL 的缓冲池(buffer pool)中。

预取有助于将 I/O 与计算重叠执行,以及将不同的 I/O 操作相互重叠执行,从而显著减少查询执行期间等待缓冲区的时间,进而提升整体查询性能。

提示

该功能默认启用。在 VectorChord 0.4.0 及更高版本中,任何查询都能从预取机制中受益。

在 0.4.0 版本之前,VectorChord 使用 PostgreSQL 的同步 I/O 接口。从 0.4.0 版本开始,VectorChord 在较新版本的 PostgreSQL 上使用异步 I/O 接口。VectorChord 所使用的 I/O 接口可以通过 GUC 参数进行控制。

在 GUC 参数中,同步 I/O 接口被称为 read_buffer,异步 I/O 接口被称为 read_stream,而 VectorChord 的模拟异步接口被称为 prefetch_buffer。

对于远小于内存大小的索引,这三种预取模式之间几乎没有差异。read_stream 可以加速所有基于磁盘的索引,但它仅在 PostgreSQL 17 和 18 上可用。为了解决这个问题,VectorChord 提供了一个名为 prefetch_buffer 的模拟异步 I/O 接口,在所有 PostgreSQL 版本上均可用,作为旧版本 PostgreSQL 的回退方案。prefetch_buffer 的效果略逊于 read_stream。

预取模式 说明 支持的 PostgreSQL 版本
read_stream PostgreSQL 中的异步 I/O 接口 PostgreSQL 17, 18
prefetch_buffer VectorChord 的模拟异步 I/O 接口,在旧版本 PostgreSQL 上作为回退方案使用 PostgreSQL 13, 14, 15, 16, 17, 18
read_buffer PostgreSQL 中的同步 I/O 接口 PostgreSQL 13, 14, 15, 16, 17, 18

要在 PostgreSQL 18 上通过 read_stream 充分发挥此功能的威力,您需要启用异步 I/O。另请参阅 PostgreSQL 调优。

参考

搜索参数(vchordrq)

vchordrq

since v0.4.0
  • 说明:该 GUC 参数控制在向量搜索中读取位向量(bit vector)时的 I/O 预取策略,会影响基于磁盘向量的搜索性能。
  • 类型:字符串(string)
  • 取值范围:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:{"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18:{"read_buffer", "prefetch_buffer", "read_stream"}
  • 默认值:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:prefetch_buffer
    • PostgreSQL 17, 18:read_stream
  • 注意:
    • read_buffer 表示优先使用 ReadBuffer。
    • prefetch_buffer 表示优先结合使用 PrefetchBuffer 和 ReadBuffer。
    • read_stream 表示优先使用 read_stream。

vchordrq.io_rerank

since v0.4.0
  • 说明:该 GUC 参数控制在向量搜索中读取全精度向量时的 I/O 预取策略,会显著影响基于磁盘向量的搜索性能。
  • 类型:字符串(string)
  • 取值范围:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:{"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18:{"read_buffer", "prefetch_buffer", "read_stream"}
  • 默认值:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:prefetch_buffer
    • PostgreSQL 17, 18:read_stream
  • 注意:
    • read_buffer 表示优先使用 ReadBuffer。
    • prefetch_buffer 表示优先结合使用 PrefetchBuffer 和 ReadBuffer。
    • read_stream 表示优先使用 read_stream。

搜索参数(vchordg)

vchordg

since v0.5.0
  • 说明:该 GUC 参数控制在向量搜索中读取位向量(bit vector)时的 I/O 预取策略,会影响基于磁盘向量的搜索性能。
  • 类型:字符串(string)
  • 取值范围:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:{"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18:{"read_buffer", "prefetch_buffer", "read_stream"}
  • 默认值:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:prefetch_buffer
    • PostgreSQL 17, 18:read_stream
  • 注意:
    • read_buffer 表示优先使用 ReadBuffer。
    • prefetch_buffer 表示优先结合使用 PrefetchBuffer 和 ReadBuffer。
    • read_stream 表示优先使用 read_stream。

vchordg.io_rerank

since v0.5.0
  • 说明:该 GUC 参数控制在向量搜索中读取全精度向量时的 I/O 预取策略,会显著影响基于磁盘向量的搜索性能。
  • 类型:字符串(string)
  • 取值范围:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:{"read_buffer", "prefetch_buffer"}
    • PostgreSQL 17, 18:{"read_buffer", "prefetch_buffer", "read_stream"}
  • 默认值:取决于 PostgreSQL 版本
    • PostgreSQL 13, 14, 15, 16:prefetch_buffer
    • PostgreSQL 17, 18:read_stream
  • 注意:
    • read_buffer 表示优先使用 ReadBuffer。
    • prefetch_buffer 表示优先结合使用 PrefetchBuffer 和 ReadBuffer。
    • read_stream 表示优先使用 read_stream。

13 - 表内重排序

PostgreSQL 索引会存储行标识符以及被索引列的值。启用 rerank_in_table 可避免在索引中重复存储原始向量,从而节省磁盘空间。
since v0.2.1

PostgreSQL 索引会存储行标识符(row identifier)以及被索引列的值。通常情况下,被索引的列具有简单的数据类型,例如整数或长度有限的短字符串。

然而,如果被索引的列是向量类型,那么表和索引都会存储向量,这意味着您需要为这些向量支付双倍的存储成本。如果您希望降低这部分开销,可以在构建索引时启用 rerank_in_table。启用后,索引将采用一种巧妙的方式:它自身不再存储原始向量,而是在需要时直接从表中读取它们。

CREATE INDEX ON items USING vchordrq (embedding vector_l2_ops) WITH (options = $$
rerank_in_table = true
residual_quantization = true
$$);

启用该选项会导致查询性能下降,并且在启用 rerank_in_table 时,预取(prefetch)功能将无法生效。

这是一种以时间换空间的折中方案,通常不推荐使用,仅适用于需要解决磁盘存储空间极度紧张的特定场景。

参考

vchordg 不支持此功能。

索引选项(vchordrq)

vchordrq

rerank_in_table

since v0.2.1
  • 说明:该选项决定是否从表中读取向量进行重排序。如果启用,索引所需的存储空间将减少,但查询延迟会显著增加。仅当磁盘空间极其有限时才应启用此选项。
  • 类型:布尔值(boolean)
  • 默认值:false
  • 示例:
    • rerank_in_table = false 表示向量同时存储在索引和表中,搜索时从索引中读取向量。
    • rerank_in_table = true 表示向量仅存储在表中,搜索时从表中读取向量。

14 - 分区调优

对于大表而言,分区阶段主导了整个索引构建的时间与内存消耗。

对于大表而言,分区阶段主导了整个索引构建的时间与内存消耗。

分区阶段的时间复杂度为 O(FC2DL)O(FC^{2}DL),内存消耗(以字节为单位)可以近似表示为 4DC(F+T+1)4DC(F + T + 1),

其中:

  • DD 是向量维度(如果设置了 build.internal.kmeans_dimension,则为该参数的值)。
  • FF 是 build.internal.sampling_factor。
  • CC 是 build.internal.lists[-1]。
  • TT 是 build.internal.build_threads。
  • LL 是 build.internal.kmeans_iterations。

本页介绍几种用于优化构建时间和内存消耗的调优技巧。

层次 K-means

层次 K-means(Hierarchical K-means)是一种比劳埃德算法(Lloyd’s algorithm)更快的聚类算法。应用该算法后,渐近时间复杂度将降低至 O(FC1.5DL)O(FC^{1.5}DL)。作为折中,它会对搜索性能和召回率产生轻微的负面影响。

用法:

build.internal.kmeans_algorithm.hierarchical = {}

降低采样因子

降低采样因子(sampling factor)可以线性地改善构建时间和内存消耗。作为折中,它会对搜索性能和召回率产生中等程度的负面影响。

用法:

build.internal.sampling_factor = 64

降维

降维功能使用快速约翰逊-林登施特劳斯变换(Fast Johnson–Lindenstrauss transform),将参与聚类的向量维度降低至原始维度以下,从而线性地改善构建时间和内存消耗。作为折中,它会对搜索性能和召回率产生轻微的负面影响。

用法:

build.internal.kmeans_dimension = 100

参考配置

LAION 100M

数据集:

  • 来源:https://sisap-challenges.github.io/2024/datasets/
  • 度量方式:内积(Inner Product)
  • 维度:768
  • 向量数量:100,000,000
  • 测试向量数量:1,000

测试机器(AWS EC2 i7i.4xlarge):

  • CPU:Intel Xeon Platinum 8559C
  • vCPU 数量:16
  • 内存:128 GiB
  • 存储:3750 GiB NVMe SSD
CREATE INDEX ON laion USING vchordrq (embedding vector_ip_ops) WITH (options = $$
build.pin = 2
build.internal.lists = [400, 160000]
build.internal.build_threads = 16
build.internal.spherical_centroids = true
build.internal.kmeans_algorithm.hierarchical = {}
build.internal.kmeans_dimension = 100
build.internal.sampling_factor = 64
$$);

测试结果:

  • 总构建时间:18 分钟
  • 峰值内存:6 GiB
  • 测试用例 1
    • 参数
      • vchordrq.probes = '40,140'
      • vchordrq.epsilon = 1.4
    • QPS @ Top10:120
    • Recall @ Top10:0.949

DEEP 1B

数据集:

  • 来源:https://big-ann-benchmarks.com/neurips21.html
  • 度量方式:欧氏距离(Euclidean)
  • 维度:96
  • 向量数量:1,000,000,000
  • 测试向量数量:10,000

测试机器(AWS EC2 i7ie.6xlarge):

  • CPU:Intel Xeon Platinum 8559C
  • vCPU 数量:24
  • 内存:192 GiB
  • 存储:7500 GiB NVMe SSD
CREATE INDEX ON deep USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.pin = 2
residual_quantization = true
build.internal.lists = [800, 640000]
build.internal.build_threads = 24
build.internal.kmeans_algorithm.hierarchical = {}
$$);

测试结果:

  • 总构建时间:107 分钟
  • 峰值内存:64 GiB
  • 测试用例 1
    • 参数
      • vchordrq.probes = '40,250'
    • QPS @ Top10:68
    • Recall @ Top10:0.951

15 - 外部构建

除了使用分区调优中的方法外,您还可以选择在其他机器或 GPU 上离线运行分区阶段,以缩短索引构建时间并降低内存消耗。

除了使用分区调优中的方法(以牺牲一定 QPS 和召回率为代价来缩短构建时间并降低内存消耗)之外,您还可以选择在其他机器或 GPU 上离线运行分区阶段。由于 vchordg 没有分区阶段,因此该特性不支持 vchordg。

假设表名为 t,待建索引的列名为 val。

CREATE TABLE t (val vector(3));

具体而言,您需要先对待建索引的列进行采样。

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;
SELECT val FROM t TABLESAMPLE SYSTEM_ROWS(1000);

基于这些样本,您可以将向量空间划分为多个 Voronoi 单元(Voronoi cells)。完成划分后,创建一张表并将分区质心插入其中。

CREATE TABLE public.t_build (id INTEGER NOT NULL UNIQUE, parent INTEGER, vector vector NOT NULL);
INSERT INTO public.t_build (id, parent, vector) VALUES (0, NULL, '{0.1, 0.2, 0.3}');
INSERT INTO public.t_build (id, parent, vector) VALUES (1, 0, '{0.1, 0.2, 0.3}');
INSERT INTO public.t_build (id, parent, vector) VALUES (2, 0, '{0.4, 0.5, 0.6}');
INSERT INTO public.t_build (id, parent, vector) VALUES (3, 0, '{0.7, 0.8, 0.9}');

随后可以使用以下语法创建索引:

CREATE INDEX ON t USING vchordrq (embedding vector_l2_ops) WITH (options = $$
build.external.table = 'public.t_build'
$$);

表结构格式

存储分区信息的表必须严格遵循以下表结构(Schema):

  • 第一列必须为 id,类型为 integer,不可为 null(NOT NULL),且必须唯一(UNIQUE)。
  • 第二列必须为 parent,类型为 integer,允许为 null(根节点为 NULL)。
  • 第三列必须为 vector,类型为 vector,不可为 null(NOT NULL)。目前尚不支持 halfvec 或其他类型。

在逻辑上,这些数据构成了一棵树。从根节点到每个叶子节点的距离(深度)必须完全相同。

如果上述任何属性未得到满足,系统都会报错。

分区示例

为了帮助您快速上手,下面给出一个使用 faiss 执行向量空间分区的最小代码示例。

from typing import List
from faiss import Kmeans
import numpy

def partition(
    samples: List[numpy.Array[float]], lists: List[int]
) -> List[numpy.Array[float]]:
    dim = dataset.shape[1]
    results = []
    for i in range(len(lists) + 1):
        kmeans = Kmeans(
            dim,
            lists[len(lists) - 1 - i] if len(lists) - 1 - i >= 0 else 1,
            gpu=False,
            verbose=True,
            niter=10,
            seed=42,
            spherical=False,
        )
        kmeans.train(samples)
        results.push(kmeans.centroids)
        samples = kmeans.centroids
    return results

这段代码会计算出不同层级上所有 Voronoi 单元的生成元(质心)。树中的父子关系可通过计算最短距离来确定,此处省略具体细节。您可以使用任何能够生成多级 Voronoi 单元的算法,例如球面 K-means(spherical K-means)、平衡 K-means(balanced K-means)或 GPU K-means。