Buscar con embeddings de vectores

En esta página, se muestra cómo usar Firestore para realizar operaciones K-cerca de vectores vecino (KNN) con las siguientes técnicas:

  • Almacena valores vectoriales
  • Crea y administra índices de vectores de KNN
  • Hacer una consulta de K-vecino más cercano (KNN) con uno de los vectores compatibles medidas de distancia

Almacena embeddings de vectores

Puedes crear valores vectoriales como incorporaciones de texto a partir de tu y almacenarlos en documentos de Firestore.

Operación de escritura con una embedding de vector

En el siguiente ejemplo, se muestra cómo almacenar una incorporación de vector en un documento de 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])
});

Calcula incorporaciones vectoriales con una Cloud Function

Calcular y almacenar incorporaciones vectoriales cada vez que se actualiza un documento o puedes configurar una función de 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,
  });
}

Crea y administra índices vectoriales

Antes de realizar una búsqueda de vecino más cercano con tus incorporaciones vectoriales, debes crear un índice correspondiente. En los siguientes ejemplos, se muestran cómo crear y administrar índices vectoriales.

Crea un índice vectorial

Antes de crear un índice vectorial, actualiza a la versión más reciente de Google Cloud CLI:

gcloud components update

Para crear un índice vectorial, usa 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

Donde:

  • collection-group es el ID del grupo de colecciones.
  • vector-field es el nombre del campo que contiene la embedding de vector.
  • database-id es el ID de la base de datos.
  • vector-configuration incluye el vector dimension y el tipo de índice. dimension es un número entero hasta 2,048. El tipo de índice debe ser flat. Da formato a la configuración del índice de la siguiente manera: {"dimension":"DIMENSION", "flat": "{}"}.

En el siguiente ejemplo, se crea un índice compuesto, que incluye un índice de vectores para el campo vector-field y un índice ascendente para el campo color. Puedes usar este tipo de índice para realizar un filtro previo de datos antes de buscar un vecino más cercano.

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

Enumerar todos los índices vectoriales

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

Reemplaza database-id por el ID de la base de datos.

Borra un índice vectorial

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

Donde:

  • index-id es el ID del índice que se borrará. Usa indexes composite list para recuperar el ID del índice.
  • database-id es el ID de la base de datos.

Describir un índice vectorial

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

Donde:

  • index-id es el ID del índice que se describirá. Usar o indexes composite list para recuperar el ID del índice.
  • database-id es el ID de la base de datos.

Haz una consulta de vecino más cercano

Puedes realizar una búsqueda de similitud para encontrar los vecinos más cercanos de la embedding de vector. Las búsquedas de similitud requieren índices vectoriales. Si no existe un índice, Firestore sugiere crear uno nuevo con gcloud CLI.

En el siguiente ejemplo, se encuentran 10 vecinos más cercanos del vector de consulta.

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

Distancias vectoriales

Las consultas de vecino más cercano admiten las siguientes opciones para la distancia vectorial:

  • EUCLIDEAN: Mide la distancia de EUCLIDEAN entre los vectores. Para obtener más información, consulta Euclidea.
  • COSINE: Compara vectores según el ángulo entre ellos, lo que te permite medir la similitud que no se basa en la magnitud de los vectores. Recomendamos usar DOT_PRODUCT con vectores normalizados de unidades en lugar de la distancia de COSINE, que es matemáticamente equivalente con un mejor rendimiento. Para obtener más información, consulta Similitud coseno para obtener más información.
  • DOT_PRODUCT: Es similar a COSINE, pero se ve afectado por la magnitud de laos vectores. Para obtener más información, consulta Producto de punto.

Elige la medida de distancia

Dependiendo de si todas tus incorporaciones vectoriales están normalizadas, puedes determinar qué medida de distancia usar para encontrar la medida de distancia. Una incorporación de vectores normalizada tiene una magnitud (longitud) de exactamente 1.0.

