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:
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:
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):
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:
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.
Solución
WITH q(v) AS (SELECT '[0.7, 0.9, 0, 0]')
SELECT p.name,
ROUND(SUM(a.value * b.value)
/ (sqrt(SUM(a.value * a.value)) * sqrt(SUM(b.value * b.value))), 3) AS similarity
FROM product_vectors pv
JOIN products p ON p.id = pv.product_id
CROSS JOIN q
JOIN json_each(pv.embedding) a
JOIN json_each(q.v) b ON b.key = a.key
GROUP BY p.id, p.name
ORDER BY similarity DESC
LIMIT 3;La misma consulta con '[0.7, 0.9, 0, 0]' en WITH q(v). Con ORDER BY similarity sin DESC salen los más lejanos: los productos de limpieza y de cocina, con similitud 0. Y el vector de «algo para el desayuno» da café y banitsa, que no son dulces.
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.
Solución
WITH q(v) AS (SELECT '[1, 0.2, 0.1, 0]')
SELECT p.name,
ROUND(SUM(a.value * b.value)
/ (sqrt(SUM(a.value * a.value)) * sqrt(SUM(b.value * b.value))), 3) AS similarity
FROM product_vectors pv
JOIN products p ON p.id = pv.product_id
CROSS JOIN q
JOIN json_each(pv.embedding) a
JOIN json_each(q.v) b ON b.key = a.key
WHERE p.price < 1.5
GROUP BY p.id, p.name
ORDER BY similarity DESC
LIMIT 3;WHERE p.price < 1.5 antes de GROUP BY. Si primero tomas los tres más cercanos y después filtras por precio, no queda ninguno: el café, la banitsa y la leche pasan de 1,50 €.
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.
Solución
WITH q(v) AS (SELECT embedding FROM product_vectors WHERE product_id = 7)
SELECT p.name,
ROUND(SUM(a.value * b.value)
/ (sqrt(SUM(a.value * a.value)) * sqrt(SUM(b.value * b.value))), 3) AS similarity
FROM product_vectors pv
JOIN products p ON p.id = pv.product_id
CROSS JOIN q
JOIN json_each(pv.embedding) a
JOIN json_each(q.v) b ON b.key = a.key
WHERE p.id <> 7
GROUP BY p.id, p.name
ORDER BY similarity DESC
LIMIT 3;WITH q(v) AS (SELECT embedding FROM product_vectors WHERE product_id = 7) y WHERE p.id <> 7. Sin la condición, el primer lugar es para el propio queso, con similitud 1.
La idea
Millones de vectores: un índice aproximado
La consulta compara la pregunta con cada producto. El plan muestra SCAN p:
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.nameFROM product_vectors pv JOIN products p ON p.id = pv.product_idWHERE p.price < 1.5ORDER BY pv.embedding <=> $1LIMIT 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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Un informe por pasos: WITH · Índices · EXPLAIN: el plan de la consulta · Buscar en el texto
- Continúa en: Cómo elegir una base de datos
- Ejercicios sobre el tema: Tipos de bases de datos