tutoriales.com

Explorando la Búsqueda Vectorial con kNN y Búsquedas Híbridas en Elasticsearch

Este tutorial te guiará a través del poder de la búsqueda vectorial (kNN) y cómo combinarla con la búsqueda tradicional para crear experiencias de búsqueda híbridas y más relevantes en Elasticsearch. Aprenderás desde la preparación de tus datos hasta la ejecución de consultas complejas.

Intermedio20 min de lectura5 views
Reportar error

🚀 Introducción a la Búsqueda Vectorial y Híbrida en Elasticsearch

En el mundo actual de grandes volúmenes de datos, la búsqueda de información relevante se ha vuelto más desafiante que nunca. Tradicionalmente, Elasticsearch ha brillado con sus capacidades de búsqueda textual, utilizando modelos como BM25 para encontrar documentos basados en la coincidencia de palabras clave. Sin embargo, a menudo las búsquedas textuales pueden fallar en capturar el significado semántico o la intención detrás de una consulta.

Aquí es donde entra en juego la búsqueda vectorial, también conocida como Nearest Neighbor Search (kNN) o Similarity Search. En lugar de comparar palabras, compara representaciones numéricas (vectores o embeddings) de documentos y consultas. Estos vectores capturan el significado semántico del texto, imágenes o cualquier otro tipo de dato, permitiendo encontrar elementos "similares" incluso si no comparten las mismas palabras clave.

La verdadera magia ocurre cuando combinamos estas dos poderosas técnicas: la búsqueda textual y la búsqueda vectorial. Esto nos lleva a la búsqueda híbrida, que aprovecha lo mejor de ambos mundos para ofrecer resultados de búsqueda más precisos, relevantes y contextuales. Este tutorial te sumergirá en cómo implementar y aprovechar estas técnicas avanzadas en Elasticsearch.


🎯 ¿Por qué Búsqueda Vectorial y Híbrida?

Imagina que estás buscando "coches deportivos" y tu sistema de búsqueda tradicional solo te muestra resultados que contienen exactamente esas dos palabras. ¿Qué pasa si un documento habla de "automóviles de alta velocidad"? Una búsqueda textual pura podría pasarlo por alto. Con la búsqueda vectorial, si "automóviles de alta velocidad" tiene un significado semántico similar a "coches deportivos", sería incluido en los resultados.

Beneficios Clave:

  • Relevancia Semántica: Encuentra resultados basados en el significado, no solo en la coincidencia exacta de palabras.
  • Manejo de Sinónimos y Jerga: Supera las limitaciones de los diccionarios de sinónimos al entender el contexto.
  • Búsqueda Multimodal: Permite buscar texto usando imágenes, o viceversa, si tienes embeddings multimodales.
  • Personalización Mejorada: Facilita la recomendación de contenido verdaderamente similar a lo que el usuario ha interactuado antes.
  • Resiliencia a la Brecha Léxica: Cierra la brecha entre las palabras que usa el usuario y las palabras que se usan en los documentos.
💡 Consejo: La búsqueda híbrida es ideal para mejorar la experiencia del usuario en e-commerce, motores de búsqueda internos, sistemas de recomendación y análisis de datos no estructurados.

🛠️ Requisitos y Configuración Inicial

Para seguir este tutorial, necesitarás:

  • Una instancia de Elasticsearch 8.x o superior (la búsqueda vectorial kNN nativa se introdujo y mejoró significativamente en estas versiones).
  • Conocimientos básicos de cómo interactuar con Elasticsearch (índices, documentos, consultas).
  • Opcionalmente, Python y librerías como transformers o sentence-transformers para generar embeddings si no usas los modelos de inferencia de ELSER (Elastic Learned Sparse Encoder).

Instalación de Elasticsearch

Si aún no tienes Elasticsearch, puedes instalarlo localmente o usar Elastic Cloud. Para una instalación local rápida con Docker:

docker pull docker.elastic.co/elasticsearch/elasticsearch:8.11.0
docker run -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:8.11.0
⚠️ Advertencia: Para entornos de producción, configura la seguridad adecuadamente y usa un cluster de varios nodos. El comando anterior es solo para pruebas locales rápidas.

