搜索索引

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

如何使用搜索索引

您可以针对要用于全文搜索的任何列创建搜索索引。如需创建搜索索引,请使用 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 有多个用于创建令牌的词元化函数。如需修改上表,以便用户运行全文搜索来查找专辑名称,请使用 TOKENIZE_FULLTEXT 函数根据专辑名称创建令牌。然后,创建一个使用 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:定义需要编入索引的令牌的列集合。这些列的顺序并不重要。

例如,在以下语句中,基本表是 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. 词法单元到 docid 的映射
  2. 从 docid 到基本表主键

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

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

  1. 一种二级索引,用于将分区键和 docid 映射到基本表主键。在上一部分中的示例中,此代码将 {SingerId, ReleaseTimestamp, uid} 映射到 {AlbumId}。二级索引还会存储 CREATE SEARCH INDEXSTORING 子句中指定的所有列。
  2. 一种将 token 映射到 docid 的 token 索引,类似于信息检索文献中的反向索引。Spanner 会为搜索索引的每个 TOKENLIST 维护一个单独的令牌索引。从逻辑上讲,令牌索引会维护每个分区中每个令牌的 docid 列表(在信息检索中称为倒排列表)。列表按令牌排序,以便快速检索,而在列表中,则使用 docid 进行排序。 各个令牌索引是不会通过 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 美好的一天
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 列(即预排序列)分配给分区的索引拆分。例如,如果采用降序排序,则具有最大排序顺序值的所有行会出现在分区的第一个索引拆分中,而具有次大排序顺序值的所有行会出现在下一个拆分中。

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

  • 如果对搜索索引的读取或写入模式可能会导致热点,建议使用统一分片搜索索引。均匀分片通过在各个分片之间均匀分配读取和写入负载来缓解热点问题,但作为一种权衡,这可能会增加查询执行期间的资源使用量。在均匀分片的搜索索引中,由于数据是随机分布的,因此查询必须读取分区中的所有拆分。在访问均匀分片的索引时,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 过滤功能,以根据任意条件排除行。如需了解详情,请参阅使用生成的列创建部分索引

后续步骤