検索インデックス

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

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

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

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

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

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

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

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

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

  • 部分文字列検索。長いテキストの本文内で短い文字列(部分文字列)を検索するクエリの一種です。
  • インデックスに登録されたデータのサブセットの条件を 1 つのインデックス スキャンに一本化する。

検索インデックスは、数値や完全一致文字列など、テキスト以外のデータのインデックス登録をサポートしていますが、検索インデックスの最も一般的なユースケースは、ドキュメント内のテキストのインデックス登録です。

検索インデックスの例

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

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

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

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

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

たとえば、次のステートメントでは、ベーステーブルは Albums です。TOKENLIST 列は AlbumTitleAlbumTitle_Tokens)と RatingRating_Tokens)に作成されます。

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

詳細については、CREATE SEARCH INDEX リファレンスをご覧ください。

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

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

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

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

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

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

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

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

検索インデックス Docid 動作
検索インデックスでは ORDER BY 句が省略されています {uid} Spanner は、各行を識別するために非表示の一意の値(UID)を追加します。
ORDER BY column {column, uid} Spanner は、パーティション内の同じ column 値を持つ行の間でタイブレーカーとして UID 列を追加します。
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 列は追加されません。column1 値と column2 値の組み合わせは、パーティション内で一意である必要があります。

使用上の注意:

  • 内部 UID 列は Spanner API では公開されません。
  • UID が追加されていないインデックスでは、既存のパーティション、並べ替え順序を持つ行を追加するトランザクションは失敗します。

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

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 番目に高いグループは次の分割に表示されます。

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

  • インデックスがタイムスタンプで並べ替えられている場合、並べ替え順序がシャーディングされた検索インデックスはホットスポット化が発生しやすくなります。詳細については、ホットスポットを防ぐ主キーの選択方法をご覧ください。一方、一連のドキュメントで書き込み負荷が増加すると、均一なシャーディングにより、増加した負荷はシャード間で均等に分散されます。
  • 標準の負荷ベースの分割では、ホットスポット化に対して適切な保護を提供する追加のスプリットが作成されます。均一なシャーディングのデメリットは、一部のクエリでより多くのリソースを使用する可能性があることです。

検索インデックスのシャーディング モードは、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 が設定されている、あるいは未指定の場合、検索インデックスは均一なシャーディングを使用して作成されます。

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

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

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

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

  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 では 64 ビット整数には収まらないナノ秒の精度が使用されるため、並べ替え順序の列で TIMESTAMP 型を使用することはできません。
  2. 並べ替え順序の列には、NULL を使用しない。この要件を満たすには、次の 2 つの方法があります。

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

次のステップ