搜索索引

本页面介绍了如何添加和使用搜索索引。全文搜索会针对搜索索引中的条目运行。

如何使用搜索索引

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

搜索索引分区

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

  • 分区索引的最佳应用场景示例是应用查询邮箱。每个查询都仅限于特定邮箱。

  • 非分区索引的最佳应用场景示例是查询涵盖产品目录中的所有产品类别。

搜索索引应用场景

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

  • JSON 搜索,这是一种对 JSON 和 JSONB 文档进行索引和查询的高效方式。
  • 子字符串搜索,这是一种在较长的文本正文中查找较短字符串(子字符串)的查询。
  • 将针对任何索引数据子集的条件(包括完全匹配和数值)合并到单个索引扫描中。

如需详细了解应用场景,请参阅搜索与二级索引

搜索索引示例

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

GoogleSQL

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

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
PRIMARY KEY(albumid));

Spanner 具有多个用于创建 token 的词元化函数。如需修改上表以便用户可以运行全文搜索来查找专辑标题,请使用 TOKENIZE_FULLTEXT 函数根据专辑标题创建 token。然后,创建一个列以使用 TOKENLIST 数据类型保存来自 TOKENIZE_FULLTEXT 的词元化输出。在此示例中,我们会创建 AlbumTitle_Tokens 列。

GoogleSQL

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

PostgreSQL

ALTER TABLE albums
  ADD COLUMN albumtitle_tokens spanner.tokenlist
    GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN;

以下示例使用 CREATE SEARCH INDEX DDL 对 AlbumTitle token (AlbumTitle_Tokens) 创建搜索索引 (AlbumsIndex):

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

PostgreSQL

此示例使用 CREATE SEARCH INDEX

CREATE SEARCH INDEX albumsindex ON albums(albumtitle_tokens);

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

GoogleSQL

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

PostgreSQL

SELECT albumid
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')

数据一致性

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

搜索索引架构定义

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

  • 基表:需要编入索引的 Spanner 表。
  • TOKENLIST:定义需要编入索引的 token 的列的集合。这些列的顺序并不重要。

例如,在以下语句中,基表是 Albums。TOKENLIST 列是在 AlbumTitle (AlbumTitle_Tokens) 和 Rating (Rating_Tokens) 上创建的。

GoogleSQL

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

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  singerid bigint NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumtitle character varying,
  rating double precision,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  rating_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(rating)) VIRTUAL HIDDEN,
PRIMARY KEY(AlbumId));

使用以下 CREATE SEARCH INDEX 语句,通过 AlbumTitleRating 的 token 创建搜索索引:

GoogleSQL

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

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC

搜索索引具有以下选项:

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

搜索索引的内部布局

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

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

  1. token 到 docid 的映射
  2. docid 到基表主键的映射

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

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

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

Spanner 支持以下四个适用于 docid 的选项。

搜索索引 Docid 行为
对搜索索引省略 ORDER BY 子句 {uid} Spanner 会添加一个隐藏的唯一值 (UID) 来标识每一行。
ORDER BY column {column, uid} Spanner 会添加 UID 列,以作为分区内具有相同 column 值的行之间的裁定项。

使用说明:

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

例如,请考虑以下数据:

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 Beautiful days
a2 1 743 Beautiful eyes

假设预排序列按升序排列,则按 SingerId 进行分区的 token 索引的内容会通过以下方式对 token 索引的内容进行分区:

SingerId _token ReleaseTimestamp uid
1 beautiful 743 uid1
1 beautiful 997 uid2
1 743 uid1
1 eyes 997 uid2

搜索索引分片

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

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

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

  • 均匀分片(默认)。在均匀分片中,每个基表行编入索引的数据都会随机分配给分区的某个索引分块。
  • 排列顺序分片。在排列顺序分片中,每个基表行的数据都会根据 ORDER BY 列(即预排序列)分配给分区的某个索引分块。例如,如果采用降序排列顺序,则具有最大排列顺序值的所有行都会出现在分区的第一个索引分块中,而具有第二大排列顺序值的行组会出现在下一个分块中。

这些分片模式需要在热点风险和查询成本之间进行权衡:

  • 如果对搜索索引的读取或写入模式可能会导致热点,建议使用均匀分片搜索索引。均匀分片通过在各个分块之间均匀分布读取和写入负载来缓解热点问题,但作为权衡,这可能会增加查询执行期间的资源使用量。在均匀分片搜索索引中,由于数据是随机分布的,因此查询必须读取分区中的所有分块。在访问均匀分片索引时,Spanner 会并行读取所有分块,以缩短总体查询延迟时间。
  • 如果读取或写入模式不太可能导致热点,则最好使用排列顺序分片搜索索引。对于其 ORDER BY 与索引的 ORDER BY 相匹配,且指定了相对较低的 LIMIT 的查询,此方法可以降低其成本。执行此类查询时,Spanner 会从分区的第一个分块开始逐步读取,如果可以提前满足 LIMIT,则查询可以在不读取所有分块的情况下完成。
  • 搜索索引的分片模式使用 OPTIONS 子句进行配置。

GoogleSQL

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

PostgreSQL

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

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC
WITH (sort_order_sharding = true);

如果设置了 sort_order_sharding=false 或未指定,则使用均匀分片创建搜索索引。

交织搜索索引

与二级索引类似,您可以在基表的父表中交织搜索索引。使用交织搜索索引的主要原因是,对于小分区,将基表数据与索引数据放置在同一位置。这种机会性共置具有以下优势:

  • 写入操作不需要执行两阶段提交
  • 搜索索引与基表的反向联接不会进行分布。

交织搜索索引具有以下限制:

  1. 只有排列顺序分片索引才能进行交织。
  2. 搜索索引只能在顶级表中(而不能在子表中)进行交织。
  3. 与交织表和二级索引一样,将父表的键作为交织搜索索引中的 PARTITION BY 列的前缀。

定义交织搜索索引

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

GoogleSQL

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

PostgreSQL

CREATE TABLE singers(
  singerid bigint NOT NULL
PRIMARY KEY(singerid));

CREATE TABLE albums(
  singerid bigint NOT NULL,
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist
  GENERATED ALWAYS
AS (
  spanner.tokenize_fulltext(albumtitle)
) VIRTUAL 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 WITH(sort_order_sharding = true);

搜索索引排列顺序

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

例如,请考虑下表:

GoogleSQL

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

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumname character varying,
  albumname_token spanner.tokenlist
      GENERATED ALWAYS AS(spanner.token(albumname)) VIRTUAL 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 需要在每个 token 旁存储一个 docid。具体来说,排列顺序列不能使用 TIMESTAMP 类型,因为 TIMESTAMP 使用的纳秒级精度超出了 64 位整数范围。
  2. 排列顺序列不得为 NULL。您可以通过以下两种方式满足此要求:

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

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

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

过滤掉 NULL 值的搜索索引

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

示例

GoogleSQL

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

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (genre)
WHERE genre IS NOT NULL

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

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

后续步骤