Rechercher dans les index

Cette page explique comment ajouter des index de recherche. La recherche en texte intégral est exécutée sur les entrées de l'index de recherche.

Utiliser les index de recherche

Vous pouvez créer un index de recherche pour toutes les colonnes que vous souhaitez mettre à la disposition des recherches en texte intégral. Pour créer un index de recherche, utilisez l'instruction LDD CREATE SEARCH INDEX. Pour mettre à jour un indice, utilisez l'instruction LDD ALTER SEARCH INDEX. Spanner crée et gère automatiquement l'index de recherche, y compris en ajoutant et en mettant à jour les données dans l'index de recherche dès qu'elles changent dans la base de données.

Partitions de l'index de recherche

Un indice de recherche peut être partitionné ou non partitionné, en fonction du type de requêtes que vous souhaitez accélérer.

  • Un exemple de cas où un index partitionné est le meilleur choix est lorsque l'application interroge une boîte de réception de messagerie. Chaque requête est limitée à une boîte de réception spécifique.

  • Une requête non partitionnée est le meilleur choix, par exemple, lorsqu'une requête s'applique à toutes les catégories de produits d'un catalogue de produits.

Cas d'utilisation de l'index de recherche

En plus de la recherche en texte intégral, les index de recherche Spanner sont compatibles avec les éléments suivants:

  • Recherches de sous-chaînes, qui sont un type de requête qui recherche une chaîne plus courte (la sous-chaîne) dans un corps de texte plus volumineux.
  • Combiner des conditions sur n'importe quel sous-ensemble de données indexées dans une seule analyse d'index.

Bien que les index de recherche acceptent l'indexation de données non textuelles, telles que des nombres et des chaînes de correspondance exacte, le cas d'utilisation le plus courant d'un index de recherche consiste à indexer le texte d'un document.

Exemple d'index de recherche

Pour illustrer les fonctionnalités des index de recherche, supposons qu'une table stocke des informations sur des albums musicaux:

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

Spanner dispose de plusieurs fonctions de tokenisation qui créent des jetons. Pour modifier le tableau précédent afin de permettre aux utilisateurs d'effectuer une recherche en texte intégral pour trouver des titres d'albums, utilisez la fonction TOKENIZE_FULLTEXT pour créer des jetons à partir des titres d'albums. Créez ensuite une colonne qui utilise le type de données TOKENLIST pour contenir la sortie de tokenisation de TOKENIZE_FULLTEXT. Pour cet exemple, nous allons créer la colonne AlbumTitle_Tokens.

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

L'exemple suivant utilise la LDD CREATE SEARCH INDEX pour créer un indice de recherche (AlbumsIndex) sur les jetons AlbumTitle (AlbumTitle_Tokens):

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

Après avoir ajouté l'index de recherche, utilisez des requêtes SQL pour rechercher les albums correspondant aux critères de recherche. Exemple :

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

Cohérence des données

Lorsqu'un indice est créé, Spanner utilise des processus automatisés pour remplir les données afin de garantir la cohérence. Lorsque les écritures sont validées, les index sont mis à jour dans la même transaction. Spanner effectue automatiquement des vérifications de cohérence des données.

Définitions de schémas d'index de recherche

Les index de recherche sont définis sur une ou plusieurs colonnes TOKENLIST d'une table. Les index de recherche se composent des éléments suivants:

  • Table de base: table Spanner à indexer.
  • Colonne TOKENLIST: ensemble de colonnes qui définissent les jetons à indexer. L'ordre de ces colonnes n'a pas d'importance.

Par exemple, dans l'instruction suivante, la table de base est Albums. Les colonnes TOKENLIST sont créées sur AlbumTitle (AlbumTitle_Tokens) et Rating (Rating_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);

Utilisez l'instruction CREATE SEARCH INDEX suivante pour créer un index de recherche à l'aide des jetons pour AlbumTitle et Rating:

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

Les index de recherche proposent les options suivantes:

  • Partitions: groupe facultatif de colonnes qui divise l'index de recherche. Interroger un indice partitionné est souvent beaucoup plus efficace que d'interroger un indice non partitionné. Pour en savoir plus, consultez la section Partitionner les index de recherche.
  • Colonne d'ordre de tri: colonne INT64 facultative qui établit l'ordre de récupération à partir de l'index de recherche. Pour en savoir plus, consultez la section Ordre de tri de l'index de recherche.
  • Entrelacement: comme les index secondaires, vous pouvez entrelacer des index de recherche. Les index de recherche intercalés utilisent moins de ressources pour écrire et joindre la table de base. Pour en savoir plus, consultez Index de recherche entrelacé.
  • Clause "Options": liste de paires clé-valeur qui remplace les paramètres par défaut de l'index de recherche.

