Almacenar incrustaciones en Elasticsearch

Elasticsearch ofrece soporte completo para almacenar y recuperar vectores, lo que lo convierte en una base de datos ideal para trabajar con incrustaciones.

Tipos de campo

En el capítulo de Búsqueda de Texto Completo de este tutorial aprendiste a crear un índice con varios campos. En ese momento se mencionó que Elasticsearch puede, en su mayor parte, determinar automáticamente el mejor tipo para cada campo basar en los propios datos. Aunque Elasticsearch 8.11 puede mapear automáticamente algunos tipos de vectores, en este capítulo definirás este tipo explícitamente como una oportunidad para aprender más sobre mapeos de tipos en Elasticsearch.

Recuperación de mapeos de tipos

Los tipos asociados a cada campo en un índice se determinan mediante un proceso llamado mapeo, que puede ser dinámico o explícito. Los mapeos creados en la parte de Búsqueda de Texto Completo de este tutorial fueron todos generados dinámicamente por Elasticsearch.

El cliente Elasticsearch ofrece un método get_mapping , que devuelve los mapeos de tipos que están en vigor para un índice dado. Si quieres explorar estos mapeos por tu cuenta, inicia un shell de Python e introduce el siguiente código:

La respuesta del método get_mapping() es un diccionario con información sobre cada campo del índice. Para tu comodidad, a continuación tienes una estructura bien formateada de esta información para el índice de my_documents creado en la sección de Búsqueda de Texto Completo del tutorial:

A partir de esto se puede ver que los campos created_on y updated_at se tipificaban automáticamente con date, mientras que todos los demás campos recibían text como tipo. Al intentar decidir un tipo, Elasticsearch primero comprueba el tipo de datos, lo que ayuda a asignar tipos numéricos, booleanos y de objeto a los campos. Cuando los datos del campo son una cadena, también intenta ver si los datos coinciden con un patrón de fechas. La detección en cadenas basada en patrones también puede activar para números si se desea.

Los campos de texto tienen una definición fields con una entrada keyword . Esto se denomina subcampo, un tipo alternativo o secundario que está disponible para su uso cuando sea apropiado. En Elasticsearch, los campos de text tipados dinámicamente reciben un subcampo keyword . Ya empleaste el subcampo category.keyword para realizar una búsqueda exacta de una categoría determinada. Para evitar que se añada el subcampo, se puede dar una correspondencia explícita de text o keyword , y entonces este será el tipo principal y único.

Agregar un campo vectorial al índice

Vamos a agregar un nuevo campo al índice donde se almacenará una incrustación para cada documento.

La estructura de un mapeo explícito coincide con la clave mappings de la respuesta devuelta por el método get_mapping() del cliente Elasticsearch. Solo se deben indicar los campos que deben ser tipados explícitamente, ya que cualquier campo que no esté incluido en el mapeo seguirá siendo tipado dinámicamente como antes.

A continuación puedes ver una nueva versión del método create_index() de la clase Search , agregando un campo explícitamente tipado llamado embedding. Sustituye este método en search.py:

Como puedes ver, el campo embedding recibe un tipo de dense_vector, que es el tipo adecuado al almacenar incrustaciones. Más adelante aprenderás sobre otro tipo de vector, el sparse_vector, que es útil en otros tipos de aplicaciones de búsqueda semántica.

El tipo dense_vector acepta algunos parámetros, todos ellos opcionales.

  • dims: el tamaño de los vectores que se almacenarán. Desde la versión 8.11, las dimensiones se asignan automáticamente cuando se inserta el primer documento.
  • index: debe establecer en True para indicar que los vectores deben indexar para la búsqueda. Esto es lo predeterminado.
  • similarity: la función de distancia a usar al comparar vectores. Los dos más comunes son dot_product y cosine. El producto escalar es más eficiente, pero requiere normalizar vectores. El valor predeterminado es cosine.

Agregar incrustaciones en documentos

En la sección anterior aprendiste a generar incrustaciones usando el framework SentenceTransformers y el modelo all-MiniLM-L6-v2 . Ahora es el momento de integrar el modelo en la aplicación.

En primer lugar, el modelo puede instanciarse en el constructor de Search clase:

Como recordarás de la parte de búsqueda en texto completo de este tutorial, la clase Search tiene métodos insert_document() y insert_documents() , para insertar documentos individuales y múltiples en el índice respectivamente. Estos dos métodos ahora necesitan generar las incrustaciones correspondientes que acompañan a cada documento.

El siguiente bloque de código muestra nuevas versiones de estos dos métodos, junto con un nuevo método asistente de get_embedding() que devuelve una incrustación.

Los métodos modificados agregan el nuevo campo embedding al documento que se va a insertar. La incrustación se genera a partir del campo summary de cada documento. En general, los embeddings se generan a partir de oraciones o párrafos cortos, por lo que en este caso el resumen es un campo ideal para usar. Otras opciones fueron el campo name , que contiene el título del documento, o quizá las primeras frases del bodydel documento.

Con estos cambios, el índice puede reconstruir para almacenar una incrustación de cada documento. Para reconstruir el índice, usa este comando:

Por si necesitas un recordatorio, el comando flask reindex está implementado en la función reindex() en app.py. Llama al método reindex() de la clase Search , que a su vez invoca create_index() y luego pasa todos los datos del archivo data.json a insert_documents().

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo