查找近似最近邻、创建向量索引和查询向量嵌入

本页面介绍了如何在 Spanner 中使用以下 ANN 距离函数查找近似最近邻 (ANN)、创建向量索引以及查询向量嵌入:

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

当数据集较小时,您可以使用 K 最近邻 (KNN) 来查找确切的 k 最近向量。但是,随着数据集的不断增加,KNN 搜索的延迟时间和费用也会增加。您可以使用 ANN 找到近似 k 最近邻,并显著缩短延迟时间和成本。

k 最近邻项

在 ANN 搜索中,k 返回的向量不是真正的 k 最近邻,因为 ANN 搜索会计算近似距离,并且可能不会查看数据集中的所有向量。有时,系统会返回一些不属于前 k 个最近邻的向量。这称为召回率损失。您能接受多少召回率取决于用例,但在大多数情况下,为了提升数据库性能而损失少许召回率是可以接受的。

如需详细了解 Spanner 近似距离函数,请参阅:

向量索引

Spanner 使用专用向量索引来加速 ANN 向量搜索。此索引利用了 Google 研究的可扩缩最近邻 (ScaNN),这是一种高效的最近邻算法。

向量索引使用基于树的结构将数据分区并加快搜索速度。Spanner 提供两级和三级树配置:

  • 两级树配置:叶节点 (num_leaves) 包含一组密切相关的向量及其对应的形心。根级别由所有叶节点的形心组成。
  • 三级树形配置:在概念上与两级树类似,但引入了一个额外的分支层 (num_branches),叶节点形心从该层进一步划分,形成根级 (num_leaves)。

Spanner 会为您选择一个索引。但是,如果您知道特定索引效果最佳,则可以使用 FORCE_INDEX 提示选择最适合您的用例的向量索引。

如需了解详情,请参阅 VECTOR INDEX 语句

限制

  • 您无法预先拆分向量索引。如需了解详情,请参阅预拆分概览

创建向量索引

要优化向量索引的召回率和性能,我们建议您:

  • 在将大多数带有嵌入的行写入数据库后,创建矢量索引。在插入新数据后,您可能还需要定期重建向量索引。如需了解详情,请参阅重新构建向量索引

  • 使用 STORING 子句在向量索引中存储列的副本。如果某个列值存储在向量索引中,则 Spanner 会在索引的叶级执行过滤,以提高查询性能。如果某个列用在过滤条件中,我们建议您存储该列。如需详细了解如何在索引中使用 STORING,请参阅为仅限索引的扫描创建索引

创建表时,嵌入列必须是 FLOAT32(推荐)或 FLOAT64 数据类型的数组,并且包含表示向量维度的 vector_length 注解。

以下 DDL 语句会创建一个 Documents 表,其中包含具有向量长度的嵌入列 DocEmbedding

CREATE TABLE Documents {
  ...
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128);
};

填充 Documents 表后,您可以在 Documents 表上创建一个具有两层树和 1000 个叶节点的向量索引,其中包含使用余弦距离的嵌入列 DocEmbedding

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING (WordCount)
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

要创建包含三级树和 1000000 个叶节点的向量索引,请执行以下操作:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 3, num_branches=1000, num_leaves = 1000000);

如果嵌入列在表定义中未标记为 NOT NULL,则必须使用 WHERE column_name IS NOT NULL 子句进行声明:

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

查询向量嵌入

如需查询矢量索引,请使用以下三种近似距离函数之一:

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

使用近似距离函数时,存在以下限制:

  • 近似距离函数必须计算嵌入列与常量表达式(例如参数或字面量)之间的距离。
  • ORDER BY 子句中,必须将近似距离函数输出用作唯一的排序键,并且必须在 ORDER BY 之后指定 LIMIT
  • 查询必须明确过滤掉未编入索引的行。在大多数情况下,这意味着查询必须包含与向量索引定义匹配的 WHERE <column_name> IS NOT NULL 子句,除非该列已在表定义中标记为 NOT NULL

如需查看限制的详细列表,请参阅近似距离函数参考页面

示例

如需搜索最接近 [1.0, 2.0, 3.0] 的 100 个向量,请执行以下操作:

SELECT DocId
FROM Documents
WHERE WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], DocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

如果嵌入列可为 null:

SELECT DocId
FROM Documents
WHERE NullableDocEmbedding IS NOT NULL AND WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], NullableDocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

最佳做法

请遵循以下最佳做法来优化向量索引并改进查询结果。

调整矢量搜索选项

最佳矢量搜索值取决于用例、矢量数据集和查询矢量。您可能需要执行迭代调整才能找到特定工作负载的最佳值。

在选择适当的值时,请遵循以下有用的准则:

  • tree_depth(树级别):如果要编入索引的表少于 1000 万行,则将 tree_depth 设为 2。否则,3tree_depth 支持的表最多包含大约 100 亿行。

  • num_leaves:使用数据集中行数的平方根。值越大,向量索引构建时间就越长。避免设置大于 table_row_count/1000num_leaves,因为这会导致叶项过小且性能不佳。

  • num_leaves_to_search:此选项指定搜索索引的叶节点数量。增加 num_leaves_to_search 可以提高召回率,但也会增加延迟时间和费用。我们建议使用 CREATE VECTOR INDEX 语句中定义的叶片总数 1% 的数字作为 num_leaves_to_search 的值。如果您使用的是过滤条件子句,请提高此值以扩大搜索范围。

如果实现了可接受的召回率,但查询费用过高,导致最大 QPS 较低,请尝试按照以下步骤增加 num_leaves

  1. num_leaves 设置为其原始值的一定倍数(例如 2 * sqrt(table_row_count))。
  2. num_leaves_to_search 设置为其原始值的倍数 k。
  3. 尝试减少 num_leaves_to_search 以降低费用和 QPS,同时保持召回率。

提高回想度

召回率恶化有多种可能性,包括:

  • num_leaves_to_search 太小:您可能会发现为某些查询向量找到最近邻更困难,因此增加 num_leaves_to_search 以搜索更多叶可以提高召回率。最近的查询可能已转而包含更多此类具有挑战性的向量。

  • 向量索引需要重新构建:在创建时针对数据集优化向量索引的树结构,此后将保持静态。因此,如果在创建初始向量索引后添加了明显不同的向量,则树结构可能并非最佳,从而导致召回率较差。

重新构建向量索引

要在不停机的情况下重新构建矢量索引,请执行以下操作:

  1. 在与当前向量索引相同的嵌入列上创建新的向量索引,根据需要更新参数(例如 OPTIONS)。
  2. 索引创建完成后,使用 FORCE_INDEX 提示指向新索引,以更新矢量搜索查询。这样可确保查询使用新的向量索引。您可能还需要在新查询中重新调整 num_leaves_to_search
  3. 删除过时的向量索引。

后续步骤