搜尋索引

本頁面說明如何新增及使用搜尋索引。全文搜尋會針對搜尋索引中的項目執行。

如何使用搜尋索引

您可以為要進行全文搜尋的任何資料欄建立搜尋索引。如要建立搜尋索引,請使用 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 權杖 (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')

為具名結構定義建立及查詢搜尋索引

您可以在具名結構定義上建立搜尋索引,如以下範例所示。

GoogleSQL

CREATE SCHEMA AlbumSchema;
CREATE TABLE AlbumSchema.Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) STORED HIDDEN,
) PRIMARY KEY(AlbumId);

CREATE SEARCH INDEX AlbumSchema.AlbumsIndex ON AlbumSchema.Albums(AlbumTitle_Tokens);

PostgreSQL

在 PostgreSQL 中,您無法使用完整名稱在具名結構定義上建立索引。

CREATE SCHEMA AlbumSchema;
CREATE TABLE AlbumSchema.albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) STORED HIDDEN,
  PRIMARY KEY(albumid)
);
CREATE SEARCH INDEX albumsindex ON AlbumSchema.albums(albumtitle_tokens);

以下範例說明如何查詢具名結構定義的搜尋索引:

GoogleSQL

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

以下範例使用查詢提示,指示查詢最佳化工具在執行查詢時,使用名為 AlbumsIndex 的索引。

SELECT AlbumId
FROM AlbumSchema.Albums@{FORCE_INDEX="AlbumSchema.AlbumsIndex"}
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")

PostgreSQL

在 PostgreSQL 中,您無法使用完整名稱在具名結構定義上建立索引。

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

以下範例使用查詢提示,指示查詢最佳化工具在執行查詢時,使用名為 albumsindex 的索引。

SELECT albumid
FROM AlbumSchema.albums /*@ FORCE_INDEX = albumsindex */
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 陳述式,透過 AlbumTitle 和 Rating 的權杖建立搜尋索引:

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. 詞元對應至文件 ID
  2. Docid 對應至基礎資料表主鍵

由於 Spanner 不需要為每個 <token, document> 配對儲存完整的基礎資料表主鍵,因此這個配置可大幅節省儲存空間。

實體索引分為兩種類型,分別實作兩個層級的對應:

  1. 次要索引:將分區鍵和 docid 對應至基礎資料表主鍵。在上一節的範例中,這會將 {SingerId, ReleaseTimestamp, uid} 對應至 {AlbumId}。次要索引也會儲存 CREATE SEARCH INDEX 的 STORING 子句中指定的所有資料欄。
  2. 權杖索引:將權杖對應至 docid,類似資訊檢索文獻中的反向索引。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 分割資料表時,會分配搜尋索引資料,讓特定基礎資料表資料列中的所有權杖都位於同一個分割區。換句話說,搜尋索引是以文件為單位進行 shard。這種資料分割策略對效能有重大影響:

  1. 無論權杖數量或已建立索引的 TOKENLIST 欄數量為何,每筆交易通訊的伺服器數量都維持不變。
  2. 涉及多個條件運算式的搜尋查詢會在每個分割區上獨立執行,避免與分散式彙整相關的效能負擔。

搜尋索引有兩種分布模式:

  • 統一資料分割 (預設)。在統一 shard 模式中,每個基礎資料表資料列的索引資料,都會隨機指派給分區的索引分割。
  • 排序順序資料分割。在排序順序 sharding 中,每個基礎資料表資料列的資料,都會根據 ORDER BY 欄 (即預先排序欄) 指派給分區的索引分割。舉例來說,如果排列順序為遞減,則所有具有最大排列順序值的資料列,都會顯示在分區的第一個索引分割區,而次大的排列順序值群組則會顯示在下一個分割區。

這些 sharding 模式會在熱點風險和查詢成本之間做出取捨:

  • 如果搜尋索引的讀取或寫入模式可能導致熱點,建議使用統一 shard 搜尋索引。統一資料分割會將讀取和寫入負載平均分配到各個分割,藉此減少熱點,但這可能會增加查詢執行期間的資源用量。在統一 shard 搜尋索引中,由於資料隨機分布,查詢必須讀取分區內的所有分割。存取平均分 shard 的索引時,Spanner 會平行讀取所有分割,縮短整體查詢延遲時間。
  • 如果讀取或寫入模式不太可能造成熱點,建議使用排列順序 shard 搜尋索引。如果查詢的 ORDER BY 與索引的 ORDER BY 相符,且指定的 LIMIT 較低,這種做法可以降低查詢成本。執行這類查詢時,Spanner 會從分區的第一個分割區開始遞增讀取,如果能提早滿足 LIMIT,查詢就能在不讀取所有分割區的情況下完成。
  • 搜尋索引的 sharding 模式是使用 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 或未指定,系統會使用統一資料分割建立搜尋索引。

交錯搜尋索引

與次要索引一樣,您可以在基礎資料表的父項資料表中交錯搜尋索引。使用交錯搜尋索引的主要原因,是將基礎資料表資料與索引資料放在同一個位置,以便處理小型分區。這種機會主義式的共置方式有以下優點:

  • 寫入作業不需要執行兩階段修訂。
  • 搜尋索引與基礎資料表的反向 join 不會分散。

交錯搜尋索引有下列限制:

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

應用程式可能會定義次要索引,使用 AlbumName 查詢資訊,並依 ReleaseTimestamp 排序:

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. 將索引設為排除空值。

時間戳記通常用於決定排列順序。常見做法是使用自 Unix 紀元起算的微秒數做為這類時間戳記。

應用程式通常會使用遞減排序的搜尋索引,先擷取最新資料。

由於排列順序欄是基礎次要索引和權杖索引中 docid 鍵的一部分,因此只要基礎資料表中的值發生變更,Spanner 就必須重寫對應的次要索引資料列和所有相關聯的權杖索引資料列,這會產生寫入負擔。如果排列順序值經常變更,請考慮從 ORDER BY 中省略該資料欄,或使用精細程度較低的值。舉例來說,如果 UpdateTime 資料欄每隔幾秒就會更新,您可以使用產生資料欄,將時間戳記分組到小時,例如 UpdateTimeHours INT64 AS (UNIX_SECONDS(TIMESTAMP_TRUNC(UpdateTime, HOUR))) STORED (或 PostgreSQL 中的 updatetimehours bigint GENERATED ALWAYS AS (EXTRACT(EPOCH FROM updatetime - '1970-01-01Z'::timestamptz)::bigint / 3600) STORED)。

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 查詢規定」。

在索引定義中使用 WHERE <column> IS NOT NULL 篩選器,可確保只為非空值建立索引。這樣一來,即使查詢的 WHERE 子句中未明確包含 IS NOT NULL 條件,查詢最佳化工具也能有效使用索引來處理相關查詢。

在產生的資料欄上使用 NULL 篩選,即可根據任何條件排除資料列。詳情請參閱「使用產生的資料欄建立部分索引」。

後續步驟