Esta publicación da inicio a una serie de varias partes sobre la indexación vectorial compuesta en Couchbase. Comenzaremos por desarrollar la intuición y, a continuación, nos adentraremos progresivamente en los aspectos internos, las optimizaciones de ejecución y el rendimiento.
La serie tratará los siguientes temas:
- Por qué son importantes los índices vectoriales compuestos, incluyendo conceptos, terminología y motivación de los desarrolladores. Se utilizará un sistema inteligente de recomendación de productos alimenticios como ejemplo práctico.
- Cómo se implementan los índices vectoriales compuestos dentro del servicio de indexación de Couchbase.
- Cómo funciona ORDER BY pushdown para consultas vectoriales compuestas.
- Comportamiento real y resultados de pruebas comparativas.
Sistema inteligente de recomendación de productos alimenticios con ANN filtrada
Imagina que estás creando una aplicación para recomendar productos de supermercado.
Un usuario lo abre un domingo por la mañana y escribe:
“Me encanta la crema de chocolate negro para untar, pero estoy tratando de reducir el consumo de azúcar y aumentar el de proteínas. ¿Qué más debería comprar?”
En este momento, su sistema necesita comprender la intención del usuario, comparar los alimentos semánticamente y aplicar filtros nutricionales estrictos.
Aquí es precisamente donde entra en juego el método Filtered Approximate Nearest Neighbor (Filtered ANN):
- Tu capa ANN primero encuentra artículos/alimentos semánticamente similares que “se parecen” al chocolate negro para untar en cuanto a perfil de sabor, textura o categoría.
- A continuación, tu capa de filtrado interviene para eliminar cualquier producto con alto contenido en azúcar, mantener los productos que superan un determinado umbral de proteínas y, tal vez, aplicar preferencias dietéticas (vegano, cetogénico, sin frutos secos).
¿El resultado? Un motor de recomendaciones que entiende tanto el significado como las limitaciones, igual que un vendedor inteligente que conoce tus gustos y tiene en cuenta tus objetivos.
Antes de llegar a FANN, desarrollemos la intuición
NN (vecino más cercano): Encontrar el lo más parecido a lo que tienes. Es como preguntar: “¿Qué alimento de mi lista sabe más parecido a esta crema de chocolate para untar?”.”
ANN (vecino más cercano aproximado): Hallazgo algo muy similar, pero más rápido. Es como decir: “No necesito el perfecto coincidir, solo algo que sea lo suficientemente cerca rápidamente”.”
FANN (vecino más cercano filtrado): Hallazgo algo lo suficientemente cercano pero solo entre los artículos que cumplen ciertas reglas. Es como decir: “Muéstrame alimentos similares a la crema de chocolate para untar, pero solo aquellos que sean bajos en azúcar y ricos en proteínas”.”

