Trouver des voisins les plus proches approximatifs, créer des index vectoriels et interroger des embeddings vectoriels

Cette page explique comment trouver les plus proches voisins approximatifs (ANN), créer des index vectoriels et interroger des embeddings vectoriels à l'aide des fonctions de distance ANN suivantes dans Spanner :

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

Lorsqu'un ensemble de données est petit, vous pouvez utiliser K-nearest neighbors (KNN) pour trouver les k-vecteurs les plus proches exacts. Toutefois, à mesure que votre ensemble de données augmente, la latence et le coût d'une recherche KNN augmentent également. Vous pouvez utiliser ANN pour trouver les k-plus proches voisins approximatifs avec une latence et des coûts considérablement réduits.

K plus proches voisins approximatifs

Dans une recherche ANN, les k vecteurs renvoyés ne sont pas les k voisins les plus proches, car la recherche ANN calcule des distances approximatives et peut ne pas examiner tous les vecteurs de l'ensemble de données. Il arrive que quelques vecteurs qui ne figurent pas parmi les k-voisins les plus proches soient renvoyés. C'est ce qu'on appelle la perte de couverture. La perte de rappel acceptable dépend du cas d'utilisation, mais dans la plupart des cas, perdre un peu de rappel en échange d'une amélioration des performances de la base de données est un compromis acceptable.

Pour en savoir plus sur les fonctions de distance approximative Spanner, consultez les pages suivantes :

Index vectoriel

Spanner accélère les recherches vectorielles ANN en utilisant un index vectoriel spécialisé. Cet index s'appuie sur Scalable Nearest Neighbor (ScaNN) de Google Research, un algorithme de recherche du plus proche voisin très efficace.

L'index vectoriel utilise une structure arborescente pour partitionner les données et faciliter des recherches plus rapides. Spanner propose des configurations d'arborescence à deux et trois niveaux :

  • Configuration de l'arborescence à deux niveaux : les nœuds feuilles (num_leaves) contiennent des groupes de vecteurs étroitement liés ainsi que leur centroïde correspondant. Le niveau racine se compose des centroïdes de tous les nœuds feuilles.
  • Configuration d'arborescence à trois niveaux : semblable à une arborescence à deux niveaux, mais avec une couche de branche supplémentaire (num_branches) à partir de laquelle les centroïdes des nœuds feuilles sont partitionnés pour former le niveau racine (num_leaves).

Spanner choisit un index pour vous. Toutefois, si vous savez qu'un index spécifique fonctionne mieux, vous pouvez utiliser l'indication FORCE_INDEX pour choisir l'index vectoriel le plus adapté à votre cas d'utilisation.

Pour en savoir plus, consultez la section Instructions VECTOR INDEX.

Limites

Créer un index vectoriel

Pour optimiser le rappel et les performances d'un index vectoriel, nous vous recommandons de :

  • Créez votre index vectoriel une fois que la plupart des lignes avec des embeddings ont été écrites dans votre base de données. Vous devrez peut-être aussi reconstruire périodiquement l'index vectoriel après avoir inséré de nouvelles données. Pour en savoir plus, consultez Reconstruire l'index vectoriel.

  • Utilisez la clause STORING pour stocker une copie d'une colonne dans l'index vectoriel. Si une valeur de colonne est stockée dans l'index vectoriel, Spanner effectue le filtrage au niveau de la feuille de l'index pour améliorer les performances des requêtes. Nous vous recommandons de stocker une colonne si elle est utilisée dans une condition de filtrage. Pour en savoir plus sur l'utilisation de STORING dans un index, consultez Créer un index pour les analyses d'index uniquement.

Lorsque vous créez votre table, la colonne d'intégration doit être un tableau de type de données FLOAT32 (recommandé) ou FLOAT64, et comporter une annotation vector_length indiquant la dimension des vecteurs.

L'instruction LDD suivante crée une table Documents avec une colonne d'embedding DocEmbedding avec une longueur de vecteur :

