搜索索引

本页介绍了如何添加搜索索引。系统会对搜索索引中的条目运行全文搜索

如何使用搜索索引

您可以针对要用于全文搜索的任何列创建搜索索引。如需创建搜索索引,请使用 CREATE SEARCH INDEX DDL 语句。如需更新索引,请使用 ALTER SEARCH INDEX DDL 语句。Spanner 会自动构建和维护搜索索引,包括在数据库中发生更改时立即在搜索索引中添加和更新数据。

搜索索引分区

搜索索引可以是分区非分区,具体取决于您要加速的查询类型。

  • 分区索引最适合用于以下场景:应用查询电子邮件邮箱。每个查询都仅限于特定邮箱。

  • 例如,如果要对商品目录中的所有商品类别进行查询,则最好使用非分区查询。

搜索索引用例

除了全文搜索之外,Spanner 搜索索引还支持以下功能:

  • 子字符串搜索,是一种在较大文本正文中搜索较短字符串(子字符串)的查询。
  • 将索引内任何子集的条件组合到单个索引扫描中。

虽然搜索索引支持对非文本数据(例如数字和完全匹配字符串)进行编入索引,但搜索索引最常见的用例是编入文档中的文本。

搜索索引示例

为了展示搜索索引的功能,假设有一个表用于存储音乐专辑的相关信息:

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX)
) PRIMARY KEY(AlbumId);

Spanner 提供了多个用于创建令牌的令牌化函数。如需修改上表,以便用户运行全文搜索来查找专辑名,请使用 TOKENIZE_FULLTEXT 函数根据专辑名创建令牌。然后,创建一个使用 TOKENLIST 数据类型的列,用于存储 TOKENIZE_FULLTEXT 中的令牌化输出。在本示例中,我们创建了 AlbumTitle_Tokens 列。

ALTER TABLE Albums
  ADD COLUMN AlbumTitle_Tokens TOKENLIST
  AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN;

以下示例使用 CREATE SEARCH INDEX DDL 在 AlbumTitle 令牌 (AlbumTitle_Tokens) 上创建搜索索引 (AlbumsIndex):

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

添加搜索索引后,使用 SQL 查询查找与搜索条件匹配的专辑。例如:

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")

数据一致性

创建索引时,Spanner 会使用自动化流程回填数据,以确保一致性。提交写入时,系统会在同一事务中更新索引。Spanner 会自动执行数据一致性检查。

搜索索引架构定义

搜索索引是在表的一个或多个 TOKENLIST 列上定义的。搜索索引包含以下组件:

  • 基表:需要编制索引的 Spanner 表。
  • TOKENLIST:一组列,用于定义需要编入索引的令牌。这些列的顺序无关紧要。

例如,在以下语句中,基本表为 Albums。系统会在 AlbumTitle (AlbumTitle_Tokens) 和 Rating (Rating_Tokens) 上创建 TOKENLIST 列。

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  SingerId INT64 NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumTitle STRING(MAX),
  Rating FLOAT64,
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Rating_Tokens TOKENLIST AS (TOKENIZE_NUMBER(Rating)) HIDDEN
) PRIMARY KEY(AlbumId);

使用以下 CREATE SEARCH INDEX 语句,使用 AlbumTitleRating 的令牌创建搜索索引:

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC

搜索索引具有以下选项:

  • 分区:用于划分搜索索引的可选列组。查询分区索引通常比查询非分区索引更高效。如需了解详情,请参阅分区搜索索引
  • 排序顺序列:一个可选的 INT64 列,用于确定从搜索索引检索的顺序。如需了解详情,请参阅搜索索引排序顺序
  • 交错:与二级索引一样,您可以交错搜索索引。交错搜索索引在写入和与基表联接时会使用更少的资源。如需了解详情,请参阅交错搜索索引
  • Options 子句:用于替换搜索索引的默认设置的键值对列表。

如需了解详情,请参阅 CREATE SEARCH INDEX 参考文档。

搜索索引的内部布局

搜索索引内部表示法的一个重要元素是 docid,它是基准表的主键(可以任意长度)的高效存储表示法。它还会根据 CREATE SEARCH INDEX 子句中用户提供的 ORDER BY 列创建内部数据布局的顺序。它表示为一个或两个 64 位整数。

搜索索引在内部实现为两级映射:

  1. 令牌转换为文档 ID
  2. 文档 ID 到基表主键

这种方案可显著节省存储空间,因为 Spanner 无需为每个 <token, document> 对存储完整的基表主键。

实现这两级映射的物理索引有两种类型:

  1. 二级索引,用于将分区键和 docid 映射到基表主键。在上一部分的示例中,这会将 {SingerId, ReleaseTimestamp, uid} 映射到 {AlbumId}。辅助索引还会存储 CREATE SEARCH INDEXSTORING 子句中指定的所有列。
  2. 令牌索引,用于将令牌映射到文档 ID,类似于信息检索文献中的倒排索引。Spanner 会为搜索索引的每个 TOKENLIST 维护一个单独的令牌索引。从逻辑上讲,令牌索引会维护每个分区中每个令牌的 docid 列表(在信息检索中称为postings 列表)。列表按令牌排序以便快速检索,在列表中,docid 用于排序。各个令牌索引是通过 Spanner API 未公开的实现细节。

Spanner 支持以下四种 docid 选项。

搜索索引 Docid 行为
搜索索引省略了 ORDER BY 子句 {uid} Spanner 会添加一个隐藏的唯一值 (UID) 来标识每行。
ORDER BY column {column, uid} Spanner 会添加 UID 列,以便在分区中具有相同 column 值的行之间进行决胜。
ORDER BY column ... OPTIONS (disable_automatic_uid_column=true) {column} 未添加 UID 列。column 值在分区内必须是唯一的。
ORDER BY column1, column2 ... OPTIONS (disable_automatic_uid_column=true) {column1, column2} 未添加 UID 列。column1column2 值的组合在分区中必须是唯一的。