Pour en savoir plus, consultez la référence CREATE SEARCH INDEX.

Mise en page interne des index de recherche

Un élément important de la représentation interne des index de recherche est un docid, qui sert de représentation efficace du stockage de la clé primaire de la table de base, qui peut être arbitrairement longue. C'est également ce qui crée l'ordre de la mise en page des données internes en fonction des colonnes ORDER BY fournies par l'utilisateur de la clause CREATE SEARCH INDEX. Il est représenté par un ou deux entiers de 64 bits.

Les index de recherche sont implémentés en interne sous la forme d'un mappage à deux niveaux:

  1. Jetons vers des docids
  2. Docids vers les clés primaires de la table de base

Ce schéma permet de réaliser des économies de stockage importantes, car Spanner n'a pas besoin de stocker la clé primaire complète de la table de base pour chaque paire <token, document>.

Il existe deux types d'index physiques qui implémentent les deux niveaux de mappage:

  1. Un indice secondaire qui mappe les clés de partition et un docid sur la clé primaire de la table de base. Dans l'exemple de la section précédente, cela mappe {SingerId, ReleaseTimestamp, uid} sur {AlbumId}. L'index secondaire stocke également toutes les colonnes spécifiées dans la clause STORING de CREATE SEARCH INDEX.
  2. Un index de jetons qui met en correspondance des jetons avec des docids, semblable aux index inversés dans la littérature sur la récupération d'informations. Spanner gère un index de jetons distinct pour chaque TOKENLIST de l'index de recherche. Logiquement, les index de jetons gèrent des listes de docids pour chaque jeton dans chaque partition (appelées listes d'annonces dans la recherche d'informations). Les listes sont triées par jetons pour une récupération rapide. Dans les listes, le docid est utilisé pour l'ordre. Les index de jeton individuels sont un détail d'implémentation non exposé via les API Spanner.

Spanner accepte les quatre options suivantes pour le docid.

index de recherche DocID Comportement
La clause ORDER BY est omise pour l'index de recherche {uid} Spanner ajoute une valeur unique (UID) masquée pour identifier chaque ligne.
ORDER BY column {column, uid} Spanner ajoute la colonne UID comme critère de départage entre les lignes ayant les mêmes valeurs column dans une partition.
ORDER BY column ... OPTIONS (disable_automatic_uid_column=true) {column} La colonne UID n'est pas ajoutée. Les valeurs column doivent être uniques dans une partition.
ORDER BY column1, column2 ... OPTIONS (disable_automatic_uid_column=true) {column1, column2} La colonne UID n'est pas ajoutée. La combinaison des valeurs column1 et column2 doit être unique dans une partition.

Remarques concernant l'utilisation :

  • La colonne UID interne n'est pas exposée via l'API Spanner.
  • Dans les index où l'UID n'est pas ajouté, les transactions qui ajoutent une ligne avec un UID déjà existant (partition,ordre de tri) échouent.

Prenons l'exemple des données suivantes:

ID de l'album SingerId ReleaseTimestamp SongTitle
a1 1 997 Beaux jours
a2 1 743 Beaux yeux

En supposant que la colonne de prétriage soit triée par ordre croissant, le contenu de l'index de jetons partitionné par SingerId partitionne le contenu de l'index de jetons comme suit:

SingerId _token ReleaseTimestamp uid
1 beau 743 uid1
1 beau 997 uid2
1 jours 743 uid1
1 yeux 997 uid2

Division de l'index de recherche

Lorsque Spanner divise une table, il distribue les données de l'index de recherche de sorte que tous les jetons d'une ligne de table de base donnée se trouvent dans la même division. En d'autres termes, l'index de recherche est partitionné par document. Cette stratégie de partitionnement a des conséquences importantes sur les performances:

  1. Le nombre de serveurs avec lesquels chaque transaction communique reste constant, quel que soit le nombre de jetons ou le nombre de colonnes TOKENLIST indexées.
  2. Les requêtes de recherche impliquant plusieurs expressions conditionnelles sont exécutées indépendamment sur chaque fractionnement, ce qui évite les coûts liés aux performances associés à une jointure distribuée.

Les index de recherche comportent deux modes de distribution:

  • Segmentation uniforme (par défaut) Dans le sharding uniforme, les données indexées de chaque ligne de la table de base sont attribuées de manière aléatoire à une division d'index d'une partition.
  • Segmentation par ordre de tri. Dans le partitionnement par ordre de tri, les données de chaque ligne de la table de base sont attribuées à une division d'index d'une partition en fonction des colonnes ORDER BY. Par exemple, dans le cas d'un ordre de tri décroissant, toutes les lignes dont les valeurs d'ordre de tri sont les plus élevées apparaissent dans la première division d'index d'une partition, et le groupe de valeurs d'ordre de tri le plus élevé suivant dans la division suivante.

