⌘+k ctrl+k
1.1.3 (stable)
Search Shortcut cmd + k | ctrl + k
Vector Similarity Search Extension

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

请参阅公告博客文章“向量相似性搜索扩展中的新功能?”文章

Usage

要在具有ARRAY列的表格上创建新的HNSW(分层可导航小世界)索引,请使用带有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) AS result FROM my_vector_table;
---- [{'vec': [1.0, 2.0, 3.0]}, {'vec': [1.0, 2.0, 4.0]}, {'vec': [2.0, 2.0, 3.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 扩展还提供了一些表宏来简化多个向量之间的匹配,即所谓的“模糊连接”。这些宏包括:

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

这些目前没有使用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, 3) AS 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': 364, 'vec': [5.0, 4.0, 5.0]} │
│   1.0 │ {'search_vec': [5.0, 5.0, 5.0]} │ {'id': 356, 'vec': [4.0, 5.0, 5.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': 10, 'vec': [2.0, 1.0, 1.0]}  │
│   1.0 │ {'search_vec': [1.0, 1.0, 1.0]} │ {'id': 2, 'vec': [1.0, 2.0, 1.0]}   │
└───────┴─────────────────────────────────┴─────────────────────────────────────┘
-- Alternatively, we can use the vss_match macro as a "lateral join"
-- to get the matches already grouped by the left table.
-- Note that this requires us to specify the left table first, and then
-- the vss_match macro which references the search column from the left
-- table (in this case, `search_vec`).
SELECT * FROM needle, vss_match(haystack, search_vec, vec, 3) AS 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': 364, 'vec': [5.0, 4.0, 5.0]}}, {'score': 1.0, 'row': {'id': 356, 'vec': [4.0, 5.0, 5.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': 10, 'vec': [2.0, 1.0, 1.0]}}, {'score': 1.0, 'row': {'id': 2, 'vec': [1.0, 2.0, 1.0]}}]      │
└─────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Limitations

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