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.
🚀 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.
🛠️ 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
transformersosentence-transformerspara 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
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:
- 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.
- 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.
- 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 entruepara 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 soncosine(coseno),l2_norm(distancia euclidiana) odot_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.")
🔍 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 campodense_vectordonde 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_candidatesdebe ser significativamente mayor quek(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.
⚡ 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:
- 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.
- Búsqueda Híbrida de Tipo
should: Usa tanto kNN como una consulta textual en la misma búsqueda, combinando los scores. - 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
knnque realiza la búsqueda vectorial. - Tenemos una cláusula
querycon unmatchpara 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.
✨ 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.
📈 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 conk * 5ok * 10.dims(Dimensiones del Vector): Vectores con menos dimensiones (384vs768) 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:
cosineydot_productsuelen tener un rendimiento similar.l2_normpuede 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 ajustarwindow_sizeenrrfpara 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 consultasbool/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
filteromusten una consultaboolpara reducir el conjunto de documentos antes de la búsqueda vectorial, mejorando el rendimiento.
🔮 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.
✅ 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_vectorysparse_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
- Explorando el Motor de Búsqueda: Cómo Funcionan las Consultas de Texto Completo en Elasticsearchintermediate20 min
- Configurando Clusteres de Elasticsearch con Alta Disponibilidad: Un Enfoque Prácticointermediate20 min
- Analizando Datos con Agregaciones en Elasticsearch: Guía Completa de Usointermediate15 min
- Optimización de Consultas en Elasticsearch: Un Enfoque Práctico para el Rendimientointermediate15 min
- Analizando Logs y Eventos en Tiempo Real con Elasticsearch y Filebeatintermediate25 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!