Effectuer une recherche à l'aide de représentations vectorielles continues

Cette page explique comment utiliser Firestore pour effectuer les K-plus proches de voisinage (KNN) à l'aide des techniques suivantes:

  • Stocker des valeurs vectorielles
  • Créer et gérer des index vectoriels KNN
  • Effectuer une requête KNN (le voisin le plus proche) à l'aide de l'un des vecteurs compatibles mesures de distance

Stocker les embeddings vectoriels

Vous pouvez créer des valeurs vectorielles, telles que des représentations vectorielles continues de texte, à partir de votre et les stocker dans des documents Firestore.

Opération d'écriture avec une représentation vectorielle continue de vecteur

L'exemple suivant montre comment stocker une représentation vectorielle continue de vecteur dans un Document Firestore:

Python
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector

firestore_client = firestore.Client()
collection = firestore_client.collection("coffee-beans")
doc = {
    "name": "Kahawa coffee beans",
    "description": "Information about the Kahawa coffee beans.",
    "embedding_field": Vector([1.0, 2.0, 3.0]),
}

collection.add(doc)
Node.js
import {
  Firestore,
  FieldValue,
} from "@google-cloud/firestore";

const db = new Firestore();
const coll = db.collection('coffee-beans');
await coll.add({
  name: "Kahawa coffee beans",
  description: "Information about the Kahawa coffee beans.",
  embedding_field: FieldValue.vector([1.0 , 2.0, 3.0])
});

Calculer des représentations vectorielles continues avec une fonction Cloud

Pour calculer et stocker des représentations vectorielles continues chaque fois qu'un document est mis à jour ou vous pouvez configurer une fonction Cloud Run:

Python
@functions_framework.cloud_event
def store_embedding(cloud_event) -> None:
  """Triggers by a change to a Firestore document.
  """
  firestore_payload = firestore.DocumentEventData()
  payload = firestore_payload._pb.ParseFromString(cloud_event.data)

  collection_id, doc_id = from_payload(payload)
  # Call a function to calculate the embedding
  embedding = calculate_embedding(payload)
  # Update the document
  doc = firestore_client.collection(collection_id).document(doc_id)
  doc.set({"embedding_field": embedding}, merge=True)
Node.js
/**
 * A vector embedding will be computed from the
 * value of the `content` field. The vector value
 * will be stored in the `embedding` field. The
 * field names `content` and `embedding` are arbitrary
 * field names chosen for this example.
 */
async function storeEmbedding(event: FirestoreEvent<any>): Promise<void> {
  // Get the previous value of the document's `content` field.
  const previousDocumentSnapshot = event.data.before as QueryDocumentSnapshot;
  const previousContent = previousDocumentSnapshot.get("content");

  // Get the current value of the document's `content` field.
  const currentDocumentSnapshot = event.data.after as QueryDocumentSnapshot;
  const currentContent = currentDocumentSnapshot.get("content");

  // Don't update the embedding if the content field did not change
  if (previousContent === currentContent) {
    return;
  }

  // Call a function to calculate the embedding for the value
  // of the `content` field.
  const embeddingVector = calculateEmbedding(currentContent);

  // Update the `embedding` field on the document.
  await currentDocumentSnapshot.ref.update({
    embedding: embeddingVector,
  });
}

Créer et gérer des index vectoriels

Avant de pouvoir effectuer une recherche du voisin le plus proche avec vos représentations vectorielles continues, vous devez créer un index correspondant. Les exemples suivants montrent comment créer et gérer des index vectoriels.

Créer un index vectoriel

Avant de créer un index vectoriel, passez à la dernière version de la Google Cloud CLI:

gcloud components update

Pour créer un index vectoriel, utilisez gcloud firestore indexes composite create:

gcloud
gcloud firestore indexes composite create \
--collection-group=collection-group \
--query-scope=COLLECTION \
--field-config field-path=vector-field,vector-config='vector-configuration' \
--database=database-id

où :

  • collection-group est l'ID du groupe de collections.
  • vector-field est le nom du champ qui contient la représentation vectorielle continue du vecteur.
  • database-id est l'ID de la base de données.
  • vector-configuration inclut le vecteur dimension et le type d'index. dimension est un entier jusqu'à 2 048. Le type d'index doit être flat. Mettez en forme la configuration d'index comme suit: {"dimension":"DIMENSION", "flat": "{}"}.

L'exemple suivant crée un index composite, y compris un index vectoriel pour le champ vector-field. et un index croissant pour le champ color. Vous pouvez utiliser ce type d'index pour pré-filtrer données avant une recherche de voisin le plus proche.

gcloud
gcloud firestore indexes composite create \
--collection-group=collection-group \
--query-scope=COLLECTION \
--field-config=order=ASCENDING,field-path="color" \
--field-config field-path=vector-field,vector-config='{"dimension":"1024", "flat": "{}"}' \
--database=database-id

Répertorier tous les index vectoriels

gcloud
gcloud firestore indexes composite list --database=database-id

Remplacez database-id par l'ID de la base de données.

Supprimer un index vectoriel

gcloud
gcloud firestore indexes composite delete index-id --database=database-id

où :

  • index-id correspond à l'ID de l'index à supprimer. Utilisez indexes composite list pour récupérer l'ID d'index.
  • database-id est l'ID de la base de données.

Décrire un index vectoriel

gcloud
gcloud firestore indexes composite describe index-id --database=database-id

où :

  • index-id est l'ID de l'index à décrire. Utiliser ou indexes composite list pour récupérer l'ID d'index.
  • database-id est l'ID de la base de données.

Effectuer une requête de voisin le plus proche

Vous pouvez effectuer une recherche par similarité pour trouver les voisins les plus proches de représentations vectorielles continues. Les recherches de similarité nécessitent des index vectoriels. Si un indice n'existe pas, Firestore suggère un indice à créer à l'aide de la gcloud CLI.

L'exemple suivant trouve les 10 voisins les plus proches du vecteur de requête.

Python
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

collection = db.collection("coffee-beans")

# Requires a single-field vector index
vector_query = collection.find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([3.0, 1.0, 2.0]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=5,
)
Node.js
import {
  Firestore,
  FieldValue,
  VectorQuery,
  VectorQuerySnapshot,
} from "@google-cloud/firestore";

// Requires a single-field vector index
const vectorQuery: VectorQuery = coll.findNearest({
  vectorField: 'embedding_field',
  queryVector: [3.0, 1.0, 2.0],
  limit: 10,
  distanceMeasure: 'EUCLIDEAN'
});

const vectorQuerySnapshot: VectorQuerySnapshot = await vectorQuery.get();

Distances vectorielles

Les requêtes de voisin le plus proche acceptent les options de distance vectorielle suivantes :

  • EUCLIDEAN: mesure la distance EUCLIDEAN entre les vecteurs. Pour en savoir plus, consultez Euclidienne.
  • COSINE: compare les vecteurs en fonction de l'angle qui les sépare, ce qui vous permet mesurer la similarité qui n'est pas basée sur l'amplitude des vecteurs. Nous vous recommandons d'utiliser DOT_PRODUCT avec des vecteurs normalisés unitaires plutôt que COSINE, ce qui équivaut mathématiquement à des performances. Pour en savoir plus, consultez la section Similitude cosinus.
  • DOT_PRODUCT: semblable à COSINE, mais il est affecté par la magnitude de l'erreur des vecteurs. Pour en savoir plus, consultez Produit scalaire :

Choisir la mesure de distance

Selon que toutes vos représentations vectorielles continues sont normalisées ou non, vous pouvez déterminer quelle mesure de distance utiliser pour trouver la mesure de distance. Un modèle normalisé la représentation vectorielle continue de vecteur a une magnitude (longueur) exactement égale à 1,0.

De plus, si vous savez avec quelle mesure de distance votre modèle a été entraîné, utilisez cette mesure de distance pour calculer la distance entre votre vecteur représentations vectorielles continues.

Données normalisées

Si vous disposez d'un ensemble de données dans lequel toutes les représentations vectorielles continues sont normalisées, les trois les mesures de distance fournissent les mêmes résultats de recherche sémantique. Concrètement, même si chaque la mesure de distance renvoie une valeur différente, ces valeurs sont triées de la même manière. Quand ? représentations vectorielles continues sont normalisées, DOT_PRODUCT est généralement la plus performante en termes de calcul efficace, mais la différence est négligeable dans la plupart des cas. Toutefois, si votre application sensible aux performances, DOT_PRODUCT peut vous aider l'optimisation des performances.

Données non normalisées

Si vous avez un ensemble de données dans lequel les représentations vectorielles continues ne sont pas normalisées, l'utilisation de DOT_PRODUCT comme distance n'est pas mathématiquement correcte mesure parce que le produit scalaire ne mesure pas la distance. En fonction sur la façon dont les représentations vectorielles continues ont été générées et sur le type de recherche à privilégier, la mesure de distance COSINE ou EUCLIDEAN génère des résultats de recherche qui sont subjectivement meilleurs que les autres mesures de distance. Vous pouvez effectuer des tests avec COSINE ou EUCLIDEAN pour déterminer la solution la plus adaptée à votre cas d'utilisation.

Je ne sais pas si les données sont normalisées ou non normalisées

Si vous ne savez pas si vos données sont normalisées et que vous souhaitez utiliser DOT_PRODUCT, nous vous recommandons d'utiliser COSINE à la place. COSINE est semblable à DOT_PRODUCT avec la normalisation intégrée. La distance mesurée à l'aide de COSINE varie de 0 à 2. Un résultat qui est proche de 0 indique que les vecteurs sont très similaires.

Préfiltrer les documents

