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

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

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

如果数据集较小,您可以使用 K 最近邻 (KNN) 来查找精确的 k 最近邻向量。不过,随着数据集的增大,KNN 搜索的延迟时间和费用也会增加。您可以使用 ANN 查找近似 k 最近邻,从而显著缩短延迟时间、降低成本。

近似 K 最近邻

在 ANN 搜索中,返回的 k 个向量并非真正的 Top-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 (
  DocId INT64 NOT NULL,
  ...
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

填充 Documents 表后,您可以使用余弦距离在具有嵌入列 DocEmbeddingDocuments 表上创建具有双层树和 1000 个叶节点的向量索引:

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

如需创建具有三级树和 100 万个叶节点的向量索引,请执行以下操作:

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);

过滤向量索引

您还可以创建过滤后的向量索引,以查找数据库中与过滤条件最匹配的相似项。过滤后的向量索引会选择性地为满足指定过滤条件的行编制索引,从而提高搜索性能。

在以下示例中,表 Documents 包含一个名为 Category 的列。在我们的矢量搜索中,我们希望为“技术”类别编制索引,因此我们创建了一个生成的列,如果未满足类别条件,该列的计算结果为 NULL。

CREATE TABLE Documents (
  DocId INT64 NOT NULL,
  Category STRING(MAX),
  NullIfFiltered BOOL AS (IF(Category = 'Tech', TRUE, NULL)) HIDDEN,
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

然后,我们创建一个带有过滤器的向量索引。TechDocEmbeddingIndex 向量索引仅对“技术”类别中的文档编制索引。

CREATE VECTOR INDEX TechDocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING(NullIfFiltered)
  WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
  OPTIONS (...);

当 Spanner 运行以下查询时,如果查询的过滤条件与 TechDocEmbeddingIndex 相匹配,系统会自动选择 TechDocEmbeddingIndex 并通过它来加速查询。该查询仅搜索“技术”类别中的文档。您还可以使用 {@FORCE_INDEX=TechDocEmbeddingIndex} 强制 Spanner 明确使用 TechDocEmbeddingIndex

SELECT *
FROM Documents
WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
ORDER BY APPROX_(....)
LIMIT 10;

查询向量嵌入

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

  • 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(树级别):如果被编入索引的表的行数少于 1, 000 万,请使用 tree_depth2。否则,tree_depth3 时,支持的表格最多可包含约 100 亿行。

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

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

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

  1. num_leaves 设置为其原始值的某个倍数 k(例如 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. 删除过时的向量索引。

后续步骤