Generación de Embeddings (Vectores)

La clave de la búsqueda vectorial son los embeddings. Estos son arrays de números (vectores) que representan la información de un documento o consulta en un espacio multidimensional. Documentos con significados similares tendrán vectores "cercanos" en este espacio.

Hay varias formas de generar embeddings:

  1. Modelos Pre-entrenados: Usar modelos de lenguaje grandes (LLMs) como BERT, RoBERTa, o modelos específicos como Sentence-Transformers. Puedes ejecutar estos modelos localmente o usar servicios en la nube.
  2. Modelos de Inferencia de Elasticsearch (ELSER): Elasticsearch 8.x en adelante permite usar modelos de inferencia directamente para generar sparse vectors. Esto es muy conveniente y lo veremos en un ejemplo.
  3. Modelos Propios: Entrenar tus propios modelos si tienes un dominio muy específico.

En este tutorial, usaremos un enfoque que genera dense vectors para el kNN clásico y exploraremos brevemente los sparse vectors con ELSER para búsquedas híbridas avanzadas.


📝 Preparando los Datos para Búsqueda Vectorial

Para realizar búsquedas vectoriales, tus documentos deben contener un campo de tipo dense_vector. Este campo almacenará los embeddings.

1. Definir el Mapping

Primero, crearemos un índice con un mapping adecuado. Supongamos que queremos buscar productos por su descripción.

PUT /productos_vectoriales
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "nombre": {
        "type": "text"
      },
      "descripcion": {
        "type": "text"
      },
      "descripcion_vector": {
        "type": "dense_vector",
        "dims": 768, 
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}
  • dense_vector: El tipo de campo para almacenar embeddings.
  • dims: El número de dimensiones del vector. Esto debe coincidir con la salida de tu modelo de embedding (por ejemplo, muchos modelos de Sentence-Transformers producen 768 o 384 dimensiones).
  • index: Establecido en true para que Elasticsearch construya un índice HNSW (Hierarchical Navigable Small Worlds) optimizado para la búsqueda kNN.
  • similarity: Define cómo se calculará la distancia/similitud entre vectores. Opciones comunes son cosine (coseno), l2_norm (distancia euclidiana) o dot_product (producto punto).

2. Generar e Indexar Embeddings

Ahora, necesitamos generar los vectores para nuestras descripciones de productos e indexarlos junto con los demás datos. Utilizaremos un modelo de Sentence-Transformers en Python para este ejemplo.

Primero, instala la librería:

pip install sentence-transformers elasticsearch

Luego, un script Python para generar e indexar:

from sentence_transformers import SentenceTransformer
from elasticsearch import Elasticsearch

# Conecta a Elasticsearch
es = Elasticsearch(
    "http://localhost:9200", # Ajusta si tu ES no está en localhost
    # api_key=("id", "api_key"), # Descomenta y configura si la seguridad está habilitada
    # basic_auth=("elastic", "password") # Descomenta y configura si la seguridad está habilitada
)

# Carga el modelo de embedding (ej. 'all-MiniLM-L6-v2' para 384 dimensiones)
# O 'all-mpnet-base-v2' para 768 dimensiones (ajusta 'dims' en el mapping)
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') 

documentos = [
    {
        "id": 1,
        "nombre": "Smartwatch Deportivo",
        "descripcion": "Reloj inteligente avanzado con monitor de ritmo cardíaco, GPS y seguimiento de actividad. Ideal para corredores y entusiastas del fitness."
    },
    {
        "id": 2,
        "nombre": "Auriculares Inalámbricos",
        "descripcion": "Auriculares Bluetooth con cancelación de ruido, sonido de alta fidelidad y batería de larga duración. Perfectos para viajes y llamadas."
    },
    {
        "id": 3,
        "nombre": "Monitor de Actividad Física",
        "descripcion": "Dispositivo compacto para monitorizar pasos, calorías quemadas y calidad del sueño. Sincroniza con tu smartphone."
    },
    {
        "id": 4,
        "nombre": "Laptop Ultraligera",
        "descripcion": "Portátil potente y ligero para profesionales, con procesador de última generación y pantalla 4K. Productividad en movimiento."
    },
    {
        "id": 5,
        "nombre": "Zapatillas de Running",
        "descripcion": "Zapatillas de correr diseñadas para alto rendimiento, con amortiguación superior y soporte para largas distancias. Mejora tu entrenamiento."
    }
]

for doc in documentos:
    # Genera el embedding para la descripción
    doc["descripcion_vector"] = model.encode(doc["descripcion"]).tolist()
    
    # Indexa el documento en Elasticsearch
    es.index(index="productos_vectoriales", id=doc["id"], document=doc)
    print(f"Documento {doc['id']} indexado.")

print("Indexación completa.")
📌 Nota: Asegúrate de que las dimensiones de tu modelo (`dims`) coincidan con las definidas en tu mapping (`384` para `all-MiniLM-L6-v2` o `768` para `all-mpnet-base-v2`). Si usas `all-MiniLM-L6-v2`, deberás ajustar el `dims` en el mapping a `384` en lugar de `768`.

🔍 Realizando Búsquedas kNN Puras (Vectoriales)

Una vez que los documentos están indexados con sus vectores, podemos realizar búsquedas kNN. Elasticsearch ofrece la consulta knn para esto.

1. Preparar el Vector de la Consulta

De manera similar a la indexación, primero necesitamos convertir nuestra consulta de texto en un vector usando el mismo modelo que usamos para indexar los documentos.

# ... (código anterior para cargar el modelo SentenceTransformer)

consulta_texto = "dispositivo para seguir el estado físico y hacer ejercicio"
consulta_vector = model.encode(consulta_texto).tolist()

# Ahora usa consulta_vector en tu consulta de Elasticsearch

2. Ejecutar la Consulta kNN

La consulta knn se ejecuta dentro de un objeto _search y busca los k vecinos más cercanos al vector de tu consulta.

GET /productos_vectoriales/_search
{
  "knn": {
    "field": "descripcion_vector",
    "query_vector": [0.1, -0.2, ..., 0.5], 
    "k": 5,
    "num_candidates": 100
  },
  "_source": ["nombre", "descripcion"]
}
  • field: El campo dense_vector donde buscar.
  • query_vector: El vector de tu consulta (reemplaza [0.1, -0.2, ..., 0.5] con el vector real generado por tu modelo).
  • k: El número de vecinos más cercanos (resultados) que quieres que se devuelvan.
  • num_candidates: El número de vectores que el algoritmo kNN inspeccionará de forma aproximada. Un valor mayor mejora la precisión, pero aumenta el tiempo de búsqueda. Generalmente, num_candidates debe ser significativamente mayor que k (ej. k * 10).

Ejemplo completo usando Python:

# ... (código anterior para conectar a ES y cargar el modelo)

consulta_texto = "dispositivo para seguir el estado físico y hacer ejercicio"
consulta_vector = model.encode(consulta_texto).tolist()

resp = es.search(
    index="productos_vectoriales",
    knn={
        "field": "descripcion_vector",
        "query_vector": consulta_vector,
        "k": 5, # Queremos los 5 mejores resultados
        "num_candidates": 50 # Inspeccionamos 50 candidatos para encontrar los 5 mejores
    },
    _source=["nombre", "descripcion"]
)

print("Resultados de la búsqueda kNN pura:")
for hit in resp["hits"]["hits"]:
    print(f"  Score: {hit['_score']:.2f}, Nombre: {hit['_source']['nombre']}, Descripción: {hit['_source']['descripcion']}")

Los resultados mostrarán documentos cuya descripción vectorial es semánticamente similar a la consulta vectorial, incluso si no contienen las palabras exactas.

💡 Consejo: El `_score` en los resultados de kNN representa la similitud entre el vector de consulta y el vector del documento, de acuerdo con la función de similitud (`cosine`, `l2_norm`, etc.) configurada en el mapping.

⚡ Búsquedas Híbridas: Combinando kNN y Búsqueda Textual

La verdadera potencia se desata cuando combinamos la precisión semántica del kNN con la robustez y la capacidad de filtrado de la búsqueda textual tradicional de Elasticsearch. Esto se logra utilizando la cláusula knn junto con las consultas query estándar.

