⌘+k ctrl+k
1.3 (稳定版)
搜索快捷键 cmd + k | ctrl + k
向量相似度搜索扩展

DuckDB 的 vss 扩展是一个实验性扩展,它添加了索引支持,以使用 DuckDB 新的固定大小 ARRAY 类型加速向量相似度搜索查询。

请参阅公告博客文章“向量相似度搜索扩展有什么新功能?”文章

用法

要在包含 ARRAY 列的表上创建新的 HNSW(Hierarchical Navigable Small Worlds)索引,请使用带 USING HNSW 子句的 CREATE INDEX 语句。例如

INSTALL vss;
LOAD vss;

CREATE TABLE my_vector_table (vec FLOAT[3]);
INSERT INTO my_vector_table
    SELECT array_value(a, b, c)
    FROM range(1, 10) ra(a), range(1, 10) rb(b), range(1, 10) rc(c);
CREATE INDEX my_hnsw_index ON my_vector_table USING HNSW (vec);

索引随后将用于加速查询,这些查询使用 ORDER BY 子句评估索引列与常量向量之间支持的距离度量函数之一,后跟 LIMIT 子句。例如

SELECT *
FROM my_vector_table
ORDER BY array_distance(vec, [1, 2, 3]::FLOAT[3])
LIMIT 3;

此外,如果 arg 参数是匹配的距离度量函数,则重载的 min_by(col, arg, n) 也可以通过 HNSW 索引进行加速。这可用于快速一次性最近邻搜索。例如,要获取与 [1, 2, 3] 最接近的向量的前 3 行

SELECT min_by(my_vector_table, array_distance(vec, [1, 2, 3]::FLOAT[3]), 3 ORDER BY vec) AS result
FROM my_vector_table;
[{'vec': [1.0, 2.0, 3.0]}, {'vec': [2.0, 2.0, 3.0]}, {'vec': [1.0, 2.0, 4.0]}]

请注意,我们如何将表名作为第一个参数传递给 min_by,以返回一个包含整个匹配行的结构体。

我们可以通过检查 EXPLAIN 输出并查找计划中的 HNSW_INDEX_SCAN 节点来验证索引是否正在使用。

EXPLAIN
SELECT *
FROM my_vector_table
ORDER BY array_distance(vec, [1, 2, 3]::FLOAT[3])
LIMIT 3;
┌───────────────────────────┐
│         PROJECTION        │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│             #0            │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│            vec            │
│array_distance(vec, [1.0, 2│
│         .0, 3.0])         │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│      HNSW_INDEX_SCAN      │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│   t1 (HNSW INDEX SCAN :   │
│           my_idx)         │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│            vec            │
│   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │
│           EC: 3           │
└───────────────────────────┘

默认情况下,HNSW 索引将使用欧几里得距离 l2sq(L2 范数平方)度量创建,匹配 DuckDB 的 array_distance 函数,但可以通过在索引创建期间指定 metric 选项来使用其他距离度量。例如

CREATE INDEX my_hnsw_cosine_index
ON my_vector_table
USING HNSW (vec)
WITH (metric = 'cosine');

下表显示了支持的距离度量及其对应的 DuckDB 函数

度量 函数 描述
l2sq array_distance 欧几里得距离
cosine array_cosine_distance 余弦相似度距离
ip array_negative_inner_product 负内积

请注意,虽然每个 HNSW 索引仅适用于单个列,但您可以在同一表上创建多个 HNSW 索引,每个索引独立索引不同的列。此外,您还可以在同一列上创建多个 HNSW 索引,每个索引支持不同的距离度量。

索引选项

除了 metric 选项,HNSW 索引创建语句还支持以下选项来控制索引构建和搜索过程的超参数

选项 默认值 描述
ef_construction 128 在索引构建过程中要考虑的候选顶点数量。值越高,索引越准确,但构建索引所需的时间也会增加。
ef_search 64 在索引搜索阶段要考虑的候选顶点数量。值越高,索引越准确,但执行搜索所需的时间也会增加。
M 16 图中每个顶点保留的最大邻居数量。值越高,索引越准确,但构建索引所需的时间也会增加。
M0 2 * M 基础连接性,即图中零层每个顶点保留的邻居数量。值越高,索引越准确,但构建索引所需的时间也会增加。

此外,您还可以在运行时通过设置 SET hnsw_ef_search = int 配置选项来覆盖在索引构建时设置的 ef_search 参数。如果您希望在每次连接时权衡搜索性能和准确性,或者反之,这会很有用。您还可以通过调用 RESET hnsw_ef_search 来取消覆盖。

持久性

由于自定义扩展索引持久性的一些已知问题,默认情况下,HNSW 索引只能在内存数据库的表上创建,除非将 SET hnsw_enable_experimental_persistence = bool 配置选项设置为 true

将此功能锁定在实验性标志后面,原因是“WAL”恢复尚未为自定义索引正确实现,这意味着如果崩溃发生或数据库在 HNSW 索引表有未提交更改时意外关闭,您可能会遇到数据丢失或索引损坏

如果启用此选项并遇到意外关机,您可以尝试通过首先单独启动 DuckDB、加载 vss 扩展,然后 ATTACH 数据库文件来恢复索引,这确保了在 WAL 回放期间 HNSW 索引功能可用,从而允许 DuckDB 的恢复过程顺利进行。但我们仍然建议您不要在生产环境中使用此功能。

