La mejor base de datos vectorial podría ser la que ya tienes.
Por qué pgvector
El mercado de bases de datos vectoriales dedicadas —Pinecone, Weaviate, Qdrant, Milvus, Chroma— explotó junto con el auge de la IA. Cada una ofrece una búsqueda rápida de vecinos más cercanos aproximados para embeddings. Cada una es también otro servicio que desplegar, monitorear, pagar y mantener. Otra cadena de conexión. Otro punto de falla.
pgvector es una extensión de PostgreSQL que añade tipos de columnas vectoriales y operadores de búsqueda de similitud directamente a Postgres. Si ya utilizas PostgreSQL —y la mayoría de las aplicaciones lo hacen— puedes añadir búsqueda vectorial sin incorporar una nueva base de datos.
Las ventajas se acumulan rápidamente:
- Una sola base de datos para todo. Datos de la aplicación, tablas de usuarios, embeddings vectoriales, todo en la misma base de datos. Haz un JOIN de los embeddings con metadatos usando SQL estándar.
- Herramientas existentes.
pg_dump,pg_restore, pooling de conexiones (PgBouncer), monitoreo (pg_stat_statements). Todo lo que ya usas para Postgres funciona en columnas vectoriales. - Transacciones ACID. Inserta un documento y su embedding en la misma transacción. Sin dolores de cabeza por consistencia distribuida entre tu base de datos principal y un almacenamiento vectorial independiente.
- Operaciones familiares. Tu equipo ya conoce SQL, tu ORM ya se comunica con Postgres, tu pipeline de despliegue ya maneja PostgreSQL. La curva de aprendizaje es la extensión, no una plataforma completa.
Instalación y configuración
En un servicio de PostgreSQL gestionado (AWS RDS, Google Cloud SQL, Supabase, Neon), pgvector suele ser una extensión que se habilita con un solo clic. En Postgres autogestionado:
# Construir desde el código fuente
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make
make install
# O a través del gestor de paquetes (Debian/Ubuntu)
apt install postgresql-16-pgvectorHabilítalo en tu base de datos:
CREATE EXTENSION IF NOT EXISTS vector;Creación de columnas vectoriales
Las columnas vectoriales almacenan arreglos de números de punto flotante de dimensión fija. Especificas la dimensionalidad al crear la columna:
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
summary TEXT,
tags TEXT[],
status TEXT DEFAULT 'draft',
published_at TIMESTAMPTZ,
embedding vector(1536), -- Dimensión de OpenAI ada-002
search_vector tsvector, -- Búsqueda de texto completo (full-text search)
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);Dimensiones comunes de embeddings:
- 1536 — OpenAI text-embedding-ada-002, text-embedding-3-small
- 3072 — OpenAI text-embedding-3-large
- 768 — Sentence transformers, muchos modelos open-source
- 384 — MiniLM y otros modelos ligeros
- 1024 — Cohere embed-v3
Inserta los embeddings como arreglos:
INSERT INTO articles (title, slug, content, embedding)
VALUES (
'Building a RAG Pipeline',
'building-rag-pipeline',
'Full article content here...',
'[0.0023, -0.0142, 0.0381, ...]'::vector(1536)
);Estrategias de indexación
Sin un índice, pgvector realiza una búsqueda exacta de vecinos más cercanos escaneando cada fila. Esto es aceptable para miles de filas, pero no escala a millones. Dos tipos de índices manejan esta situación.
IVFFlat (Inverted File Index)
Divide los vectores en clusters (listas) y solo busca en los clusters más cercanos al momento de la consulta.
-- Crear un índice IVFFlat
CREATE INDEX ON articles
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);Cómo funciona: durante la creación del índice, los vectores se agrupan en grupos de lists usando k-means. Al realizar la consulta, el índice examina los clusters más cercanos según el parámetro probes y busca solo en esos vectores. Más probes = mayor recall pero consultas más lentas.
Cuándo usarlo: es más rápido de construir que HNSW. Es bueno para datasets que cambian con poca frecuencia o cuando necesitas reconstruir índices rápidamente. Funciona bien hasta ~1M de vectores.
Ajuste (Tuning):
-- Establecer probes al momento de la consulta (el valor por defecto es 1)
SET ivfflat.probes = 10; -- Buscar en 10 de las 100 listasRegla general: lists = sqrt(num_rows) para tablas de hasta 1M de filas. probes = sqrt(lists) es un punto de partida razonable.
HNSW (Hierarchical Navigable Small World)
Construye un grafo de múltiples capas donde cada vector está conectado a sus vecinos más cercanos aproximados. Las búsquedas atraviesan el grafo desde las capas gruesas hasta las finas.
-- Crear un índice HNSW
CREATE INDEX ON articles
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);Cómo funciona: el índice construye una jerarquía de grafos de proximidad. m controla el número de conexiones por nodo (mayor = más conexiones = mejor recall pero más memoria). ef_construction controla la calidad de la construcción del índice (mayor = construcción más lenta pero mejor recall).
Cuándo usarlo: mejor recall que IVFFlat a la misma velocidad. Soporta inserciones incrementales sin necesidad de reconstrucción. Es el estándar para la mayoría de las cargas de trabajo en producción.
Ajuste (Tuning):
-- Establecer la calidad de búsqueda al momento de la consulta
SET hnsw.ef_search = 100; -- El valor por defecto es 40. Mayor = mejor recall, más lento.Comparación práctica:
| Aspecto | IVFFlat | HNSW |
|---|---|---|
| Tiempo de construcción | Más rápido | Más lento |
| Inserción sin reconstrucción | Requiere reindexar | Soporta incremental |
| Memoria | Menor | Mayor (~2-4x) |
| Recall a la misma velocidad | Menor | Mayor |
| Ideal para | Datasets estáticos | Dinámico, uso en producción |
Métricas de distancia
pgvector soporta tres operadores de distancia:
-- Distancia de coseno (más común para embeddings de texto)
SELECT * FROM articles
ORDER BY embedding <=> query_embedding -- distancia de coseno
LIMIT 10;
-- Distancia L2 (Euclidiana)
SELECT * FROM articles
ORDER BY embedding <-> query_embedding -- distancia L2
LIMIT 10;
-- Producto interno (negativo, para búsqueda de producto interno máximo)
SELECT * FROM articles
ORDER BY embedding <#> query_embedding -- producto interno negativo
LIMIT 10;La distancia de coseno (<=>) es la opción estándar para embeddings de texto. Es uno menos la similitud de coseno:
por lo que una dirección idéntica da una distancia de 0 y una opuesta da 2. Mide la similitud angular, que es para lo que la mayoría de los modelos de embeddings optimizan. Los embeddings normalizados (vectores unitarios) hacen que la distancia de coseno sea equivalente al producto interno.
La distancia L2 (<->) mide la distancia en línea recta en el espacio de embeddings. Es útil cuando la magnitud importa (por ejemplo, embeddings de imágenes donde la distancia correlaciona con la diferencia visual).
El producto interno (<#>) es computacionalmente el más rápido, pero solo tiene sentido como métrica de similitud en vectores normalizados. Úsalo cuando tus embeddings estén pre-normalizados y busques la máxima velocidad.
Búsqueda híbrida: vectores + texto completo
La característica estrella de pgvector en Postgres es combinar la similitud vectorial con la búsqueda tradicional de texto completo (full-text search) en una sola consulta. Las bases de datos vectoriales dedicadas no pueden hacer eso sin un sistema de búsqueda independiente.
-- Búsqueda híbrida: combina similitud semántica con coincidencia de palabras clave
WITH semantic AS (
SELECT id, 1 - (embedding <=> $1::vector) AS semantic_score
FROM articles
WHERE status = 'published'
ORDER BY embedding <=> $1::vector
LIMIT 50
),
keyword AS (
SELECT id, ts_rank(search_vector, websearch_to_tsquery('english', $2)) AS text_score
FROM articles
WHERE status = 'published'
AND search_vector @@ websearch_to_tsquery('english', $2)
)
SELECT
a.id, a.title, a.slug, a.summary, a.tags,
COALESCE(s.semantic_score, 0) * 0.7 +
COALESCE(k.text_score, 0) * 0.3 AS combined_score
FROM articles a
LEFT JOIN semantic s ON a.id = s.id
LEFT JOIN keyword k ON a.id = k.id
WHERE s.id IS NOT NULL OR k.id IS NOT NULL
ORDER BY combined_score DESC
LIMIT 10;Esta consulta hace tres cosas:
- Encuentra artículos semánticamente similares mediante búsqueda vectorial (entiende el significado).
- Encuentra artículos que coincidan con palabras clave usando
tsvector(atrapa términos exactos). - Combina las puntuaciones con pesos configurables (0.7 semántico, 0.3 palabra clave).
La ponderación depende de tu caso de uso. Para la búsqueda en un blog, una configuración orientada a lo semántico de 0.7/0.3 funciona bien. Para búsqueda de código donde los identificadores exactos importan, podrías invertirlo a 0.3/0.7.
Para soportar la búsqueda de texto completo de manera eficiente, mantén una columna tsvector con un índice GIN:
-- Añadir columna tsvector con índice GIN
CREATE INDEX articles_search_idx ON articles USING gin(search_vector);
-- Actualización automática de search_vector al insertar/actualizar
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.summary, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.content, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER articles_search_update
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION update_search_vector();Ejemplo de esquema real
Un esquema práctico de un blog con capacidades de RAG, similar al que impulsa este sitio:
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT NOT NULL,
summary TEXT,
tags TEXT[],
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
reading_time TEXT,
published_at TIMESTAMPTZ,
embedding vector(768), -- Salida del modelo de embeddings
search_vector tsvector, -- Búsqueda de texto completo
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Índices
CREATE INDEX articles_embedding_idx ON articles
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX articles_search_idx ON articles USING gin(search_vector);
CREATE INDEX articles_status_idx ON articles (status);
CREATE INDEX articles_tags_idx ON articles USING gin(tags);
CREATE INDEX articles_published_idx ON articles (published_at DESC);Con esto obtienes:
- Búsqueda semántica en todos los artículos mediante similitud vectorial.
- Búsqueda de palabras clave de texto completo con campos ponderados.
- Filtrado basado en etiquetas usando operadores de arreglos (
tags @> ARRAY['rag']). - Filtrado por estado (borrador/publicado/archivado).
- Listado cronológico por fecha de publicación.
- Todo en una sola tabla con acceso SQL estándar.
Ajuste de rendimiento (Performance tuning)
Parámetros clave que afectan el rendimiento de las consultas:
hnsw.ef_search controla el equilibrio entre recall y velocidad al momento de la consulta. El valor por defecto es 40. Para producción:
- 40-100: buen equilibrio para la mayoría de las aplicaciones.
- 200+: cuando necesitas un recall muy alto y puedes tolerar aproximadamente el doble de latencia.
- Realiza un perfilado (profiling) en tu dataset real para encontrar el valor adecuado.
maintenance_work_mem debe configurarse con un valor más alto al construir índices HNSW. La construcción consume mucha memoria.
SET maintenance_work_mem = '2GB'; -- Antes de crear el índice HNSWshared_buffers es importante porque el índice HNSW necesita caber en memoria para un mejor rendimiento. Ajusta tus shared_buffers en consecuencia. Estimación aproximada: el tamaño del índice HNSW es aproximadamente de 1.5 a 2 veces el tamaño de los datos vectoriales sin procesar.
El pooling de conexiones también importa. Las consultas vectoriales son intensivas en CPU. Usa PgBouncer y evita saturar todas las conexiones con búsquedas vectoriales simultáneas.
Limitaciones vs. bases de datos vectoriales dedicadas
La honestidad es importante aquí. pgvector tiene sus compromisos.
Techo de escala. Las bases de datos vectoriales dedicadas como Pinecone y Milvus están diseñadas para miles de millones de vectores con sharding automático. pgvector funciona bien hasta ~10M de vectores por tabla; pasado ese punto, estarás luchando contra la arquitectura de nodo único de PostgreSQL.
Sin reranking integrado. Las plataformas dedicadas a menudo incluyen reranking mediante cross-encoders. Con pgvector, debes implementar el reranking en tu capa de aplicación.
Tiempo de construcción del índice. La creación de índices HNSW en tablas grandes (millones de filas) es lenta y consume mucha memoria. Las bases de datos dedicadas manejan esto con construcciones distribuidas.
Sin generación de embeddings nativa. Tú generas los embeddings en tu aplicación y los almacenas. Algunas bases de datos vectoriales ofrecen APIs de embeddings integradas.
Para la mayoría de las aplicaciones —startups, productos SaaS medianos, herramientas internas, plataformas de contenido— el techo de escala de pgvector es más que suficiente. Probablemente no tengas miles de millones de vectores. La simplicidad operativa de tener una base de datos en lugar de dos vale más que una escalabilidad teórica que realmente no necesitas.
Comienza con pgvector. Si llegas a superarlo, tus embeddings son portátiles. Migrar a una base de datos vectorial dedicada es sencillo, porque el cuello de botella siempre son los datos, no el esquema.
