Ir al contenido
wannacode

Buscar por significado

En resumen: La búsqueda vectorial convierte el texto en un vector de números (un embedding) y encuentra los registros más cercanos en significado mediante la similitud del coseno.

Problema

«Algo para el desayuno»

Un cliente escribe «algo para el desayuno» en el buscador de la tienda. La palabra no está en ningún nombre de producto:

SQLbase de datos: tienda

La búsqueda de texto completo de la lección anterior encuentra solo la reseña «El pan integral es estupendo para el desayuno.» Y, sin embargo, la leche, un bollo, el café y la banitsa son justo lo que quiere el cliente. Busca significado, no una palabra. Los sinónimos, las traducciones y el «algo para…» no se atrapan con palabras.

La idea

Un texto como un punto

Un modelo de lenguaje (un modelo de embeddings) convierte un texto en un vector: una lista de números. Está entrenado para que los textos de significado cercano den vectores cercanos. «Leche», «bollo» y «algo para el desayuno» están cerca, y «detergente en polvo» está lejos.

Los modelos reales dan de unos cientos a unos miles de números, y ninguno tiene nombre. Aquí, para que veas lo que pasa, los vectores están inventados y tienen solo cuatro dimensiones con nombre: desayuno, dulce, cocinar y limpiar. La tabla product_vectors guarda el vector de cada producto como un array JSON:

SQLbase de datos: tienda

El bollo es muy «desayuno» y un poco «dulce». Las papas son solo «cocinar», y el detergente en polvo, solo «limpiar».

Más precisamente, para curiosos

El vector se calcula una vez, cuando se añade el producto o cambia su descripción, y se guarda en la base de datos. La pregunta del cliente se convierte en un vector con el mismo modelo en cada búsqueda. Los dos vectores tienen que venir del mismo modelo: los vectores de modelos distintos no se pueden comparar. Hay modelos abiertos (por ejemplo, de la familia sentence-transformers, que funcionan en una computadora corriente) y servicios de pago.

La idea

Qué tan cerca: el coseno

Dos vectores están cerca cuando apuntan en la misma dirección. La medida es la similitud del coseno: 1 significa la misma dirección, 0 significa nada en común. Se calcula así: multiplicas los números de las mismas posiciones y los sumas (el producto escalar), y luego divides por las longitudes de los dos vectores.

json_each despliega un array JSON en filas: key es la posición y value es el número. El queso blanco frente a la pregunta «algo para el desayuno» ([1, 0.2, 0.1, 0], como la daría el modelo):

SQLbase de datos: tienda

La suma de la última columna es 0,66. Dividida por las longitudes (0,849 del queso y 1,025 de la pregunta) da 0,759. Lo mismo para todos los productos, del más cercano hacia abajo:

SQLbase de datos: tienda

WITH q(v) da el vector de la pregunta. Los dos json_each se unen por la posición, y GROUP BY suma los productos de cada elemento. Ninguno de estos productos tiene la palabra «desayuno» en el nombre.

Pruébalo

Algo dulce para el desayuno

El modelo ha convertido la pregunta «algo dulce para el desayuno» en el vector [0.7, 0.9, 0, 0]. Encuentra los tres productos más cercanos (las columnas name y similarity, redondeadas a 0.001), del más cercano hacia abajo.

SQLbase de datos: tienda

La idea

Filtrar antes de ordenar

La búsqueda por significado a menudo tiene también condiciones normales: por debajo de un precio, solo con stock, solo en una categoría. La condición va en WHERE, es decir, antes de GROUP BY, ORDER BY y LIMIT. Si primero tomas los tres más cercanos y después filtras, puede no quedar nada: son café por 14,90 €, banitsa por 2,50 € y leche por 1,60 €.

Más precisamente, para curiosos

Los índices vectoriales (la siguiente tarjeta) tienen justo este problema. La búsqueda aproximada devuelve, por ejemplo, los 100 más cercanos, y si el filtro viene después y los productos baratos no están entre esos 100, el resultado queda vacío. Por eso las bases de datos vectoriales tienen filtros que se aplican durante la búsqueda, y pgvector tiene ajustes como hnsw.ef_search y escaneos iterativos (hnsw.iterative_scan).

Ejercicio

Barato y parecido

1. El cliente ha elegido «hasta 1,50 €».

Encuentra los tres productos más cercanos a «algo para el desayuno» ([1, 0.2, 0.1, 0]) con un precio inferior a 1,50 € (las columnas name y similarity), del más cercano hacia abajo.

SQLbase de datos: tienda

2. La página del producto muestra «productos similares». El vector de la pregunta es el vector del propio producto.

Encuentra los tres productos más parecidos al queso blanco (producto 7), sin el propio queso (las columnas name y similarity), del más cercano hacia abajo.

SQLbase de datos: tienda

La idea

Millones de vectores: un índice aproximado

La consulta compara la pregunta con cada producto. El plan muestra SCAN p:

SQLbase de datos: tienda

Con 23 productos de 4 números cada uno, esto no es nada. Con 10 millones de documentos de 1536 números cada uno, cada búsqueda son 15 mil millones de multiplicaciones. Un índice normal no ayuda: ordena por un valor, mientras que «cerca» en vectores es en cientos de direcciones a la vez.

Por eso las bases de datos vectoriales usan índices aproximados (ANN, approximate nearest neighbours, vecinos más cercanos aproximados). El más extendido es HNSW: un grafo en el que cada vector está unido a sus cercanos, en varios niveles. La búsqueda empieza en el nivel de arriba, el más disperso, y va bajando, pasando siempre a vecinos más cercanos. Encuentra los más cercanos en milisegundos, pero a veces pierde alguno. La proporción de los verdaderos más cercanos que encuentra el índice es el recall, y se ajusta frente a la velocidad.

La idea

Dónde viven los vectores

Lo más sencillo es que los vectores estén junto a los datos. pgvector es una extensión de PostgreSQL:

CREATE EXTENSION vector;
CREATE TABLE product_vectors (
product_id integer PRIMARY KEY REFERENCES products(id),
embedding vector(384)
);
CREATE INDEX ON product_vectors USING hnsw (embedding vector_cosine_ops);
-- <=> es la distancia del coseno (1 menos la similitud)
SELECT p.name
FROM product_vectors pv JOIN products p ON p.id = pv.product_id
WHERE p.price < 1.5
ORDER BY pv.embedding <=> $1
LIMIT 3;

Las bases de datos vectoriales aparte (Qdrant, Weaviate, Milvus, Pinecone) compensan con cientos de millones de vectores o cuando la búsqueda por significado es la tarea principal. Elasticsearch, OpenSearch y Redis también tienen búsqueda vectorial.

El uso más común hoy es RAG (retrieval-augmented generation, generación aumentada con recuperación): el chatbot de la tienda convierte la pregunta en un vector, encuentra los fragmentos más cercanos de descripciones, reseñas y reglas y se los da al modelo de lenguaje, para que responda a partir de ellos y no de memoria. La búsqueda por significado se mezcla a menudo con la de texto completo: las palabras atrapan nombres y códigos exactos, y los vectores, el significado.

Test

Ponte a prueba

1. ¿Qué es un embedding?
2. La similitud del coseno de dos vectores es 1. ¿Qué significa?
3. Buscas los tres productos más cercanos por menos de 1,50 €. ¿Por qué el filtro de precio no se aplica después de LIMIT 3?
4. ¿Qué aporta un índice HNSW en una base de datos vectorial?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·