Arquitectura de Búsqueda Vectorial: Construyendo Sistemas de Búsqueda por Similaridad Listos para Producción
Guia tecnica para sistemas de busqueda vectorial. Aprende bases de datos vectoriales, algoritmos de indexacion (HNSW, IVF) y estrategias de escalado.
Por Qué la Búsqueda Vectorial Importa Ahora
Aquí está el problema con la búsqueda tradicional: solo encuentra coincidencias exactas. Buscas "zapatillas de correr" y te pierdes todos los resultados sobre "tenis para jogging" porque las palabras no coinciden. La búsqueda vectorial resuelve esta limitación fundamental al entender significado, no solo coincidir palabras clave.
Hemos desplegado sistemas de búsqueda vectorial para descubrimiento de productos en e-commerce, recuperación de documentos en pipelines RAG, y motores de recomendación manejando millones de consultas por día. La tecnología ha madurado significativamente en los últimos dos años, y las herramientas están finalmente listas para producción.
La búsqueda vectorial no se trata solo de encontrar elementos similares. Se trata de construir sistemas que entienden lo que los usuarios realmente quieren decir, no solo lo que escriben.
Déjame mostrarte cómo construir estos sistemas correctamente, desde elegir la base de datos correcta hasta escalar para tráfico real.
Cómo Funciona Realmente la Búsqueda Vectorial
Antes de sumergirnos en bases de datos y algoritmos, entendamos qué estamos haciendo realmente. La búsqueda vectorial funciona en tres pasos:
Paso 1: Crear Embeddings
Tomas tus datos (texto, imágenes, productos, lo que sea) y los conviertes en vectores usando un modelo de embedding. Estos vectores son arrays de números que capturan el significado semántico de tu contenido.
// Usando embeddings de OpenAI
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: "zapatillas cómodas para maratones"
});
const vector = response.data[0].embedding;
// Retorna: [0.0023, -0.0142, 0.0089, ...] (1536 dimensiones)
Paso 2: Almacenar e Indexar Vectores
Almacenas estos vectores en una base de datos especializada que construye un índice para recuperación rápida. Aquí es donde ocurre la magia con algoritmos como HNSW e IVF.
Paso 3: Consultar por Similaridad
Cuando un usuario busca, conviertes su consulta en un vector y encuentras los vecinos más cercanos en tu índice. La base de datos retorna los elementos más similares ordenados por distancia.
| Componente | Propósito | Ejemplo |
|---|---|---|
| Modelo de Embedding | Convierte datos a vectores | OpenAI ada-002, Cohere embed-v3 |
| Base de Datos Vectorial | Almacena e indexa vectores | Pinecone, Weaviate, Qdrant |
| Métrica de Distancia | Mide similaridad | Cosine, Euclidean, Dot Product |
| Algoritmo ANN | Búsqueda aproximada rápida | HNSW, IVF, PQ |
Eligiendo una Base de Datos Vectorial
Comparemos las opciones principales. Voy a ser honesto sobre los trade-offs porque cada base de datos destaca en diferentes escenarios.
Pinecone: Simplicidad Gestionada
Pinecone es la más fácil para empezar. Completamente gestionada, escala automáticamente, y simplemente funciona. ¿El inconveniente? Estás atado a su infraestructura y los precios pueden subir bastante a escala.
import { Pinecone } from '@pinecone-database/pinecone';
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });
const index = pinecone.index('products');
// Upsert de vectores
await index.upsert([
{
id: 'product-123',
values: embedding,
metadata: { category: 'zapatillas', price: 129.99 }
}
]);
// Consulta con filtrado de metadatos
const results = await index.query({
vector: queryEmbedding,
topK: 10,
filter: { category: { $eq: 'zapatillas' } },
includeMetadata: true
});
Ideal para: Equipos que quieren lanzar rápido sin gestionar infraestructura. Startups con financiación. Casos de uso bajo 10M de vectores donde el costo no es la preocupación principal.
Weaviate: Schema-First con GraphQL
Weaviate toma un enfoque diferente con su diseño basado en esquema y API GraphQL. Defines clases de objetos con propiedades, y Weaviate maneja la vectorización automáticamente si quieres.
import weaviate from 'weaviate-ts-client';
const client = weaviate.client({
scheme: 'https',
host: 'your-cluster.weaviate.network'
});
// Definir esquema
await client.schema.classCreator().withClass({
class: 'Product',
vectorizer: 'text2vec-openai',
properties: [
{ name: 'name', dataType: ['text'] },
{ name: 'description', dataType: ['text'] },
{ name: 'price', dataType: ['number'] }
]
}).do();
// Consulta con GraphQL
const result = await client.graphql
.get()
.withClassName('Product')
.withNearText({ concepts: ['zapatillas de maratón'] })
.withLimit(10)
.withFields('name description price _additional { distance }')
.do();
Ideal para: Equipos construyendo knowledge graphs, aplicaciones que necesitan búsqueda híbrida (palabra clave + vector), proyectos donde importa el enforcement del esquema.
Qdrant: Enfocado en Rendimiento y Open Source
Qdrant está escrito en Rust y optimizado para rendimiento. Es open source, puede auto-alojarse, y tiene un excelente sistema de filtrado. Lo hemos visto manejar 50M+ vectores con latencias sub-100ms en hardware modesto.
import { QdrantClient } from '@qdrant/js-client-rest';
const client = new QdrantClient({ url: 'http://localhost:6333' });
// Crear colección con configuración optimizada
await client.createCollection('products', {
vectors: {
size: 1536,
distance: 'Cosine'
},
optimizers_config: {
indexing_threshold: 20000
},
hnsw_config: {
m: 16,
ef_construct: 100
}
});
// Upsert con payload
await client.upsert('products', {
points: [
{
id: 'product-123',
vector: embedding,
payload: { category: 'zapatillas', price: 129.99, in_stock: true }
}
]
});
// Consulta con filtros complejos
const results = await client.search('products', {
vector: queryEmbedding,
limit: 10,
filter: {
must: [
{ key: 'category', match: { value: 'zapatillas' } },
{ key: 'price', range: { lte: 150 } },
{ key: 'in_stock', match: { value: true } }
]
}
});
Ideal para: Despliegues auto-alojados, proyectos sensibles a costos a escala, aplicaciones que necesitan filtrado complejo, equipos que quieren control sobre la infraestructura.
pgvector: Búsqueda Vectorial en PostgreSQL
Si ya estás corriendo PostgreSQL, pgvector te permite añadir búsqueda vectorial sin otra base de datos. No es la opción más rápida, pero suele ser suficientemente rápida y simplifica drásticamente tu arquitectura.
-- Habilitar extensión
CREATE EXTENSION vector;
-- Crear tabla con columna vectorial
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
description TEXT,
category TEXT,
price NUMERIC,
embedding VECTOR(1536)
);
-- Crear índice HNSW para consultas más rápidas
CREATE INDEX ON products
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- Consultar vecinos más cercanos
SELECT id, name, price,
1 - (embedding <=> query_embedding) AS similarity
FROM products
WHERE category = 'zapatillas' AND price < 150
ORDER BY embedding <=> query_embedding
LIMIT 10;
Ideal para: Equipos ya en PostgreSQL, aplicaciones bajo 5M de vectores, casos de uso donde necesitas transacciones ACID con búsqueda vectorial, reducir la complejidad operacional.
Comparación de Bases de Datos Vectoriales
| Característica | Pinecone | Weaviate | Qdrant | pgvector |
|---|---|---|---|---|
| Despliegue | Solo gestionado | Gestionado + Auto-alojado | Gestionado + Auto-alojado | Auto-alojado |
| Escala Máxima | Miles de millones | Cientos de millones | Cientos de millones | ~10 millones |
| Latencia Consulta | <50ms | <100ms | <50ms | <200ms |
| Filtrado | Bueno | Excelente | Excelente | SQL nativo |
| Curva de Aprendizaje | Fácil | Moderada | Fácil | Mínima si conoces SQL |
| Costo a Escala | Alto | Moderado | Bajo (auto-alojado) | Bajo |
| Búsqueda Híbrida | Limitada | Excelente | Buena | Via búsqueda full-text |
Entendiendo los Algoritmos de Indexación
El algoritmo de índice determina qué tan rápido puedes buscar en millones de vectores. Esto es lo que necesitas saber sobre los dos enfoques más comunes.
HNSW: Hierarchical Navigable Small World
HNSW es el algoritmo dominante para búsqueda vectorial. Construye un grafo multicapa donde las capas superiores tienen menos nodos y saltos más grandes, mientras que las capas inferiores tienen más nodos y conexiones más cortas. La búsqueda empieza arriba y baja.
| Parámetro | Qué controla | Trade-off |
|---|---|---|
m | Número de conexiones por nodo | Mayor = mejor recall, más memoria |
ef_construct | Amplitud de búsqueda durante construcción | Mayor = mejor calidad, indexación más lenta |
ef_search | Amplitud de búsqueda durante consultas | Mayor = mejor recall, consultas más lentas |
// Configuración HNSW de Qdrant
const hnswConfig = {
m: 16, // 16 conexiones por nodo (default)
ef_construct: 100, // Calidad de construcción
full_scan_threshold: 10000 // Cambiar a fuerza bruta por debajo de esto
};
// Para requisitos de alto recall
const highRecallConfig = {
m: 32,
ef_construct: 200
};
// Para entornos limitados en memoria
const lowMemoryConfig = {
m: 8,
ef_construct: 50
};
Pros: Consultas rápidas, buen recall, funciona bien para la mayoría de casos de uso Contras: Intensivo en memoria, actualizaciones de índice lentas, no ideal para datos en streaming
IVF: Inverted File Index
IVF agrupa tus vectores en clusters y solo busca en clusters relevantes. Es más rápido de construir que HNSW y usa menos memoria, pero típicamente tiene menor recall.
| Parámetro | Qué controla | Trade-off |
|---|---|---|
nlist | Número de clusters | Mayor = más precisión, consultas más lentas |
nprobe | Clusters a buscar | Mayor = mejor recall, consultas más lentas |
# Ejemplo Faiss IVF
import faiss
dimension = 1536
nlist = 100 # Número de clusters
# Crear índice IVF con cuantizador plano
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# Entrenar con datos de muestra
index.train(training_vectors)
index.add(all_vectors)
# Buscar con nprobe
index.nprobe = 10 # Buscar 10 clusters
distances, indices = index.search(query_vector, k=10)
Pros: Eficiente en memoria, construcción de índice rápida, bueno para actualizaciones en streaming Contras: Menor recall que HNSW, requiere paso de entrenamiento, necesita más tuning
Product Quantization: Comprimiendo Vectores
Cuando la memoria es escasa, Product Quantization (PQ) comprime vectores dividiéndolos en subvectores y usando codebooks. Cambias recall por ahorro dramático de memoria.
# IVF + PQ para escala masiva
m = 8 # Número de subcuantizadores
bits = 8 # Bits por subvector
index = faiss.IndexIVFPQ(quantizer, dimension, nlist, m, bits)
# Reduce memoria ~32x comparado con índice plano
Usa PQ cuando: Tienes 100M+ vectores y no puedes permitirte almacenamiento de precisión completa. Espera 5-10% de pérdida de recall.
Métricas de Similaridad: Eligiendo la Distancia Correcta
La métrica de distancia determina cómo se calcula la similaridad. La elección importa más de lo que piensas.
| Métrica | Fórmula | Ideal Para |
|---|---|---|
| Cosine | 1 - (A.B / ||A|| ||B||) | Embeddings de texto, vectores normalizados |
| Euclidean (L2) | sqrt(sum((A-B)^2)) | Embeddings de imagen, features densos |
| Dot Product | A.B | Cuando la magnitud importa, recomendaciones |
Regla general: Si tus embeddings vienen de un modelo de texto (OpenAI, Cohere, etc.), usa similaridad coseno. Los vectores ya están normalizados, y coseno los maneja correctamente. Para embeddings de imagen o modelos custom, Euclidean suele funcionar mejor.
// La mayoría de APIs de embedding de texto retornan vectores normalizados
// Similaridad coseno = dot product para vectores normalizados
const cosineSimilarity = (a, b) => {
return a.reduce((sum, val, i) => sum + val * b[i], 0);
};
// Distancia euclidiana para vectores no normalizados
const euclideanDistance = (a, b) => {
return Math.sqrt(
a.reduce((sum, val, i) => sum + Math.pow(val - b[i], 2), 0)
);
};
Escalando la Búsqueda Vectorial para Producción
Aquí es donde la teoría se encuentra con la realidad. Déjame compartir lo que hemos aprendido escalando búsqueda vectorial para manejar millones de consultas.
Estrategias de Sharding
Cuando un solo nodo no puede contener todos tus vectores, necesitas shardear. Hay dos enfoques principales:
Sharding Geográfico: Dividir por región si las consultas están localizadas
shard_latam: Productos Latinoamérica (5M vectores)
shard_es: Productos España (3M vectores)
shard_global: Productos Globales (2M vectores)
Sharding por Hash: Distribuir uniformemente entre nodos
const shardId = hash(productId) % numShards;
Sharding por Metadatos: Dividir por categoría o atributo
shard_electronica: Productos Electrónica
shard_ropa: Productos Ropa
shard_hogar: Productos Hogar y Jardín
Replicación para Disponibilidad
Corre al menos 3 réplicas para cargas de trabajo de producción. La búsqueda vectorial es intensiva en lectura, así que las réplicas también ayudan con el throughput.
# Configuración de cluster Qdrant
cluster:
enabled: true
replication_factor: 3
shard_number: 6
# Pinecone (automático)
# Solo selecciona el tier "production" con múltiples pods
# Weaviate
replicationConfig:
factor: 3
Cacheando Consultas Frecuentes
Algunas consultas son mucho más comunes que otras. Cachéalas.
import { Redis } from 'ioredis';
const redis = new Redis();
const CACHE_TTL = 3600; // 1 hora
async function searchWithCache(query, filters) {
const cacheKey = `vsearch:${hash(query)}:${hash(filters)}`;
// Verificar cache primero
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Consultar base de datos vectorial
const embedding = await generateEmbedding(query);
const results = await vectorDB.search({
vector: embedding,
filter: filters,
limit: 20
});
// Cachear resultados
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(results));
return results;
}
Batching y Procesamiento Asíncrono
No proceses vectores de uno en uno. Haz batch de todo.
// Mal: Procesamiento secuencial
for (const doc of documents) {
const embedding = await generateEmbedding(doc.text);
await vectorDB.upsert({ id: doc.id, vector: embedding });
}
// Bien: Procesamiento por batch
const BATCH_SIZE = 100;
for (let i = 0; i < documents.length; i += BATCH_SIZE) {
const batch = documents.slice(i, i + BATCH_SIZE);
// Generar embeddings en paralelo
const embeddings = await Promise.all(
batch.map(doc => generateEmbedding(doc.text))
);
// Upsert en batch
await vectorDB.upsert(
batch.map((doc, j) => ({
id: doc.id,
vector: embeddings[j],
metadata: doc.metadata
}))
);
}
Ejemplo de Arquitectura de Producción
Aquí hay una arquitectura real que usamos para un sistema de búsqueda e-commerce manejando 10M+ productos y 1000+ consultas por segundo:
┌─────────────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ API Gateway │
│ (Rate limiting, auth, routing) │
└─────────────────────┬───────────────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌────▼────┐
│ Cache │ │ Search │ │ Search │
│ (Redis) │ │ Node 1 │ │ Node 2 │
└───────────┘ └────┬────┘ └────┬────┘
│ │
┌───────▼───────────▼───────┐
│ Cluster Qdrant │
│ (3 shards, 3 réplicas) │
└───────────────────────────┘
│
┌───────────▼───────────────┐
│ Servicio Embedding │
│ (Acelerado por GPU) │
└───────────────────────────┘
Componentes Clave
Search Nodes: Servicios stateless que manejan procesamiento de consultas, generación de embeddings y ranking de resultados. Corremos 2-4 instancias detrás de un load balancer.
Cluster Qdrant: 3 shards para distribución de datos, 3 réplicas para disponibilidad. Cada shard maneja ~3.5M vectores. Memoria total: ~50GB en todo el cluster.
Servicio Embedding: Servicio GPU dedicado para generar embeddings. Usamos modelos optimizados con ONNX para inferencia 10x más rápida que transformers vanilla.
Cache Redis: Cachea consultas comunes y embeddings frecuentes. Reduce la carga de Qdrant en ~60%.
Trampas Comunes y Cómo Evitarlas
Trampa 1: No Normalizar Vectores
Si estás usando similaridad coseno pero tus vectores no están normalizados, obtendrás resultados incorrectos. La mayoría de APIs de embedding retornan vectores normalizados, pero si usas un modelo custom, normalízalos tú mismo.
function normalize(vector) {
const magnitude = Math.sqrt(
vector.reduce((sum, val) => sum + val * val, 0)
);
return vector.map(val => val / magnitude);
}
Trampa 2: Ignorar el Modelo de Embedding
El modelo de embedding importa más que la base de datos. Un buen modelo con pgvector superará a un mal modelo con Pinecone. Invierte tiempo evaluando la calidad de los embeddings antes de optimizar la infraestructura.
Trampa 3: No Planificar Actualizaciones
La mayoría de bases de datos vectoriales están optimizadas para lecturas, no escrituras. Si necesitas actualizaciones frecuentes, diseña para ello:
- Usa write-ahead logs
- Haz batch de actualizaciones durante períodos de bajo tráfico
- Considera un índice de staging para nuevos datos
Trampa 4: Filtrar Demasiado Antes de la Búsqueda Vectorial
Filtrar después de la búsqueda vectorial es generalmente más eficiente que filtrar antes. Deja que el índice vectorial haga su trabajo, luego filtra los resultados.
// Menos eficiente: Filtrar primero, luego buscar en vectores restantes
// Más eficiente: Buscar todo, luego filtrar los mejores resultados
const results = await vectorDB.search({
vector: queryEmbedding,
limit: 100 // Obtener más resultados de los necesarios
});
const filtered = results.filter(r =>
r.metadata.price < maxPrice &&
r.metadata.in_stock
).slice(0, 10);
Monitoreo y Observabilidad
No puedes mejorar lo que no mides. Rastrea estas métricas:
| Métrica | Objetivo | Acción si se excede |
|---|---|---|
| Latencia p50 | <50ms | Verificar configuración del índice |
| Latencia p99 | <200ms | Añadir réplicas o shards |
| Recall@10 | >95% | Aumentar ef_search o m |
| QPS por nodo | <1000 | Añadir más nodos |
| Uso de memoria | <80% | Shardear o usar compresión PQ |
// Rastrear latencia de búsqueda
const startTime = Date.now();
const results = await vectorDB.search(query);
const latency = Date.now() - startTime;
metrics.histogram('vector_search_latency_ms', latency, {
collection: 'products',
filter_count: Object.keys(query.filter || {}).length
});
// Rastrear recall (requiere ground truth)
const recall = calculateRecall(results, groundTruth);
metrics.gauge('vector_search_recall', recall);
Empezando: Recomendaciones Prácticas
Si estás empezando desde cero, esto es lo que recomendaría:
-
Menos de 1M vectores: Empieza con pgvector. Es simple, probablemente suficientemente rápido, y ya conoces SQL.
-
1M-10M vectores: Qdrant auto-alojado te da el mejor rendimiento por dólar. Pinecone si no quieres gestionar infraestructura.
-
10M-100M vectores: Qdrant o Weaviate con sharding apropiado. Considera el tier enterprise de Pinecone si el presupuesto lo permite.
-
100M+ vectores: Necesitas arquitectura especializada. Considera Milvus, Qdrant multi-cluster, o soluciones custom con Faiss.
Empieza con índices HNSW (el default en la mayoría de bases de datos). Solo optimiza cuando encuentres problemas de rendimiento reales. La optimización prematura desperdicia tiempo en problemas que quizás no tengas.
Conclusión
La búsqueda vectorial ha pasado de curiosidad de investigación a necesidad de producción. Ya sea que estés construyendo búsqueda semántica, sistemas de recomendación, o pipelines RAG, entender estos fundamentos te ayudará a tomar mejores decisiones arquitectónicas.
La tecnología está madura, las herramientas son buenas, y la comunidad ha resuelto la mayoría de problemas comunes. Lo que queda es elegir las herramientas correctas para tu caso de uso específico e implementarlas de manera reflexiva.
Si estás construyendo sistemas de búsqueda vectorial y quieres discutir arquitectura, siempre estamos contentos de compartir lo que hemos aprendido de despliegues en producción.
Temas cubiertos
Guías relacionadas
Guía Empresarial de Sistemas de IA Agéntica
Guia tecnica de sistemas de IA agentica en entornos empresariales. Descubre la arquitectura, capacidades y aplicaciones de agentes IA autonomos.
Leer guíaComercio Agéntico: Cómo Dejar que los Agentes IA Compren de Forma Segura
Cómo diseñar comercio iniciado por agentes IA con gobernanza. Motores de políticas, puertas de aprobación HITL, recibos HMAC, idempotencia, aislamiento de tenants y el Agentic Checkout Protocol completo.
Leer guíaLos 9 Puntos Donde Tu Sistema de IA Filtra Datos (y Cómo Sellar Cada Uno)
Un mapa sistemático de cada lugar donde se filtran datos en sistemas de IA. Prompts, embeddings, logs, llamadas a herramientas, memoria de agentes, mensajes de error, caché, datos de fine-tuning y handoffs entre agentes.
Leer guía¿Listo para construir sistemas de IA listos para producción?
Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.
Iniciar una conversación