Estrategias de Búsqueda Híbrida

Hay varias maneras de combinar búsquedas:

  1. Re-rankeo de kNN con Query Textual: Realiza una búsqueda kNN amplia y luego filtra o re-rankea esos resultados usando una consulta textual.
  2. Búsqueda Híbrida de Tipo should: Usa tanto kNN como una consulta textual en la misma búsqueda, combinando los scores.
  3. Búsqueda Híbrida con RRF (Reciprocal Rank Fusion): Una técnica más avanzada para combinar los rangos de diferentes tipos de consultas de forma imparcial. Es la forma recomendada por Elasticsearch para la mayoría de los casos de uso híbrido.

Vamos a enfocarnos en la estrategia Reciprocal Rank Fusion (RRF), que es la más efectiva para búsquedas híbridas en Elasticsearch 8.x.

Implementando Búsqueda Híbrida con RRF

RRF funciona combinando los resultados de múltiples consultas (una kNN y una textual, por ejemplo) basándose en su rango, en lugar de su score absoluto. Esto evita que una consulta domine a la otra si sus scores no son comparables.

Primero, asegurémonos de que nuestro índice tiene también un campo text para la búsqueda textual.

Mapping (si no lo hiciste antes):

PUT /productos_vectoriales
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "nombre": {
        "type": "text"
      },
      "descripcion": {
        "type": "text"
      },
      "descripcion_vector": {
        "type": "dense_vector",
        "dims": 384, 
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

Ejemplo de Consulta Híbrida con RRF:

GET /productos_vectoriales/_search
{
  "knn": {
    "field": "descripcion_vector",
    "query_vector": [0.1, -0.2, ..., 0.5], 
    "k": 5,
    "num_candidates": 100
  },
  "query": {
    "match": {
      "descripcion": "dispositivo para seguir el estado físico"
    }
  },
  "rank": {
    "rrf": {}
  },
  "_source": ["nombre", "descripcion"]
}

En este ejemplo:

  • Tenemos una cláusula knn que realiza la búsqueda vectorial.
  • Tenemos una cláusula query con un match para la búsqueda textual.
  • La cláusula rank: { "rrf": {} } indica a Elasticsearch que combine los resultados de ambas consultas usando Reciprocal Rank Fusion.

Ejemplo completo en Python:

# ... (código anterior para conectar a ES y cargar el modelo)

consulta_texto = "dispositivo para seguir el estado físico y hacer ejercicio"
consulta_vector = model.encode(consulta_texto).tolist()

resp = es.search(
    index="productos_vectoriales",
    knn={
        "field": "descripcion_vector",
        "query_vector": consulta_vector,
        "k": 5, # Queremos los 5 mejores resultados del kNN
        "num_candidates": 50 
    },
    query={
        "match": {
            "descripcion": consulta_texto
        }
    },
    rank={
        "rrf": {}
    },
    _source=["nombre", "descripcion"]
)

print("\nResultados de la búsqueda híbrida con RRF:")
for hit in resp["hits"]["hits"]:
    print(f"  Score: {hit['_score']:.2f}, Nombre: {hit['_source']['nombre']}, Descripción: {hit['_source']['descripcion']}")

Observa cómo los resultados ahora combinan la relevancia textual con la semántica, potencialmente trayendo documentos que de otra manera podrían haberse perdido en una búsqueda puramente textual o puramente vectorial.

🔥 Importante: La búsqueda híbrida con RRF te da un mayor control sobre cómo se mezclan los resultados de diferentes estrategias de búsqueda, lo que lleva a una mejora significativa en la relevancia percibida por el usuario.

✨ Uso de Sparse Vectors con ELSER para Búsquedas Híbridas Avanzadas

Elasticsearch también soporta la generación de sparse vectors a través de su modelo ELSER (Elastic Learned Sparse Encoder). A diferencia de los dense vectors que tienen un número fijo de dimensiones y donde cada dimensión contribuye al significado, los sparse vectors son de alta dimensionalidad y solo unas pocas dimensiones son distintas de cero. Estos son particularmente efectivos para búsquedas basadas en palabras clave expandidas y semánticamente ricas.

ELSER está disponible como un modelo pre-entrenado en Elasticsearch. Para usarlo, primero debes desplegar el modelo.

1. Desplegar el Modelo ELSER

Esto se hace a través de la API de Machine Learning de Elasticsearch.

PUT _ml/trained_models/elser_model_v1/deployment/_start
{
  "model_id": "elser_model_v1",
  "deployment_id": "elser_deployment",
  "threads_per_alloc": 1,
  "number_of_allocations": 1
}

Verifica el estado del despliegue:

GET _ml/trained_models/elser_model_v1?deployment_stats=true

2. Actualizar el Mapping para Sparse Vectors

Necesitarás un campo de tipo sparse_vector en tu mapping para almacenar los embeddings de ELSER. Además, puedes usar un inference_processor para generar automáticamente los sparse vectors al indexar.

PUT /productos_elser
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "analysis": {
      "analyzer": {
        "text_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase"]
        }
      }
    },
    "default_pipeline": "elser-ingest"
  },
  "mappings": {
    "properties": {
      "nombre": {
        "type": "text"
      },
      "descripcion": {
        "type": "text",
        "analyzer": "text_analyzer"
      },
      "descripcion_embedding": {
        "type": "sparse_vector"
      }
    }
  },
  "processors": [
    {
      "inference": {
        "model_id": "elser_model_v1",
        "target_field": "descripcion_embedding",
        "field_map": {
          "descripcion": "text_field"
        },
        "on_failure": [
          {
            "set": {
              "field": "_ingest.on_failure_message",
              "value": "Processor 'inference' failed with message {{ _ingest.on_failure_message }}"
            }
          }
        ]
      }
    }
  ]
}