Ces modes de partitionnement impliquent un compromis entre les risques de hotspotting et le coût des requêtes:

  • Les index de recherche partitionnés par ordre de tri sont sujets aux points chauds lorsque l'index est trié par code temporel. Pour en savoir plus, consultez Choisir une clé primaire pour éviter les hotspots. En revanche, lorsque la charge d'écriture augmente sur une plage de documents, le sharding uniforme garantit que l'augmentation est répartie uniformément sur les segments.
  • La répartition basée sur la charge standard crée des divisions supplémentaires qui offrent une protection adéquate contre le hotspotting. L'inconvénient du sharding uniforme est qu'il peut utiliser plus de ressources pour certains types de requêtes.

Le mode de partitionnement d'un index de recherche est configuré à l'aide de la clause OPTIONS:

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

Lorsque sort_order_sharding=false est défini ou laissé non spécifié, l'index de recherche est créé à l'aide d'un fractionnement uniforme.

Index de recherche entrelacés

Comme les index secondaires, vous pouvez entrelacer des index de recherche dans une table parente de la table de base. La principale raison d'utiliser des index de recherche intercalés est de placer les données de la table de base à côté des données d'index pour les petites partitions. Cette colocation opportuniste présente les avantages suivants:

  • Les écritures n'ont pas besoin d'effectuer un commit en deux phases.
  • Les jointures arrière de l'index de recherche avec la table de base ne sont pas distribuées.

Les index de recherche intercalés sont soumis aux restrictions suivantes:

  1. Seuls les index shardés par ordre de tri peuvent être entrelacés.
  2. Les index de recherche ne peuvent être entrelacés que dans les tables racine (et non dans les tables enfants).
  3. Comme pour les tables entrelacées et les index secondaires, faites de la clé de la table parente un préfixe des colonnes PARTITION BY de l'index de recherche entrelacé.

Définir un index de recherche entrelacé

L'exemple suivant montre comment définir un indice de recherche entrelacé:

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

Ordre de tri de l'index de recherche

Les exigences concernant la définition de l'ordre de tri de l'index de recherche sont différentes de celles des index secondaires.

Prenons l'exemple du tableau suivant:

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

L'application peut définir un indice secondaire pour rechercher des informations à l'aide de AlbumName triées par ReleaseTimestamp:

CREATE INDEX AlbumsSecondaryIndex ON Albums(AlbumName, ReleaseTimestamp DESC);

L'index de recherche équivalent se présente comme suit (il utilise la tokenisation par correspondance exacte, car les index secondaires ne sont pas compatibles avec les recherches en texte intégral):

CREATE SEARCH INDEX AlbumsSearchIndex
ON Albums(AlbumName_Token)
ORDER BY ReleaseTimestamp DESC;

L'ordre de tri de l'index de recherche doit respecter les exigences suivantes:

  1. N'utilisez des colonnes INT64 que pour l'ordre de tri d'un indice de recherche. Les colonnes dont la taille est arbitraire utilisent trop de ressources dans l'index de recherche, car Spanner doit stocker un docid à côté de chaque jeton. Plus précisément, la colonne d'ordre de tri ne peut pas utiliser le type TIMESTAMP, car TIMESTAMP utilise une précision de nanoseconde qui ne tient pas dans un entier de 64 bits.
  2. Les colonnes d'ordre de tri ne doivent pas être NULL. Pour répondre à cette exigence, vous avez deux options:

    1. Déclarez la colonne d'ordre de tri comme NOT NULL.
    2. Configurez l'index pour exclure les valeurs NULL.

Un code temporel est souvent utilisé pour déterminer l'ordre de tri. Il est courant d'utiliser des microsecondes depuis l'époque Unix pour ces codes temporels.

Les applications récupèrent généralement d'abord les données les plus récentes à l'aide d'un indice de recherche trié par ordre décroissant.

Index de recherche filtré par NULL

Les index de recherche peuvent utiliser la syntaxe WHERE column IS NOT NULL pour exclure les lignes de la table de base. Le filtrage NULL peut s'appliquer aux clés de partitionnement, aux colonnes d'ordre de tri et aux colonnes stockées. Le filtrage NULL sur les colonnes de tableau stockées n'est pas autorisé.

Exemple

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

La requête doit spécifier la condition de filtrage NULL (Genre IS NOT NULL pour cet exemple) dans la clause WHERE. Sinon, l'optimiseur de requête ne peut pas utiliser l'index de recherche. Pour en savoir plus, consultez la section Exigences concernant les requêtes SQL.

Utilisez le filtrage NULL sur une colonne générée pour exclure des lignes en fonction de critères arbitraires. Pour en savoir plus, consultez la section Créer un indice partiel à l'aide d'une colonne générée.

Étape suivante