Pour préfiltrer les documents avant de trouver les voisins les plus proches, vous pouvez combiner un la recherche par similarité avec d'autres opérateurs de requête. Les and et Les filtres composites or sont acceptés. Pour en savoir plus sur les filtres de champ compatibles, consultez la section Opérateurs de requête.

Python
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

collection = db.collection("coffee-beans")

# Similarity search with pre-filter
# Requires a composite vector index
vector_query = collection.where("color", "==", "red").find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([3.0, 1.0, 2.0]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=5,
)
Node.js
// Similarity search with pre-filter
// Requires composite vector index
const preFilteredVectorQuery: VectorQuery = coll
    .where("color", "==", "red")
    .findNearest({
      vectorField: "embedding_field",
      queryVector: [3.0, 1.0, 2.0],
      limit: 5,
      distanceMeasure: "EUCLIDEAN",
    });

const vectorQueryResults = await preFilteredVectorQuery.get();

Récupérer la distance vectorielle calculée

Vous pouvez récupérer la distance vectorielle calculée en attribuant un Nom de la propriété de sortie distance_result_field sur la requête FindNearest, comme illustré dans l'exemple suivant:

Python
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

collection = db.collection("coffee-beans")

vector_query = collection.find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([3.0, 1.0, 2.0]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=10,
    distance_result_field="vector_distance",
)

docs = vector_query.stream()

for doc in docs:
    print(f"{doc.id}, Distance: {doc.get('vector_distance')}")
Node.js
const vectorQuery: VectorQuery = coll.findNearest(
    {
      vectorField: 'embedding_field',
      queryVector: [3.0, 1.0, 2.0],
      limit: 10,
      distanceMeasure: 'EUCLIDEAN',
      distanceResultField: 'vector_distance'
    });

const snapshot: VectorQuerySnapshot = await vectorQuery.get();

snapshot.forEach((doc) => {
  console.log(doc.id, ' Distance: ', doc.get('vector_distance'));
});

Si vous souhaitez utiliser un masque de champ pour renvoyer un sous-ensemble de champs de document avec un distanceResultField, vous devez également inclure la valeur de distanceResultField dans le masque de champ, comme illustré dans l'exemple suivant :

Python
vector_query = collection.select(["color", "vector_distance"]).find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([3.0, 1.0, 2.0]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=10,
    distance_result_field="vector_distance",
)
Node.js
const vectorQuery: VectorQuery = coll
    .select('color', 'vector_distance')
    .findNearest({
      vectorField: 'embedding_field',
      queryVector: [3.0, 1.0, 2.0],
      limit: 10,
      distanceMeasure: 'EUCLIDEAN',
      distanceResultField: 'vector_distance'
    });

Spécifier un seuil de distance

Vous pouvez spécifier un seuil de similarité qui ne renvoie que les documents compris dans le de sortie. Le comportement du champ seuil dépend de la mesure de distance vous choisissez:

  • Les distances EUCLIDEAN et COSINE limitent le seuil aux documents où la distance est inférieure ou égale au seuil spécifié. Ces mesures de distance diminuent à mesure que les vecteurs deviennent plus similaires.
  • La distance DOT_PRODUCT limite le seuil aux documents dont la distance est supérieure ou égale au seuil spécifié. Distances des produits scalaires augmenter à mesure que les vecteurs deviennent plus similaires.

L'exemple suivant montre comment spécifier un seuil de distance pour afficher jusqu'à 10 documents les plus proches situés à une distance maximale de 4,5 unités à l'aide de la métrique de distance EUCLIDEAN:

Python
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
from google.cloud.firestore_v1.vector import Vector

collection = db.collection("coffee-beans")

vector_query = collection.find_nearest(
    vector_field="embedding_field",
    query_vector=Vector([3.0, 1.0, 2.0]),
    distance_measure=DistanceMeasure.EUCLIDEAN,
    limit=10,
    distance_threshold=4.5,
)

docs = vector_query.stream()

for doc in docs:
    print(f"{doc.id}")
Node.js
const vectorQuery: VectorQuery = coll.findNearest({
  vectorField: 'embedding_field',
  queryVector: [3.0, 1.0, 2.0],
  limit: 10,
  distanceMeasure: 'EUCLIDEAN',
  distanceThreshold: 4.5
});

const snapshot: VectorQuerySnapshot = await vectorQuery.get();

snapshot.forEach((doc) => {
  console.log(doc.id);
});

Limites

Lorsque vous utilisez des représentations vectorielles continues, tenez compte des limites suivantes:

  • La dimension de représentation vectorielle continue maximale acceptée est de 2 048. Pour stocker des index plus volumineux, utilisez réduction de la dimensionnalité.
  • Le nombre maximal de documents à renvoyer à partir d'une requête du voisin le plus proche est de 1 000.
  • La recherche vectorielle n'est pas compatible avec les écouteurs d'instantanés en temps réel.
  • Seules les bibliothèques clientes Python et Node.js sont compatibles avec la recherche vectorielle.

Étape suivante