暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

DuckDB 向量相似度搜索带来哪些重大更新?

alitrack 2024-10-29
507

DuckDB 向量相似度搜索带来哪些重大更新?

✍️: Max Gabrielsson


🔗:https://duckdb.org/2024/10/23/whats-new-in-the-vss-extension.html



在 上一篇博客文章 中,我们介绍了 DuckDB 的 向量相似度搜索 (VSS) 扩展[1]。尽管该扩展仍处于实验阶段,我们认为详细说明一些自首次发布以来的新增功能和改进会很有趣。

索引速度提升

如之前文档所述,针对已有数据表创建 HNSW(Hierarchical Navigable Small World graphs,即分层可导航小世界)索引,要比先创建索引再插入数据高效得多。这是因为如果能提前知道表的总行数,就能更准确地预测索引大小,从而将任务分配到多个线程。然而,早期版本中的任务分配过于粗放,每 行组[2] (默认大约 120,000 行)仅安排了一个额外线程。

我们现在引入了一个新的缓冲步骤,使任务分配更为细化,内存分配更加智能,并减少线程间的争抢。这显著提高了 CPU 利用率,无论底层表的大小如何,在多线程环境中构建 HNSW 索引的速度有了明显提升。

此外,这一改进还允许在索引创建时显示进度条,即便索引构建仍需一定时间,但资源利用率已经大幅提升。

新的距离函数

在 VSS 的最初版本中,我们提供了三种距离函数:
array_distance[3]
array_cosine_similarity[4],以及 array_inner_product[5]。不过,严格来说,只有 array_distance
 是真正的“距离”函数,它在向量相似时返回接近 0 的值,在不相似时返回接近 1。而例如 array_cosine_similarity
 返回的是向量完全相同时的 1。

为了解决这一问题,我们引入了两个新距离函数:

  • • array_cosine_distance
    ,等效于 1 - array_cosine_simililarity

  • • array_negative_inner_product
    ,等效于 -array_inner_product

现在,这些函数都能使用 HNSW 索引进行加速,保证无论使用何种度量标准,查询模式和排序都能保持一致。此外,如果您使用的是 cosine
 度量,并且通过 1 - array_cosine_similarity
 编写 top-k 风格的查询,优化器会将其自动规范为 array_cosine_distance
 并使用索引加速查询。

为了保持一致性,我们还为 LIST 数据类型[6] 添加了相应的距离函数(以 list_
 作为前缀),并将 <=>
 二元操作符改为 array_cosine_distance
 的别名,与 PostgreSQL 的 pgvector 扩展[7] 相匹配。

索引加速的 Top-K
 聚合

自上次更新以来,DuckDB 核心功能中又新增了对 min_by[8] 和 max_by[9] 聚合函数(及其别名 arg_min
 和 arg_max
)的额外支持。

这些新函数增加了一个可选参数 n
,用于指定保留前 n
 个元素,并将它们输出为排序的 LIST
。以下是一个示例:

