El año pasado evalué cuatro bases de datos vectoriales
para un sistema RAG en producción. Leí todos los
benchmarks, asistí a tres demos de proveedores y construí
integraciones de prueba de concepto con Pinecone, Weaviate
y Qdrant.
Después lancé pgvector en la instancia de Postgres que ya
teníamos corriendo. Me tomó una tarde.
Seis meses después, ese sistema atiende 50,000 búsquedas
semánticas por día con latencia p95 por debajo de 40ms.
El costo total de infraestructura para búsqueda vectorial
es $0 adicionales al mes porque corre en la misma base de
datos que almacena usuarios, sesiones y datos de la
aplicación.
El Ciclo de Hype de las Bases de Datos Vectoriales
El capital de riesgo invirtió más de $350M en startups de
bases de datos vectoriales en 2023-2024. Pinecone levantó
$138M. Weaviate levantó $50M. Qdrant levantó $28M. Ese
dinero necesita justificarse, lo que significa presupuestos
de marketing dirigidos directamente a convencerte de que
Postgres no puede manejar tus embeddings.
Así suena el pitch: "Necesitas una base de datos vectorial
construida a propósito para IA en producción. Postgres no
fue diseñado para búsqueda de similitud en alta dimensión.
Vas a chocar contra muros de escalabilidad."
Ese pitch no está equivocado para todos. Está equivocado
para cerca del 90% de los equipos construyendo features de
IA hoy.
La mayoría de los sistemas RAG en producción tienen menos
de 5M de vectores. La mayoría de los volúmenes de consulta
están por debajo de 100 QPS. La mayoría de los requisitos
de latencia son "menos de 200ms." Si eso describe tu
sistema, eres el cliente objetivo de un servicio de
$200K/año que no necesitas.
Lo Básico de pgvector
pgvector es una extensión de Postgres que agrega tipos de
datos vectoriales y operadores de búsqueda por similitud.
Lo habilitas con una línea:
CREATE EXTENSION IF NOT EXISTS vector;
Eso te da un tipo de columna vector, tres operadores de
distancia (<-> para L2, <=> para coseno, <#> para
producto interno), y tres estrategias de indexación.
Flat Scan (Sin Índice)
Sin índice. Postgres escanea cada fila y calcula la
distancia. 100% de recall, pero rendimiento O(n).
Funciona para tablas con menos de 10,000 filas. Lo uso
para desarrollo y tablas de consulta pequeñas.
IVFFlat
Índice de archivo invertido. Particiona vectores en
clusters (llamados "lists"), luego busca solo en los
clusters más cercanos al momento de la consulta. Tú
eliges el número de lists y cuántas explorar.
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Buen recall a velocidad razonable. La trampa: necesitas
construir el índice después de insertar tus datos, y la
calidad depende de elegir el número correcto de lists.
Regla general: lists = filas / 1000 para hasta 1M de
filas.
HNSW
Hierarchical Navigable Small World graph. Este es el que
quieres para producción. Construye una estructura de grafo
multicapa que permite búsqueda en tiempo logarítmico con
recall configurable.
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
HNSW tarda más en construirse pero ofrece mejor
rendimiento de consulta que IVFFlat y no requiere
reconstrucción después de inserciones. Los dos parámetros
que importan:
- m: conexiones por nodo (default 16, mayor = mejor
recall pero más memoria)
- ef_construction: amplitud de búsqueda en
construcción (default 64, mayor = mejor recall pero
builds más lentos)
La Realidad del Rendimiento
Corrí benchmarks en una instancia Supabase Pro (4 GB RAM,
CPU de 2 núcleos) con 1M de vectores a 1536 dimensiones
(tamaño de salida de OpenAI text-embedding-3-small).
Esto es lo que medí:
| Métrica | HNSW | IVFFlat | Pinecone p1 |
|---|---|---|---|
| Latencia p50 | 8ms | 12ms | 5ms |
| Latencia p95 | 22ms | 35ms | 11ms |
| Latencia p99 | 38ms | 58ms | 18ms |
| Recall@10 | 0.98 | 0.95 | 0.99 |
| QPS (conexión única) | 120 | 85 | 200 |
Pinecone es más rápido. Sin duda. Un índice vectorial
construido a propósito corriendo en hardware optimizado
le ganará a una base de datos de propósito general. Pero
mira los números reales. 22ms vs 11ms en p95. Para un
pipeline RAG donde el paso de generación del LLM toma
800-2000ms, estás discutiendo por 11 milisegundos.
Recall de 0.98 significa que de cada 100 consultas,
pgvector devuelve los mismos top-10 resultados que una
búsqueda exacta 98 veces. ¿El 2% donde difiere? Los
resultados "incorrectos" siguen siendo semánticamente
cercanos -- generalmente ítems rankeados 11vo o 12vo en
el ordenamiento exacto.
Ajusté ef_search (la amplitud de búsqueda en consulta)
a 100:
SET hnsw.ef_search = 100;
Eso llevó el recall a 0.99 con p95 en 28ms. Aún bien
dentro del rango aceptable para cualquier aplicación de
cara al usuario.
La Ventaja de Supabase
Corro mi Postgres en Supabase, que viene con pgvector
habilitado por defecto. Cero configuración. El mismo
connection string que mi app de Next.js usa para auth de
usuarios y datos de la aplicación también sirve consultas
de búsqueda vectorial.
Esto importa más que los benchmarks. Aquí está por qué:
Un servicio menos que monitorear. Sin dashboard
separado de base de datos vectorial, sin SLA de uptime
adicional que rastrear, sin segundo conjunto de
credenciales de acceso que rotar.
Un salto de red menos. Mi aplicación consulta
usuarios, embeddings y metadata en una sola consulta SQL
con JOINs. Una base de datos vectorial dedicada significa
una llamada API separada, un round trip separado, y la
complejidad de correlacionar resultados entre dos stores
de datos.
Una sorpresa de facturación menos. Supabase Pro
cuesta $25/mes. Sé exactamente cuánto cuesta. El pricing
de bases de datos vectoriales es por vector, por consulta,
por dimensión, por namespace -- un modelo de precios
diseñado para escalar con tu éxito y castigarte por ello.
Consistencia transaccional. Cuando inserto un nuevo
documento, el contenido de texto, los metadatos y el
embedding aterrizan en la misma transacción. No hay lag
de sincronización entre mi base de datos de aplicación y
mi índice vectorial. Sin dolores de cabeza de consistencia
eventual.
Cuando pgvector NO Es Suficiente
Sería deshonesto si te dijera que pgvector resuelve todo.
Aquí están los casos donde una base de datos vectorial
dedicada justifica su costo:
Más de 10M de vectores. A esta escala, los tiempos de
construcción del índice HNSW se extienden a horas y los
requisitos de memoria exceden lo que una instancia
estándar de Postgres provee. Pinecone y Qdrant hacen
sharding entre máquinas de forma nativa. pgvector no.
Requisitos de latencia p99 por debajo de 10ms. Si
estás construyendo un sistema de recomendaciones en
tiempo real donde cada milisegundo cuenta (piensa en
servicio de anuncios, señales de trading), el rango de
20-40ms p99 de pgvector no va a funcionar.
Aislamiento multi-tenant a escala masiva. Tienes 1000
tenants cada uno con 1M de vectores. Los namespaces en
Pinecone manejan esto limpiamente. En pgvector, estás
particionando tablas o filtrando con cláusulas WHERE, y
ninguno escala tan elegantemente pasado cierto punto.
Búsqueda acelerada por GPU. Algunas cargas de trabajo
necesitan el throughput que la búsqueda vectorial en GPU
provee. pgvector corre solo en CPU.
Si tu caso de uso cae en alguno de estos, la base de
datos vectorial dedicada vale el dinero. Pero sé honesto
sobre si estás resolviendo el problema de hoy o un
problema que imaginas tener en 18 meses.
Implementación: Supabase + pgvector
Aquí está el setup completo que uso en producción. Cinco
pasos de cero a búsqueda semántica.
1. Crear la Tabla
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
embedding VECTOR(1536),
created_at TIMESTAMPTZ DEFAULT NOW()
);
2. Agregar el Índice HNSW
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
Puse ef_construction en 200 para producción. Más alto
que el default, pero solo afecta el tiempo de
construcción, no el de consulta. Vale el costo inicial
para mejor recall.
3. Crear una Función de Búsqueda
CREATE OR REPLACE FUNCTION match_documents(
query_embedding VECTOR(1536),
match_threshold FLOAT DEFAULT 0.78,
match_count INT DEFAULT 10
)
RETURNS TABLE (
id BIGINT,
content TEXT,
metadata JSONB,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
d.id,
d.content,
d.metadata,
1 - (d.embedding <=> query_embedding) AS similarity
FROM documents d
WHERE 1 - (d.embedding <=> query_embedding)
> match_threshold
ORDER BY d.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
4. Insertar Embeddings (TypeScript)
import { createClient } from '@supabase/supabase-js'
import OpenAI from 'openai'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
)
const openai = new OpenAI()
async function insertDocument(
content: string,
metadata: Record<string, unknown>
) {
const { data: embeddingData } =
await openai.embeddings.create({
model: 'text-embedding-3-small',
input: content,
})
const embedding =
embeddingData[0].embedding
const { error } = await supabase
.from('documents')
.insert({
content,
metadata,
embedding,
})
if (error) throw error
}
5. Búsqueda Semántica (TypeScript)
async function searchDocuments(query: string) {
const { data: embeddingData } =
await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
})
const queryEmbedding =
embeddingData[0].embedding
const { data, error } = await supabase
.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.78,
match_count: 5,
})
if (error) throw error
return data
}
Esa es la implementación completa. Sin SDK para un
servicio vectorial separado. Sin gestión de API keys para
otro proveedor. Sin pipeline de sincronización de datos.
Solo SQL y el cliente de Supabase que ya tienes.
Comparación de Costos: Pinecone vs. pgvector
Coticé ambas opciones para una carga de trabajo realista:
500K vectores a 1536 dimensiones, 50K consultas por día,
una sola región.
| | Pinecone (s1) | Supabase Pro |
|---|---|---|
| Costo base | $70/mes | $25/mes |
| Almacenamiento (500K vecs) | incluido | incluido |
| Costos de consulta | incluido | incluido |
| Escrituras (10K/día) | incluido | incluido |
| Infra adicional | $0 | $0 |
| Total | $70/mes | $25/mes |
El pod s1 de Pinecone a $70/mes maneja esta carga de
trabajo. Supabase Pro a $25/mes también la maneja -- y
además corre tu auth, tu base de datos de aplicación,
tus suscripciones en tiempo real y tu almacenamiento.
Pero el costo real no es la factura. Es el tiempo de
ingeniería.
Agregar Pinecone significa: un nuevo SDK que aprender,
un pipeline de sincronización de datos que construir y
mantener, una configuración de monitoreo separada,
rotación de credenciales para otro servicio, un camino
de migración si superas el pod s1 o Pinecone cambia
precios, y la carga cognitiva de razonar sobre
consistencia de datos entre dos stores.
Estimo ese overhead en 2-4 horas de ingeniería por mes
para un equipo pequeño. A $150/hora con carga completa,
eso son $300-600/mes en costos ocultos. Más que la
infraestructura misma.
La Tesis de la Tecnología Aburrida
Dan McKinley escribió "Choose Boring Technology" en 2015.
El argumento: cada elección de tecnología que haces gasta
de un presupuesto limitado de innovación. Solo puedes
sostener unas pocas tecnologías novedosas a la vez antes
de que tu equipo se ahogue en complejidad operacional.
Postgres es la tecnología más aburrida que conozco. Ha
estado en producción desde 1996. Cada ingeniero en tu
equipo lo ha usado. Tu infraestructura de monitoreo,
backup y failover ya lo maneja.
pgvector convierte a Postgres en una base de datos
vectorial lo suficientemente buena. No la más rápida. No
la más rica en features. Pero lo suficientemente buena
para la gran mayoría de cargas de trabajo de IA en
producción. Y lo hace sin gastar nada de tu presupuesto
de innovación.
He lanzado tres features de IA sobre pgvector. Ninguna
requirió migrar a una base de datos vectorial dedicada
después. La única vez que lo consideré, me di cuenta de
que el cuello de botella era mi estrategia de chunking,
no el índice vectorial. Arreglé el problema real y seguí
adelante.
La brecha de rendimiento de 10-20% entre pgvector y una
solución dedicada es real. Pero es una brecha que
intercambias por: un servicio menos, una relación con
proveedor menos, un modo de falla menos, y una cosa
menos que explicarle al ingeniero de guardia a las 3 AM.
Ese intercambio vale la pena casi siempre.
Empieza Aquí
Si estás evaluando bases de datos vectoriales, intenta
esto antes de firmar un contrato:
- Habilita pgvector en tu Postgres existente
- Carga tus embeddings reales (no un dataset de
benchmark)
- Corre tus consultas reales y mide la latencia
- Compara esa latencia con tus requisitos reales
La mayoría de los equipos descubren que pgvector cumple
sus necesidades antes de terminar el paso 3. Los que no,
generalmente saben exactamente por qué, lo que hace el
caso para una solución dedicada mucho más claro.
La mejor decisión de infraestructura es la que no tienes
que tomar. Ya corres Postgres. Úsalo.