Guía técnica

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.

10 de febrero de 202618 min de lecturaEquipo de Ingeniería Oronts

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.

ComponentePropósitoEjemplo
Modelo de EmbeddingConvierte datos a vectoresOpenAI ada-002, Cohere embed-v3
Base de Datos VectorialAlmacena e indexa vectoresPinecone, Weaviate, Qdrant
Métrica de DistanciaMide similaridadCosine, Euclidean, Dot Product
Algoritmo ANNBúsqueda aproximada rápidaHNSW, 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ísticaPineconeWeaviateQdrantpgvector
DespliegueSolo gestionadoGestionado + Auto-alojadoGestionado + Auto-alojadoAuto-alojado
Escala MáximaMiles de millonesCientos de millonesCientos de millones~10 millones
Latencia Consulta<50ms<100ms<50ms<200ms
FiltradoBuenoExcelenteExcelenteSQL nativo
Curva de AprendizajeFácilModeradaFácilMínima si conoces SQL
Costo a EscalaAltoModeradoBajo (auto-alojado)Bajo
Búsqueda HíbridaLimitadaExcelenteBuenaVia 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ámetroQué controlaTrade-off
mNúmero de conexiones por nodoMayor = mejor recall, más memoria
ef_constructAmplitud de búsqueda durante construcciónMayor = mejor calidad, indexación más lenta
ef_searchAmplitud de búsqueda durante consultasMayor = 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ámetroQué controlaTrade-off
nlistNúmero de clustersMayor = más precisión, consultas más lentas
nprobeClusters a buscarMayor = 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étricaFórmulaIdeal Para
Cosine1 - (A.B / ||A|| ||B||)Embeddings de texto, vectores normalizados
Euclidean (L2)sqrt(sum((A-B)^2))Embeddings de imagen, features densos
Dot ProductA.BCuando 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étricaObjetivoAcción si se excede
Latencia p50<50msVerificar configuración del índice
Latencia p99<200msAñadir réplicas o shards
Recall@10>95%Aumentar ef_search o m
QPS por nodo<1000Añ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:

  1. Menos de 1M vectores: Empieza con pgvector. Es simple, probablemente suficientemente rápido, y ya conoces SQL.

  2. 1M-10M vectores: Qdrant auto-alojado te da el mejor rendimiento por dólar. Pinecone si no quieres gestionar infraestructura.

  3. 10M-100M vectores: Qdrant o Weaviate con sharding apropiado. Considera el tier enterprise de Pinecone si el presupuesto lo permite.

  4. 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

búsqueda vectorialbase de datos vectorialPineconeWeaviateQdrantpgvectorHNSWIVFbúsqueda por similaridadembeddingsbúsqueda semánticaANNapproximate nearest neighbor

¿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