-- 创建一个包含示例数据的表
CREATE OR REPLACE TABLE vecs AS 
    SELECT
        row_number() OVER () AS id, 
        [a, b, c]::FLOAT[3AS vec 
    FROM
        range(1,4AS x(a), range(1,4AS y(b), range(1,4AS z(c);

-- 查找与向量 [2, 2, 2] 最近的前三行
SELECT
    arg_min(vecs, array_distance(vec, [222]::FLOAT[3]), 3)
FROM
    vecs;

[{'id': 14, 'vec': [2.0, 2.0, 2.0]}, {'id': 13, 'vec': [2.0, 1.0, 2.0]}, {'id': 11, 'vec': [1.0, 2.0, 2.0]}]

当排序依据为引用索引向量列的距离函数时,VSS 扩展现在包含了

优化规则,能够使用 HNSW 索引加速 top-k 聚合。这样,您可以使用更简洁易读的方式表达查询,同时仍然避免对底层表进行全表扫描和排序(只要该表具有匹配的 HNSW 索引)。

索引加速的 LATERAL
 联接

在对 VSS 初始版本的基准测试中,我们发现虽然 HNSW 索引查找速度非常快(得益于其基于的 USearch[10] 库),但 DuckDB 在处理单个向量搜索时的延迟较高。问题不在于 USearch 实现,而在于 DuckDB 并未针对单点查询进行优化(即只处理一行的查询)。因为 DuckDB 基于向量化执行引擎,最小的工作单元是 2,048 行,我们通常会花费大量时间来优化查询计划和预分配内存缓存,以便在实际执行时提高效率。然而,当查询的数据集非常小时,这些优化显得多余。

因此,我们采取了另一种方法,专注于 DuckDB 的长处:处理大规模数据。与其优化“1:N”查询(即“给定一个嵌入,返回最接近的 N 个嵌入”),我们转而优化“N:M”查询(即“给定 N 个嵌入,找到每个嵌入最接近的 M 个匹配项”)。这就是 LATERAL 联接[11] 的用武之地。

我们现在可以使用 HNSW 索引加速 LATERAL
 联接,其中“内层”查询类似于常见的 top-k 风格查询,例如:

SELECT a
FROM b
ORDER BY array_distance(a.vec, query_vec)
LIMIT k;

但其中的 query_vec
 数组现在是“外层”联接表的引用。唯一的要求是内层表的向量列上存在与距离函数匹配的 HNSW 索引。以下是一个示例:

-- 设置随机种子以确保结果可复现
SELECT setseed(0.42);

-- 创建一些示例表
CREATE TABLE queries AS
    SELECT
        i AS id,
        [random(), random(), random()]::FLOAT[3AS embedding 
    FROM generate_series(110_000) r(i);

CREATE TABLE items AS
    SELECT
        i AS id,
        [random(), random(), random()]::FLOAT[3AS embedding
FROM generate_series(110_000) r(i);

-- 收集与每个查询嵌入最接近的 5 个 items
SELECT queries.id as id, list(inner_id) AS matches 
    FROM queries, LATERAL (
        SELECT
            items.id AS inner_id,
            array_distance(queries.embedding, items.embedding) AS dist
        FROM items 
        ORDER BY dist 
        LIMIT 5
    )
GROUP BY queries.id;

在配备 36 GB 内存的 Apple M3 Pro Macbook 上执行此查询大约需要 10 秒。

如果我们对这个查询计划使用 EXPLAIN
,可以看到一些高级操作符:

PRAGMA explain_output = 'optimized_only';
EXPLAIN ...

查询计划(操作符和预期基数)

┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│       ~5000000 Rows       │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│       HASH_GROUP_BY       │
│    ────────────────────   │
│       ~5000000 Rows       │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│    ────────────────────   │
│       ~10000000 Rows      │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│    ────────────────────   │
│       ~10000000 Rows      │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│      RIGHT_DELIM_JOIN     │
│    ────────────────────   │
│       ~10000000 Rows      ├──────────────┐
└─────────────┬─────────────┘              │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│         SEQ_SCAN          ││         HASH_JOIN         │
│    ────────────────────   ││    ────────────────────   │
│        ~10000 Rows        ││       ~10000000 Rows      ├──────────────┐
└───────────────────────────┘└─────────────┬─────────────┘              │
                             ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
                             │         PROJECTION        ││         DUMMY_SCAN        │
                             │    ────────────────────   ││                           │
                             │       ~10000000 Rows      ││                           │
                             └─────────────┬─────────────┘└───────────────────────────┘
                             ┌─────────────┴─────────────┐
                             │           FILTER          │
                             │    ────────────────────   │
                             │       ~10000000 Rows      │
                             └─────────────┬─────────────┘
                             ┌─────────────┴─────────────┐
                             │         PROJECTION        │
                             │    ────────────────────   │
                             │       ~50000000 Rows      │
                             └─────────────┬─────────────┘
                             ┌─────────────┴─────────────┐
                             │           WINDOW          │
                             └─────────────┬─────────────┘
                             ┌─────────────┴─────────────┐
                             │         PROJECTION        │
                             │    ────────────────────   │
                             │       ~50000000 Rows      │
                             └─────────────┬─────────────┘
                             ┌─────────────┴─────────────┐
                             │       CROSS_PRODUCT       ├──────────────┐
                             └─────────────┬─────────────┘              │
                             ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
                             │         SEQ_SCAN          ││         DELIM_SCAN        │
                             │    ────────────────────   ││    ────────────────────   │
                             │        ~10000 Rows        ││         ~5000 Rows        │
                             └───────────────────────────┘└───────────────────────────┘

这个计划看起来非常复杂,但最令人担忧的操作符是计划底部的 CROSS_PRODUCT
,它使得预期基数急剧上升,表明我们在进行大量不必要的计算。然而,如果我们为 items
 表创建一个 HNSW 索引:

CREATE INDEX my_hnsw_idx ON items USING HNSW(embedding);

然后重新运行 EXPLAIN
,我们将得到简化的计划:

使用 HNSW 索引的简化查询计划

┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│       HASH_GROUP_BY       │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         PROJECTION        │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│      HNSW_INDEX_JOIN      │
│    ────────────────────   │
│        ~50000 Rows        │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         SEQ_SCAN          │
│    ────────────────────   │
│        ~10000 Rows        │
└───────────────────────────┘

我们可以看到计划被大幅简化,最重要的是,新的 HNSW_INDEX_JOIN
 操作符替代了之前的 CROSS_PRODUCT
,预计的基数从 5,000,000 降到了 50,000!执行时间也从 10 秒缩短到约 0.15 秒,速度提升了 66 倍!

这个优化最近才被添加到 VSS 扩展中,如果您已经为 DuckDB v1.1.2 安装了 vss
,可以通过以下命令获取最新版本:

UPDATE EXTENSIONS (vss);

结论

这次更新就到这里!我们希望您喜欢 DuckDB 向量相似度搜索扩展的新功能介绍。这次更新侧重于新特性和优化,如更快的索引构建、额外的距离函数和优化规则,但我们仍在努力改进之前文章中提到的一些限制。希望不久后能分享更多与自定义索引及基于索引的优化相关的内容!如果您有任何问题或反馈,请在 duckdb-vss GitHub 仓库[12] 或 DuckDB Discord[13] 上联系我们。期待您的加入!

引用链接

[1]
 向量相似度搜索 (VSS) 扩展: https://duckdb.org/docs/extensions/vss
[2]
 行组: https://duckdb.org/docs/internals/storage#row-groups
[3]
 array_distance: https://duckdb.org/docs/sql/functions/array#array_distancearray1-array2
[4]
 array_cosine_similarity: https://duckdb.org/docs/sql/functions/array#array_cosine_similarityarray1-array2
[5]
 array_inner_product: https://duckdb.org/docs/sql/functions/array#array_inner_productarray1-array2
[6]
 LIST 数据类型: https://duckdb.org/docs/sql/data_types/list
[7]
 pgvector 扩展: https://github.com/pgvector/pgvector
[8]
 min_by: https://duckdb.org/docs/sql/functions/aggregates#min_byarg-val-n
[9]
 max_by: https://duckdb.org/docs/sql/functions/aggregates#max_byarg-val-n
[10]
 USearch: https://github.com/unum-cloud/usearch
[11]
 LATERAL 联接: https://duckdb.org/docs/sql/query_syntax/from#lateral-joins
[12]
 duckdb-vss GitHub 仓库: https://github.com/duckdb/duckdb_vss
[13]
 DuckDB Discord: https://discord.duckdb.org/


文章转载自alitrack,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论