CREATE TABLE Documents (
  DocId INT64 NOT NULL,
  ...
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

Une fois que vous avez rempli votre table Documents, vous pouvez créer un index vectoriel avec un arbre à deux niveaux et 1 000 nœuds feuilles sur une table Documents avec une colonne d'intégration DocEmbedding à l'aide de la distance cosinus :

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING (WordCount)
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

Pour créer un index vectoriel avec un arbre à trois niveaux et 1 000 000 de nœuds feuilles :

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 3, num_branches=1000, num_leaves = 1000000);

Si votre colonne d'intégration n'est pas marquée comme NOT NULL dans la définition de table, vous devez la déclarer avec une clause WHERE COLUMN_NAME IS NOT NULL :

CREATE VECTOR INDEX DocEmbeddingIndex
  ON Documents(NullableDocEmbedding)
  STORING (WordCount)
  WHERE NullableDocEmbedding IS NOT NULL
  OPTIONS (distance_type = 'COSINE', tree_depth = 2, num_leaves = 1000);

Filtrer un index vectoriel

Vous pouvez également créer un index vectoriel filtré pour trouver les éléments les plus similaires de votre base de données qui correspondent à la condition de filtre. Un index vectoriel filtré indexe de manière sélective les lignes qui répondent aux conditions de filtre spécifiées, ce qui améliore les performances de recherche.

Dans l'exemple suivant, la table Documents comporte une colonne appelée Category. Dans notre recherche vectorielle, nous souhaitons indexer la catégorie "Tech". Nous créons donc une colonne générée qui renvoie NULL si la condition de catégorie n'est pas remplie.

CREATE TABLE Documents (
  DocId INT64 NOT NULL,
  Category STRING(MAX),
  NullIfFiltered BOOL AS (IF(Category = 'Tech', TRUE, NULL)) HIDDEN,
  DocEmbedding ARRAY<FLOAT32>(vector_length=>128),
) PRIMARY KEY (DocId);

Ensuite, nous créons un index vectoriel avec un filtre. L'index vectoriel TechDocEmbeddingIndex n'indexe que les documents de la catégorie "Tech".

CREATE VECTOR INDEX TechDocEmbeddingIndex
  ON Documents(DocEmbedding)
  STORING(NullIfFiltered)
  WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
  OPTIONS (...);

Lorsque Spanner exécute la requête suivante, qui comporte des filtres correspondant à TechDocEmbeddingIndex, il sélectionne automatiquement TechDocEmbeddingIndex et l'accélère. La requête ne recherche que les documents de la catégorie "Tech". Vous pouvez également utiliser {@FORCE_INDEX=TechDocEmbeddingIndex} pour forcer Spanner à utiliser explicitement TechDocEmbeddingIndex.

SELECT *
FROM Documents
WHERE DocEmbedding IS NOT NULL AND NullIfFiltered IS NOT NULL
ORDER BY APPROX_(....)
LIMIT 10;

Interroger les embeddings vectoriels

Pour interroger un index vectoriel, utilisez l'une des trois fonctions de distance approximative :

  • APPROX_COSINE_DISTANCE
  • APPROX_EUCLIDEAN_DISTANCE
  • APPROX_DOT_PRODUCT

Les restrictions suivantes s'appliquent lorsque vous utilisez les fonctions de distance approximative :

  • La fonction de distance approximative doit calculer la distance entre une colonne d'embedding et une expression constante (par exemple, un paramètre ou un littéral).
  • La sortie de la fonction de distance approximative doit être utilisée dans une clause ORDER BY comme seule clé de tri, et un LIMIT doit être spécifié après le ORDER BY.
  • La requête doit filtrer explicitement les lignes qui ne sont pas indexées. Dans la plupart des cas, cela signifie que la requête doit inclure une clause WHERE <column_name> IS NOT NULL qui correspond à la définition de l'index vectoriel, sauf si la colonne est déjà marquée comme NOT NULL dans la définition de la table.

Pour obtenir la liste détaillée des limites, consultez la page de référence sur la fonction de distance approximative.

Exemples

Pour rechercher les 100 vecteurs les plus proches de [1.0, 2.0, 3.0] :

SELECT DocId
FROM Documents
WHERE WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], DocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