Los algoritmos ANN intercambian un poco de eficacia (precisión) para un mayor eficacia (velocidad y memoria).
A índice compuesto es un índice creado a partir de varios campos (columnas) juntos, no solo uno. Para ePor ejemplo, es como ordenar una hoja de cálculo primero por Categoría, luego por Azúcares y después por Proteínas. Este método de ordenación agrupa primero todas las cremas de chocolate para untar. Dentro de ese grupo, puedes encontrar rápidamente productos bajos en azúcar y altos en proteínas sin tener que revisar todo.
Por qué fallan los índices tradicionales
Supongamos que tienes un pequeño subconjunto del conjunto de datos World Food Facts cargado en la memoria como:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
type Food struct { ID string ProductName string Category string Description string Sugars100g float64 Proteins100g float64 Tags []string Ingredients []string ... Country string } |
Para encontrar alimentos como cremas para untar de chocolate negro con bajo contenido en azúcar y alto contenido en proteínas, puedes utilizar una consulta como la siguiente:
|
1 2 3 4 |
SELECT product_name FROM food WHERE category = "chocolate_spread" AND sugars_100g < 20 AND proteins_100g > 10; |
Para acelerar la consulta, puede utilizar un índice secundario compuesto como el que se muestra a continuación:
|
1 |
CREATE INDEX idx_food ON food(category, sugars_100g, proteins_100g, product_name) |
Los índices secundarios compuestos pueden considerarse como listas ordenadas de claves concatenadas que permiten búsquedas más rápidas de valores específicos o iteraciones en un rango de valores bajos a altos (es decir, exploración de rango). Estos valores de búsqueda, así como los valores altos y bajos, se construyen en el momento de la consulta utilizando los predicados de consulta.
|
1 2 3 4 5 6 7 8 9 |
... ("almond_butter", 15, 20, "Almond butter with chocolate chips") ("chocolate_spread", 19, 7, "Chocolate spread with nuts") ("chocolate_spread", 20, 4, "Creamy chocolate spread") ("chocolate_spread", 23, 6, "Chocolate spread with honey") ("chocolate_spread", 25, 5, "Coffee chocolate spread") ("milk_chocolate", 4, 6, "Milk chocolate spread") ("peanut_butter", 19, 30, "Chocolate flavored peanut butter") ... |
Los índices compuestos funcionan muy bien para búsquedas estructuradas.
Pero un filtro de categoría nunca puede encontrar:
- mantequillas de nueces con sabor a chocolate
- cremas para untar de chocolate y proteína
- mezclas de cacao y avellana
- barras de proteína de chocolate
... aunque los humanos saben al instante que son parientes de las cremas de chocolate para untar.
Los índices tradicionales solo coinciden con la estructura, no con el significado. Por eso fallan los escaneos de rango basados en categorías.
Cómo funciona la ANN filtrada
Puede convertir la consulta y los datos en vectores.
La frase del usuario se introduce en un modelo de incrustación (por ejemplo, OpenAI, Cohere o un modelo específico del dominio).
El resultado es un vector denso que captura conceptos como:
- sabor similar al chocolate
- textura untable
- categoría de postres/aperitivos
Este vector representa lo que el usuario quiere, en lugar de solo las palabras literales.
A continuación, puede encontrar los vecinos más cercanos (similitud semántica).
Los candidatos podrían ser:
- Crema de avellanas y cacao para untar
- Mantequilla de chocolate y almendras
- Crema para untar de proteína de cacao
- Tahini de chocolate
Pero no todas son opciones saludables, y el usuario pidió específicamente bajas en azúcar y altas en proteínas.
Puede aplicar filtros estrictos, que es la parte “filtrada” de la ANN filtrada.
Puede filtrar los artículos:
- Azúcar > umbral (por ejemplo, >5 g por porción)
- Proteínas < umbral (por ejemplo, <8 g por porción)
Su sistema también puede combinar filtros de metadatos:
- Solo vegano
- Sin aceite de palma
- Sin nueces
- Menos de $10
Lo que queda es un conjunto de elementos que coinciden tanto en significado como en restricciones.
Por qué no funciona utilizar únicamente filtros
Si solo utilizas filtros, obtendrás:
- Cualquier producto con alto contenido en proteínas y bajo contenido en azúcar.
- Además de productos no relacionados con el chocolate (como tofu, yogur griego, pechuga de pollo).
Pero el usuario quiere algo “similar a la crema de chocolate para untar”.”
ANN filtrada = Personalización + Restricciones. Imita la forma en que un empleado de una tienda respondería a la solicitud: “Si quieres algo parecido a la crema de chocolate para untar, pero más saludable, prueba esto...”.”
Sin embargo, entre bastidores, su motor de recomendaciones se enfrenta a un problema sutil pero grave. Las bases de datos vectoriales modernas afirman que pueden realizar “búsquedas híbridas”, pero suelen mantener los campos escalares, como el azúcar o las proteínas, aparte, como metadatos simples. El índice ANN no sabe cómo utilizarlos.
¿Y qué pasa entonces?
El sistema primero recopila un gran lote de candidatos similares en cuanto a vectores... y solo entonces comienza a verificar las reglas nutricionales, como azúcares_100g 10.
Es como si un empleado de una tienda sacara todos los productos relacionados con el chocolate del almacén, los colocara en el mostrador y luego dijera:
“Espera, ¿querías bajo en azúcar? ¿Alto en proteínas? Déjame tirar la mayoría de estos”.”
Algunos sistemas vectoriales intentan filtrar antes durante el recorrido del gráfico, pero siguen sin poder realizar un filtrado de rango real ni una poda de prefijos. Deben recuperar y decodificar cada candidato antes de decidir si descartarlo.
¿Qué significa esto para tu aplicación?
- Más lecturas de disco
- Más cálculos de distancia
- Más latencia
... Y mucho trabajo desperdiciado para obtener resultados que el usuario nunca verá.
Es precisamente por eso que un índice vectorial compuesto que fusiona la similitud vectorial y la poda escalar en un mismo índice supone un cambio revolucionario.
Índices vectoriales compuestos: descripción general
Paso 1: Capa de incrustaciones: crear incrustaciones vectoriales
La descripción textual de cada producto (etiquetas, nombre del producto, categoría, ingredientes) se convierte en un vector de alta dimensión utilizando un modelo de lenguaje. Los productos con significados similares tendrán vectores similares.
Por ejemplo, incrustaciones para nombres de productos:
- “crema de chocolate negro para untar” → [0,23; -0,15; 0,87; …] (384 dimensiones)
- “mantequilla de chocolate y avellanas” → [0.25, -0.12, 0.85, …] (vector similar)
- “barrita proteica de chocolate” → [0,18; -0,08; 0,79; …] (algo similar)
Paso 2: Índice FANN: Crear un índice vectorial compuesto
Cree un índice vectorial (por ejemplo, Couchbase Vector Index, FAISS) que pueda encontrar rápidamente los vecinos más cercanos en el espacio de incrustación.
¿En qué se diferencian los vectores de otros tipos de datos en un índice vectorial compuesto?
- Los vectores no tienen un orden total natural, por lo que el orden de clasificación de los campos vectoriales no se puede determinar en el momento de la construcción del índice.
- Los campos vectoriales no admiten predicados de comparación convencionales (como filtros de igualdad o rango) en la cláusula WHERE.
- Sin embargo, los campos vectoriales se utilizan en ORDER BY con funciones de distancia vectorial y pueden participar en la planificación de consultas a través de esas expresiones.
- El orden se establece en el momento del escaneo utilizando la similitud con un vector de consulta. El usuario elige la función de similitud según las necesidades de los datos y la aplicación.
- APPROX_VECTOR_DISTANCE se puede utilizar en la cláusula ORDER BY y se admite de manera eficiente cuando existe un índice vectorial compatible; de lo contrario, da como resultado un escaneo completo.
- Dado que cada dimensión de un vector no tiene ningún significado por sí sola, solo se pueden formular preguntas como “¿qué grado de similitud hay entre dos vectores?”. Por lo tanto, solo se pueden encontrar los elementos más cercanos o similares.
- Del mismo modo, la función y la consulta deben proporcionarse como entrada en el momento de la consulta.
- La búsqueda del vecino más cercano es un problema que requiere un gran esfuerzo computacional y que solo empeora con el aumento de las dimensiones del vector. Por lo tanto, se necesita una solución eficiente en tiempo y espacio para obtener resultados aproximados.
- Los métodos de cuantificación se proporcionan en la descripción del DDL.
- Tendrás que reducir el número de comparaciones en el momento de la consulta para que esta sea más rápida.
- El número de centroides y el valor nprobes ayudan a reducir el espacio de búsqueda.
El índice vectorial compuesto es un índice en el que al menos una de las claves tiene un atributo vectorial, mientras que otros atributos como la dimensión, la similitud y la descripción, entre otros, se proporcionan para calificar el vector.
|
1 |
CREATE INDEX idx_vec ON food(sugars_100g, proteins_100g, text_vector Vector, product_name) WITH { "dimension": 384, "similarity" : "L2", "description": "IVF,SQ8" } |
En esta definición, la palabra clave VECTOR marca explícitamente text_vector como un atributo vectorial. Esto es necesario porque, a nivel JSON, una incrustación vectorial se almacena como una simple matriz de números de punto flotante. Sin la anotación vectorial, GSI trataría el campo como una matriz ordinaria y aplicaría la semántica de indexación estándar.
Al declarar un campo como vector, el usuario establece un contrato explícito con el servicio GSI que:
- El índice contendrá una única clave vectorial, y esa clave representa la incrustación utilizada para la búsqueda de similitud vectorial en este índice.
- La aplicación es responsable de generar la incrustación vectorial (por ejemplo, utilizando un modelo de incrustación externo) y conservarla en el campo del documento especificado.
- El servicio GSI debe interpretar el campo semánticamente como una incrustación vectorial y crear estructuras de índice sensibles al vector optimizadas para la búsqueda del vecino más cercano aproximado (ANN), en lugar de utilizar la lógica convencional de indexación escalar o de matriz.
En el índice vectorial DDL, el usuario debe especificar algunos parámetros adicionales, como:
- Dimensión: longitud de las incrustaciones vectoriales creadas.
- Similitud: métrica utilizada para la búsqueda ANN.
- Descripción: Índice FAISS similar a la descripción para especificar la relación entre precisión y velocidad.
En el ejemplo anterior:
- Creamos las incrustaciones de 384 dimensiones para los campos de etiquetas, nombre del producto, categoría e ingredientes utilizando el modelo sentence-transformers/all-MiniLM-L6-v2 y las almacenamos en el campo text_vector del documento.
- Utilizamos un cuantificador grueso IVF con un número predeterminado de centroides y cuantificación SQ8.
Paso 3: Consulta ANN filtrada
En lugar de filtrar por categoría exacta, nosotros:
- Genera una incrustación para la consulta “crema de chocolate negro para untar”.”
- query_text = “crema de chocolate negro para untar”
- query_embedding = [0.23, -0.15, 0.87, 0.42, …, -0.31] # Vector de 384 dimensiones
- Busca los k productos más similares utilizando la búsqueda ANN (por ejemplo, los 10 primeros) que cumplan nuestros criterios (azúcares_100g 10).
- Devuelve los resultados más relevantes.
Ejemplo de SQL++ (Couchbase):
|
1 2 3 4 5 |
SELECT product_name FROM food WHERE sugars_100g < 20 AND proteins_100g > 10 ORDER BY APPROX_VECTOR_DISTANCE(text_vector, [query_embedding], 'L2') LIMIT 10; |
Ventajas clave
Este enfoque encuentra productos que son:
- Semánticamente similar a “crema para untar de chocolate negro” (utilizando búsqueda vectorial).
- Conoce los filtros nutricionales (bajo contenido en azúcar, alto contenido en proteínas).
- Puede incluir productos de diferentes categorías, como “barritas proteicas de chocolate”, “cremas para untar de mantequilla de frutos secos” o “aperitivos con sabor a chocolate”, que son similares en significado, pero no coinciden con el filtro de la categoría “cremas para untar de chocolate”.
Obtenga más información sobre los índices vectoriales compuestos en la siguiente parte de esta serie, donde responderemos a preguntas prácticas como:
- ¿Cómo se almacenan y organizan de manera eficiente las incrustaciones vectoriales dentro de la capa de índice?
- ¿Puede un índice vectorial compuesto responder a consultas solo escalares sin leer el documento completo?
- ¿Importa el orden de los campos escalares y los campos vectoriales en la definición del índice?
Profundiza en la mecánica de la indexación vectorial compuesta consultando el segunda publicación en esta serie, donde exploramos su implementación dentro de Couchbase.