使用说明:

  • 内部 UID 列不会通过 Spanner API 公开。
  • 在未添加 UID 的索引中,添加具有现有(分区、排序顺序)的行的事务会失败。

例如,请考虑以下数据:

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 美好的日子
a2 1 743 美丽的眼睛

假设预排序列按升序排列,则按 SingerId 分区的令牌索引的内容会按以下方式分区:

SingerId _token ReleaseTimestamp uid
1 美丽 743 uid1
1 美丽 997 uid2
1 743 uid1
1 眼睛 997 uid2

搜索索引分片

当 Spanner 拆分表时,它会分发搜索索引数据,以便特定基表行中的所有令牌都位于同一分块中。换句话说,搜索索引是按文档进行分片的。这种分片策略对性能有重大影响:

  1. 无论令牌数量或编入索引的 TOKENLIST 列数量如何,每个事务与之通信的服务器数量都保持不变。
  2. 涉及多个条件表达式的搜索查询会在每个分块上独立执行,从而避免与分布式联接相关的性能开销。

搜索索引有两种分发模式:

  • 均匀分片(默认)。在均匀分片中,每个基表行的编入索引的数据会随机分配到分区的索引分块。
  • 排序顺序分片。在排序分片中,每个基表行的相关数据会根据 ORDER BY 列分配到分区的索引分块。例如,在降序排序的情况下,排序值最高的所有行都会显示在分区的第一个索引分块中,排序值次高的一组排序值会显示在下一个分块中。

这些分片模式在热点风险和查询开销之间存在权衡:

  • 当索引按时间戳排序时,排序分片搜索索引容易出现热点问题。如需了解详情,请参阅选择主键以防止出现热点。另一方面,当一系列文档的写入负载增加时,均匀分片可确保增加量均匀分布在各个分片中。
  • 标准的基于负载的拆分会创建额外的拆分,从而提供充分的保护来防范热点。均匀分片的缺点是,它可能会为某些类型的查询使用更多资源。

搜索索引的分片模式使用 OPTIONS 子句进行配置:

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC
OPTIONS (sort_order_sharding = true);

设置 sort_order_sharding=false 或将其留空时,系统会使用均匀分片创建搜索索引。

交错搜索索引

与二级索引一样,您可以在基表的父表中交错搜索索引。使用交错搜索索引的主要原因是为小分区将基表数据与索引数据共置。这种机会共存具有以下优势:

  • 写入操作无需执行两阶段提交
  • 搜索索引与基表之间的回联不会分布。

交错搜索索引存在以下限制:

  1. 只有排序分片索引可以交错。
  2. 搜索索引只能交错在顶级表中(不能在子表中)。
  3. 与交错表和二级索引一样,将父表的键设为交错搜索索引中 PARTITION BY 列的前缀。

定义交错搜索索引

以下示例演示了如何定义交错搜索索引:

CREATE TABLE Singers (
  SingerId INT64 NOT NULL
) PRIMARY KEY(SingerId);

CREATE TABLE Albums (
  SingerId INT64 NOT NULL,
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN
) PRIMARY KEY(SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
PARTITION BY SingerId,
INTERLEAVE IN Singers
OPTIONS (sort_order_sharding = true);

搜索索引排序顺序

搜索索引排序规则定义的要求与二级索引的要求不同。

例如,请参考下表:

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumName STRING(MAX),
  AlbumName_Token TOKENLIST AS (TOKEN(AlbumName)) HIDDEN
) PRIMARY KEY(AlbumId);

应用可以定义一个二级索引,以便使用按 ReleaseTimestamp 排序的 AlbumName 查找信息:

CREATE INDEX AlbumsSecondaryIndex ON Albums(AlbumName, ReleaseTimestamp DESC);

等效的搜索索引如下所示(由于二级索引不支持全文搜索,因此此索引使用完全匹配的令牌化):

CREATE SEARCH INDEX AlbumsSearchIndex
ON Albums(AlbumName_Token)
ORDER BY ReleaseTimestamp DESC;

搜索索引排序顺序必须符合以下要求:

  1. 仅将 INT64 列用于搜索索引的排序顺序。大小任意的列会在搜索索引中使用过多资源,因为 Spanner 需要在每个令牌旁边存储一个 docid。具体而言,排序列无法使用 TIMESTAMP 类型,因为 TIMESTAMP 使用纳秒精度,而纳秒精度无法放入 64 位整数中。
  2. 排序列不得为 NULL。您可以通过以下两种方式满足此要求:

    1. 将排序顺序列声明为 NOT NULL
    2. 将索引配置为排除 NULL 值

时间戳通常用于确定排序顺序。常见做法是使用自 Unix 纪元以来的微秒数来表示此类时间戳。

应用通常会先使用按降序排序的搜索索引检索最新的数据。

已过滤 NULL 的搜索索引

搜索索引可以使用 WHERE column IS NOT NULL 语法来排除基表行。NULL 过滤可应用于分区键、排序列和存储列。不允许对存储的数组列进行 NULL 过滤。

示例

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (Genre)
WHERE Genre IS NOT NULL

查询必须在 WHERE 子句中指定 NULL 过滤条件(本例中为 Genre IS NOT NULL)。否则,查询优化器将无法使用搜索索引。如需了解详情,请参阅 SQL 查询要求

对生成的列使用 NULL 过滤,以便根据任何任意条件排除行。如需了解详情,请参阅使用生成的列创建部分索引

后续步骤