En este mapping, hemos definido un default_pipeline que usa un inference_processor. Cada vez que se indexe un documento en este índice, el descripcion_embedding se generará automáticamente a partir del campo descripcion utilizando el modelo elser_model_v1.

3. Indexar Documentos con ELSER

Simplemente indexa tus documentos de forma normal; el pipeline de ingestión se encargará de generar el descripcion_embedding.

PUT /productos_elser/_doc/1
{
  "nombre": "Smartwatch Deportivo Pro",
  "descripcion": "Reloj de fitness avanzado con GPS, monitor de ritmo cardíaco y seguimiento de sueño. Batería de larga duración."
}

PUT /productos_elser/_doc/2
{
  "nombre": "Auriculares Noise Cancelling",
  "descripcion": "Auriculares over-ear con cancelación activa de ruido y sonido inmersivo. Perfectos para audiofilos."
}

4. Realizar Búsqueda Híbrida con ELSER (Sparse Vector + Búsqueda Textual)

Ahora, podemos usar los sparse vectors de ELSER en una consulta híbrida. La consulta text_expansion es específica para sparse vectors.

GET /productos_elser/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "text_expansion": {
            "descripcion_embedding": {
              "model_id": "elser_model_v1",
              "model_text": "dispositivo para seguir el estado físico y hacer ejercicio",
              "boost": 0.5
            }
          }
        },
        {
          "match": {
            "descripcion": {
              "query": "dispositivo para seguir el estado físico",
              "boost": 0.5
            }
          }
        }
      ]
    }
  },
  "rank": {
    "rrf": {}
  },
  "_source": ["nombre", "descripcion"]
}

Aquí, estamos usando text_expansion para la búsqueda vectorial (sparse) y match para la búsqueda textual, combinándolos con rrf. El model_text en text_expansion es tu consulta de texto que ELSER convertirá en sparse vector en tiempo de consulta.

⚠️ Advertencia: La búsqueda con `text_expansion` es más costosa computacionalmente en tiempo de consulta que la búsqueda `knn` con dense vectors (donde el vector ya está precalculado). Considera tus necesidades de latencia.

📈 Optimización y Consideraciones de Rendimiento

Implementar la búsqueda vectorial y híbrida requiere considerar el rendimiento, especialmente a escala.

