検索インデックス

このページでは、検索インデックスを追加して使用する方法について説明します。全文検索は、検索インデックス内のエントリに対して実行されます。

検索インデックスの使用方法

全文検索で使用できるようにする列に対して検索インデックスを作成できます。検索インデックスを作成するには、CREATE SEARCH INDEX DDL ステートメントを使用します。インデックスを更新するには、ALTER SEARCH INDEX DDL ステートメントを使用します。データベースのデータが変更されると、Spanner は検索インデックスのデータの追加や更新など、検索インデックスの自動的な構築と維持をすぐに行います。

検索インデックスのパーティション

検索インデックスは、高速化が必要なクエリのタイプに応じて、「パーティション分割」または「パーティション分割なし」にできます。

  • パーティション分割インデックスが最適な場合の例としては、アプリケーションがメールボックスをクエリするケースが挙げられます。クエリはそれぞれ特定のメールボックスに制限されます。

  • パーティション分割がないクエリが最適な場合の例としては、商品カタログ内のすべての商品カテゴリをクエリするケースが挙げられます。

検索インデックスのユースケース

Spanner の検索インデックスは、全文検索に加えて、次の機能をサポートしています。

  • JSON 検索: JSON ドキュメントと JSONB ドキュメントをインデックスに登録してクエリする効率的な方法です。
  • 部分文字列検索。長いテキストの本文内で短い文字列(部分文字列)を検索するクエリの一種です。
  • インデックスに登録されたデータのサブセット(完全一致や数値など)の条件を 1 つのインデックス スキャンにまとめることができます。

ユースケースの詳細については、検索インデックスとセカンダリ インデックスの比較をご覧ください。

検索インデックスの例

音楽アルバムに関する情報を格納するテーブルを例に、検索インデックスの機能について説明します。

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

データの整合性

インデックスが作成されると、Spanner は自動プロセスを使用してデータをバックフィルし、整合性を確保します。書き込みが commit されると、同じトランザクションでインデックスが更新されます。Spanner はデータの整合性チェックを自動的に実行します。

検索インデックスのスキーマ定義

検索インデックスは、テーブルの 1 つ以上の TOKENLIST 列に定義されます。検索インデックスには次のコンポーネントがあります。

  • ベーステーブル: インデックス登録が必要な Spanner テーブル。
  • TOKENLIST: インデックス登録が必要なトークンを定義する列のコレクション。これらの列の順序は重要ではありません。

たとえば、次のステートメントでは、ベーステーブルは Albums です。TOKENLIST 列は AlbumTitleAlbumTitle_Tokens)と RatingRating_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 のトークンを使用して検索インデックスを作成します。

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 列(省略可)。詳細については、検索インデックスの並べ替え順序をご覧ください。
  • インターリーブ: セカンダリ インデックスと同様に、検索インデックスをインターリーブできます。インターリーブされた検索インデックスを使用すると、ベーステーブルへの書き込みと結合に必要なリソースが少なくなります。詳細については、インターリーブされた検索インデックスをご覧ください。
  • オプション句: 検索インデックスのデフォルト設定をオーバーライドする Key-Value ペアのリスト。

検索インデックスの内部レイアウト

検索インデックスの内部表現の重要な要素は docid です。これは、任意の長さのベーステーブルの主キーを保存効率の良い形式で表したものです。また、CREATE SEARCH INDEX 句でユーザー指定の ORDER BY 列に従って、内部データ レイアウトの順序を作成します。これは、1 つまたは 2 つの 64 ビット整数で表されます。

検索インデックスは、内部的には次の 2 つのレベルのマッピングとして実装されています。

  1. トークンから docid
  2. docid からベーステーブルの主キー

このスキームでは、Spanner が <token, document> のペアごとにベーステーブルの主キー全体を保存する必要がないため、ストレージが大幅に節約されます。

2 つのレベルのマッピングを実装する物理インデックスには次の 2 種類があります。

  1. セカンダリ インデックス。ベーステーブルの主キーにパーティション キーと docid をマッピングします。前のセクションの例では、{SingerId, ReleaseTimestamp, uid}{AlbumId} にマッピングされます。セカンダリ インデックスには、CREATE SEARCH INDEXSTORING 句で指定された列もすべて保存されます。
  2. トークン インデックス。トークンを docid にマッピングします。これは、情報検索の「逆索引」に似ています。Spanner は、検索インデックスの TOKENLIST ごとに個別のトークン インデックスを保持します。論理的には、トークン インデックスは各パーティション内の各トークンの docid のリストを保持します(情報検索では「ポスティング リスト」と呼ばれます)。このリストはトークン順に並べ替えられ、迅速な取得が可能です。リスト内では docid が並べ替えに使用されます。個々のトークン インデックスは、Spanner API では公開されない実装の詳細です。

Spanner は、docid に対して次の 4 つのオプションをサポートします。

検索インデックス Docid 動作
検索インデックスでは ORDER BY 句が省略されています。 {uid} Spanner は、各行を識別するために非表示の一意の値(UID)を追加します。
ORDER BY column {column, uid} Spanner は、パーティション内の同じ column 値を持つ行の間でタイブレーカーとして UID 列を追加します。

使用上の注意:

  • 内部 UID 列は Spanner API では公開されません。
  • UID が追加されていないインデックスでは、(partition,sort order) がすでに存在する行を追加するトランザクションは失敗します。

たとえば、次のデータで考えてみましょう。

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

並べ替え前の列が昇順であることを前提とすると、SingerId でパーティション分割されたトークン インデックスのコンテンツは、次のようにトークン インデックスのコンテンツをパーティション分割します。

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

検索インデックスのシャーディング

Spanner はテーブルを分割するときに、特定のベーステーブル行のすべてのトークンが同じスプリットに含まれるように検索インデックスのデータを分散します。つまり、検索インデックスはドキュメントにシャーディングされます。このシャーディング戦略は、パフォーマンスに大きな影響を与えます。

  1. 各トランザクションが通信するサーバーの数は、トークンの数やインデックス付きの TOKENLIST 列の数に関係なく一定です。
  2. 複数の条件式を含む検索クエリは、各スプリットで独立して実行されるため、分散結合に関連するパフォーマンスのオーバーヘッドを回避できます。

検索インデックスには、次の 2 つの分散モードがあります。

  • 均一なシャーディング(デフォルト)。均一なシャーディングでは、各ベーステーブル行のインデックス データが、パーティションのインデックス分割にランダムに割り当てられます。
  • 並べ替え順序シャーディング。並べ替え順序シャーディングでは、各ベーステーブル行のデータが、ORDER BY 列(並べ替え前の列)に基づいてパーティションのインデックス分割に割り当てられます。たとえば、並べ替え順序が降順の場合、並べ替え順序の値が最も高い行はすべてパーティションの最初のインデックス分割に表示され、並べ替え順序の値が 2 番目に高いグループは次の分割に表示されます。

これらのシャーディング モードには、ホットスポット化のリスクとクエリコストのトレードオフが伴います。

  • 検索インデックスへの読み取りまたは書き込みパターンがホットスポットにつながる可能性がある場合は、均一にシャーディングされた検索インデックスをおすすめします。均一なシャーディングでは、読み取りと書き込みの負荷をスプリット間で均等に分散することでホットスポットを軽減しますが、そのトレードオフとして、クエリ実行中のリソース使用量が増加する可能性があります。均一にシャーディングされた検索インデックスでは、データがランダムに分散されるため、クエリはパーティション内のすべてのスプリットを読み取る必要があります。均一にシャーディングされたインデックスにアクセスすると、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 が設定されているか、未指定の場合、検索インデックスは均一なシャーディングを使用して作成されます。

インターリーブされた検索インデックス

セカンダリ インデックスと同様、検索インデックスをベーステーブルの親テーブルにインターリーブできます。インターリーブされた検索インデックスを使用する主な理由は、ベーステーブル データを小さなパーティションのインデックス データと同じ場所に配置(コロケーション)することです。この日和見的なコロケーションには次のような利点があります。

  • 書き込みで 2 フェーズ commit を行う必要がない。
  • 検索インデックスとベーステーブルのバック結合が分散されない。

インターリーブされた検索インデックスには次の制限があります。

  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 がすべてのトークンに docid を保存する必要があるため、検索インデックスに非常に多くのリソースを使用します。具体的には、TIMESTAMP では 64 ビット整数には収まらないナノ秒の精度が使用されるため、並べ替え順序の列で TIMESTAMP 型を使用することはできません。
  2. 並べ替え順序の列には NULL を使用しない。この要件を満たすには、次の 2 つの方法があります。

    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 フィルタリングを使用して、任意の条件に基づいて行を除外します。詳細については、生成列を使用した部分的なインデックスの作成をご覧ください。

次のステップ