启用 hnsw_enable_experimental_persistence 选项后,索引将持久化到 DuckDB 数据库文件(如果您使用磁盘支持的数据库文件运行 DuckDB),这意味着在数据库重新启动后,索引可以从磁盘重新加载到内存中,而无需重新创建。考虑到这一点,持久性索引存储没有增量更新,因此每当 DuckDB 执行检查点时,整个索引都将被序列化到磁盘并覆盖自身。同样,在数据库重新启动后,索引将完整地反序列化回主内存。尽管这将在您首次访问与索引关联的表时延迟。根据索引的大小,反序列化过程可能需要一些时间,但它仍应比简单地删除和重新创建索引更快。

插入、更新、删除和重新压缩

HNSW 索引支持在索引创建后从表中插入、更新和删除行。但是,有两点需要记住

  • 在用数据填充表后创建索引会更快,因为初始批量加载可以更好地利用大型表上的并行性。
  • 删除不会立即反映在索引中,而是被“标记”为已删除,这可能导致索引随时间变得陈旧,并对查询质量和性能产生负面影响。

为了解决最后一点,您可以调用 PRAGMA hnsw_compact_index('index_name') pragma 函数来触发索引的重新压缩,修剪已删除的项,或者在大量更新后重新创建索引。

附赠:向量相似度搜索连接

vss 扩展还提供了一些表宏,以简化多个向量之间的匹配,即所谓的“模糊连接”(fuzzy joins)。它们是

  • vss_join(left_table, right_table, left_col, right_col, k, metric := 'l2sq')
  • vss_match(right_table", left_col, right_col, k, metric := 'l2sq')

k 是从 right_table 中为每个 left_table 记录选择的记录数量,按分数排序。

这些目前使用 HNSW 索引,但作为方便的实用函数提供给那些乐于执行暴力向量相似度搜索而无需自己编写连接逻辑的用户。将来这些也可能成为基于索引优化的目标。

这些函数可以按如下方式使用

CREATE TABLE haystack (id int, vec FLOAT[3]);
CREATE TABLE needle (search_vec FLOAT[3]);

INSERT INTO haystack
    SELECT row_number() OVER (), array_value(a, b, c)
    FROM range(1, 10) ra(a), range(1, 10) rb(b), range(1, 10) rc(c);

INSERT INTO needle
    VALUES ([5, 5, 5]), ([1, 1, 1]);

SELECT *
FROM vss_join(needle, haystack, search_vec, vec, 2) res;
┌───────┬─────────────────────────────────┬─────────────────────────────────────┐
│ score │            left_tbl             │              right_tbl              │
│ float │   struct(search_vec float[3])   │  struct(id integer, vec float[3])   │
├───────┼─────────────────────────────────┼─────────────────────────────────────┤
│   0.0 │ {'search_vec': [5.0, 5.0, 5.0]} │ {'id': 365, 'vec': [5.0, 5.0, 5.0]} │
│   1.0 │ {'search_vec': [5.0, 5.0, 5.0]} │ {'id': 356, 'vec': [5.0, 5.0, 4.0]} │
│   0.0 │ {'search_vec': [1.0, 1.0, 1.0]} │ {'id': 1, 'vec': [1.0, 1.0, 1.0]}   │
│   1.0 │ {'search_vec': [1.0, 1.0, 1.0]} │ {'id': 2, 'vec': [1.0, 2.0, 1.0]}   │
└───────┴─────────────────────────────────┴─────────────────────────────────────┘

或者,我们可以将 vss_match 宏用作“横向连接”(lateral join),以获取已经按左表分组的匹配项。请注意,这要求我们首先指定左表,然后指定引用左表搜索列(在此示例中为 search_vec)的 vss_match

SELECT *
FROM needle, vss_match(haystack, search_vec, vec, 2) res;
┌─────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│   search_vec    │                                                         matches                                                          │
│    float[3]     │                              struct(score float, "row" struct(id integer, vec float[3]))[]                               │
├─────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ [5.0, 5.0, 5.0] │ [{'score': 0.0, 'row': {'id': 365, 'vec': [5.0, 5.0, 5.0]}}, {'score': 1.0, 'row': {'id': 356, 'vec': [5.0, 5.0, 4.0]}}] │
│ [1.0, 1.0, 1.0] │ [{'score': 0.0, 'row': {'id': 1, 'vec': [1.0, 1.0, 1.0]}}, {'score': 1.0, 'row': {'id': 2, 'vec': [1.0, 2.0, 1.0]}}]     │
└─────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

限制

  • 目前仅支持由 FLOAT(32位,单精度)组成的向量。
  • 索引本身不是缓冲区管理的,必须能够完全放入 RAM 内存中。
  • 内存中索引的大小不计入 DuckDB 的 memory_limit 配置参数。
  • HNSW 索引只能在内存数据库的表上创建,除非将 SET hnsw_enable_experimental_persistence = ⟨bool⟩ 配置选项设置为 true,更多信息请参见持久性
  • 向量连接表宏(vss_joinvss_match)不需要或不使用 HNSW 索引。