Además, si sabes con qué medida de distancia se entrenó tu modelo, esa medición de distancia para calcular la distancia entre tu vector de las incorporaciones.

Datos normalizados

Si tienes un conjunto de datos en el que todas las incorporaciones vectoriales están normalizadas, entonces las tres las mediciones de distancia proporcionan los mismos resultados de búsqueda semántica. En esencia, aunque cada medida de distancia muestra un valor diferente, esos valores se ordenan de la misma manera. Cuando las incorporaciones se normalizan, DOT_PRODUCT suele ser la más eficiente en términos de procesamiento, pero la diferencia es despreciable en la mayoría de los casos. Sin embargo, si tus aplicación es muy sensible al rendimiento, DOT_PRODUCT podría ayudar con ajustar el rendimiento.

Datos no normalizados

Si tienes un conjunto de datos en el que las incorporaciones vectoriales no están normalizadas, entonces no es matemáticamente correcto usar DOT_PRODUCT como distancia medir porque el producto punto no mide la distancia. Según cómo se generaron las incorporaciones y qué tipo de búsqueda se prefiere, la medición de distancia COSINE o EUCLIDEAN produce resultados de la búsqueda que son subjetivamente mejores que las otras mediciones de distancia. La experimentación con COSINE o EUCLIDEAN podría necesaria para determinar cuál es la mejor para tu caso de uso.

No sé si los datos están normalizados o no normalizados

Si no estás seguro de si tus datos están normalizados o no y deseas usar DOT_PRODUCT, te recomendamos que uses COSINE en su lugar. COSINE es como DOT_PRODUCT con normalización integrada. La distancia medida con COSINE varía de 0 a 2. Un resultado cerca de 0 indica que los vectores son muy similares.

Cómo aplicar un filtro previo a los documentos

Para filtrar previamente los documentos antes de encontrar los vecinos más cercanos, puedes combinar un la búsqueda de similitud con otros operadores de consulta. Se admiten los filtros compuestos and y or. Para obtener más información sobre los filtros de campo admitidos, consulta Operadores de consulta.

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

Cómo recuperar la distancia del vector calculada

Puedes recuperar la distancia vectorial calculada asignando un Nombre de la propiedad de salida de distance_result_field en la consulta FindNearest, como como se muestra en el siguiente ejemplo:

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 deseas usar una máscara de campo para mostrar un subconjunto de campos de documento junto con una distanceResultField, también debes incluir el valor de distanceResultField en la máscara de campo, como se muestra en el siguiente ejemplo:

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

Especifica un umbral de distancia

Puedes especificar un umbral de similitud que muestre solo documentos dentro del umbral. El comportamiento del campo de umbral depende de la medición de distancia eliges:

  • Las distancias EUCLIDEAN y COSINE limitan el umbral a los documentos en los que de distancia es menor o igual que el umbral especificado. Estas distancias disminuyen a medida que los vectores se vuelven más similares.
  • La distancia de DOT_PRODUCT limita el umbral a los documentos en los que la distancia es mayor o igual que el umbral especificado. Distancias del producto punto aumentan a medida que los vectores se vuelven más similares.

En el siguiente ejemplo, se muestra cómo especificar un umbral de distancia para mostrar hasta 10 documentos más cercanos que se encuentren, como máximo, a 4.5 unidades de distancia con la métrica de distancia 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);
});

Limitaciones

Cuando trabajes con incorporaciones vectoriales, ten en cuenta las siguientes limitaciones:

  • La dimensión de incorporación máxima admitida es 2,048. Para almacenar índices más grandes, usa reducción de dimensiones.
  • La cantidad máxima de documentos que se pueden mostrar con una consulta de vecino más cercano es de 1,000.
  • La búsqueda vectorial no admite objetos de escucha de instantáneas en tiempo real.
  • Solo las bibliotecas cliente de Python y Node.js admiten la búsqueda vectorial.

¿Qué sigue?