Si la colonne d'embedding peut avoir une valeur nulle :

SELECT DocId
FROM Documents
WHERE NullableDocEmbedding IS NOT NULL AND WordCount > 1000
ORDER BY APPROX_EUCLIDEAN_DISTANCE(
  ARRAY<FLOAT32>[1.0, 2.0, 3.0], NullableDocEmbedding,
  options => JSON '{"num_leaves_to_search": 10}')
LIMIT 100

Bonnes pratiques

Suivez ces bonnes pratiques pour optimiser vos index vectoriels et améliorer les résultats des requêtes.

Régler les options de recherche vectorielle

La valeur de recherche vectorielle la plus optimale dépend du cas d'utilisation, de l'ensemble de données vectorielles et des vecteurs de requête. Vous devrez peut-être effectuer un réglage itératif pour trouver les meilleures valeurs pour votre charge de travail spécifique.

Voici quelques consignes utiles à suivre pour choisir les valeurs appropriées :

  • tree_depth (niveau de l'arborescence) : si la table indexée comporte moins de 10 millions de lignes, utilisez un tree_depth de 2. Sinon, un tree_depth de 3 accepte les tables comportant jusqu'à environ 10 milliards de lignes.

  • num_leaves : utilisez la racine carrée du nombre de lignes dans l'ensemble de données. Une valeur plus élevée peut augmenter le temps de création de l'index vectoriel. Évitez de définir num_leaves sur une valeur supérieure à table_row_count/1000, car cela entraînerait des feuilles trop petites et de mauvaises performances.

  • num_leaves_to_search : cette option spécifie le nombre de nœuds feuilles de l'index à rechercher. L'augmentation de num_leaves_to_search améliore le rappel, mais augmente également la latence et les coûts. Nous vous recommandons d'utiliser une valeur pour num_leaves_to_search qui correspond à 1 % du nombre total de feuilles défini dans l'instruction CREATE VECTOR INDEX. Si vous utilisez une clause de filtre, augmentez cette valeur pour élargir la recherche.

Si vous obtenez un rappel acceptable, mais que le coût des requêtes est trop élevé, ce qui entraîne un faible nombre maximal de requêtes par seconde, essayez d'augmenter num_leaves en suivant ces étapes :

  1. Définissez num_leaves sur un multiple k de sa valeur d'origine (par exemple, 2 * sqrt(table_row_count)).
  2. Définissez num_leaves_to_search sur le même multiple k de sa valeur d'origine.
  3. Essayez de réduire num_leaves_to_search pour améliorer le coût et les RPS tout en conservant le rappel.

Améliorer le rappel

Plusieurs raisons peuvent expliquer la dégradation du rappel, y compris les suivantes :

  • num_leaves_to_search est trop petit : vous aurez peut-être plus de mal à trouver les voisins les plus proches pour certains vecteurs de requête. Augmenter num_leaves_to_search pour rechercher davantage de feuilles peut aider à améliorer le rappel. Il est possible que les requêtes récentes aient évolué pour contenir davantage de ces vecteurs complexes.

  • L'index vectoriel doit être reconstruit : la structure arborescente de l'index vectoriel est optimisée pour l'ensemble de données au moment de la création et reste statique par la suite. Par conséquent, si des vecteurs très différents sont ajoutés après la création de l'index vectoriel initial, la structure arborescente peut être sous-optimale, ce qui entraîne un rappel plus faible.

Recompiler l'index vectoriel

Pour recompiler votre index vectoriel sans temps d'arrêt :

  1. Créez un index vectoriel sur la même colonne d'embedding que l'index vectoriel actuel, en mettant à jour les paramètres (par exemple, OPTIONS) selon vos besoins.
  2. Une fois l'index créé, utilisez l'indication FORCE_INDEX pour pointer vers le nouvel index afin de mettre à jour la requête de recherche vectorielle. afin que la requête utilise le nouvel index de vecteur. Vous devrez peut-être également réajuster num_leaves_to_search dans votre nouvelle requête.
  3. Supprimez l'index vectoriel obsolète.

Étapes suivantes