1. Optimización de kNN (dense_vector)

  • num_candidates: Un valor más alto mejora la precisión pero ralentiza la consulta. Experimenta para encontrar el equilibrio óptimo. Empieza con k * 5 o k * 10.
  • dims (Dimensiones del Vector): Vectores con menos dimensiones (384 vs 768) son más rápidos de buscar y requieren menos almacenamiento, pero pueden ser menos precisos. Elige un modelo de embedding adecuado para tus necesidades.
  • Función de Similitud: cosine y dot_product suelen tener un rendimiento similar. l2_norm puede ser ligeramente más lento. Elige la que mejor se adapte a tu espacio vectorial.
  • Hardware: La búsqueda vectorial es intensiva en CPU y memoria. Asegúrate de tener recursos suficientes en tus nodos de Elasticsearch.

2. Optimización de ELSER (sparse_vector)

  • Despliegue de Modelos: Asegúrate de que los modelos estén desplegados eficientemente y que haya suficientes recursos (threads/allocations) para manejar la carga de inferencia, tanto en indexación como en tiempo de consulta.
  • Caché de Inferencia: Elasticsearch cachea los resultados de inferencia. Asegúrate de que tu configuración de JVM tenga suficiente memoria para el heap para aprovechar esto.

3. Consideraciones para la Búsqueda Híbrida

  • RRF rank.rrf.window_size: Puedes ajustar window_size en rrf para controlar cuántos resultados de cada sub-consulta se consideran para la fusión. Un valor más pequeño es más rápido pero puede perder algunos candidatos.
  • Pesos (boost): En consultas bool/should (donde no se usa RRF), puedes ajustar los pesos (boost) de las consultas kNN y textual para dar más importancia a una sobre la otra.
  • Pre-filtrado: Puedes combinar la búsqueda kNN con filtros filter o must en una consulta bool para reducir el conjunto de documentos antes de la búsqueda vectorial, mejorando el rendimiento.
⚠️ Advertencia: La latencia es un factor crítico. Monitorea de cerca los tiempos de respuesta de tus consultas híbridas y ajusta los parámetros de optimización según sea necesario.

🔮 Futuro y Próximos Pasos

La búsqueda vectorial y la inteligencia artificial generativa están evolucionando rápidamente. Elasticsearch está a la vanguardia, integrando cada vez más capacidades de IA directamente en el motor de búsqueda.

Temas Avanzados a Explorar:

  • Semantic Search con RAG (Retrieval Augmented Generation): Combina Elasticsearch como motor de recuperación con LLMs para generar respuestas contextuales y precisas. Elasticsearch ya tiene integraciones y funciones para esto.
  • Personalización con Vectores: Usa vectores de perfiles de usuario y de ítems para ofrecer recomendaciones altamente personalizadas.
  • Búsqueda Multimodal: Indexa vectores de diferentes tipos de datos (texto, imágenes, audio) para búsquedas complejas entre ellos.
  • Filtrado Híbrido Avanzado: Explora cómo los filtros booleanos interactúan con las búsquedas kNN para refinar aún más los resultados.
📌 Nota: Mantente al día con las últimas versiones de Elasticsearch, ya que las capacidades de búsqueda vectorial e híbrida están en constante mejora y expansión.

✅ Conclusión

Dominar la búsqueda vectorial y la búsqueda híbrida en Elasticsearch abre un mundo de posibilidades para construir experiencias de búsqueda y descubrimiento de información mucho más inteligentes y relevantes. Al combinar la potencia de la coincidencia semántica con la precisión de la búsqueda textual, puedes superar las limitaciones de los sistemas tradicionales y ofrecer a tus usuarios resultados que realmente entiendan su intención.

Has aprendido a:

  • Configurar campos dense_vector y sparse_vector.
  • Generar embeddings y indexarlos.
  • Realizar búsquedas kNN puras.
  • Implementar búsquedas híbridas utilizando RRF.
  • Entender las consideraciones de rendimiento y optimización.

¡Ahora estás listo para llevar tus aplicaciones de búsqueda al siguiente nivel! Experimenta con tus propios datos, ajusta los modelos y los parámetros, y observa cómo la relevancia de tus resultados mejora drásticamente.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!