La mayoría monta una base vectorial dedicada antes de tener el problema que esta resuelve. pgvector le suma un tipo vector e índices ANN al Postgres que ya tienes corriendo, así la búsqueda semántica vive junto a tus datos relacionales: una sola base, un solo backup, un solo conjunto de transacciones. Es el default pragmático para RAG, y aquí te explico cuándo es la decisión correcta, cómo montarlo en Supabase y los detalles de índice que, sin avisar, convierten consultas rápidas en escaneos completos.

En resumen
- pgvector le suma a un Postgres común un tipo de columna vector más índices de vecino más cercano aproximado (HNSW e IVFFlat). Supabase ya lo trae: te basta con un «create extension».
- La ventaja real no es la velocidad, es la colocación: filtras por tenant, estado o fecha en la misma consulta que la búsqueda por similitud. Sin un segundo sistema, sin job de sincronización, sin bugs de consistencia eventual.
- Hasta unos pocos millones de vectores con filtrado por metadatos, es el default aburrido y correcto. De ahí en adelante, o si necesitas QPS muy alto, evalúa un motor especializado.
- El operador de distancia tiene que coincidir con la opclass del índice; si no, Postgres se va a un escaneo completo sin decir nada. Confírmalo siempre con EXPLAIN.
- HNSW es el índice por defecto que conviene en la mayoría de las apps. Constrúyelo después de la carga masiva y ajusta por consulta cuánto esfuerzo pones en la búsqueda.
Aparece RAG y el reflejo es montar una base vectorial dedicada: un segundo servicio, una segunda credencial, otra historia de backup que mantener y un job de sincronización para tenerla al día con tus datos reales. Para la mayoría de las apps, eso es un problema que compraste antes de necesitarlo. pgvector apuesta justo al revés: hace que el mismo Postgres que ya tienes corriendo también guarde y busque vectores, así los embeddings quedan en la misma tabla que las filas que describen. Sales de aquí sabiendo exactamente cuándo vale la pena esa apuesta, cómo dejarlo listo en el stack que de verdad uso (Supabase) y los dos o tres errores de índice que, sin avisar, se llevan toda la velocidad que viniste a buscar.
Nota
Esto da por sentado que puedes correr un Postgres del que tú controlas las migraciones y que tienes un modelo de embeddings al que llamar. pgvector es la capa de almacenamiento y búsqueda: no genera embeddings. Si todavía estás decidiendo si de verdad necesitas recuperación, resuelve eso primero; RAG es una concesión, no un default.
01 · Qué es
pgvector es una extensión de Postgres. Agrega un tipo de columna vector y una pequeña familia de operadores de distancia, más dos tipos de índice de vecino más cercano aproximado (ANN): HNSW e IVFFlat. Eso es todo lo que hay: ni un servidor aparte ni un protocolo aparte. Le hablas en SQL, dentro de la misma conexión y la misma transacción que todo lo demás en tu base.
Una vez habilitada la extensión, declaras una columna con una dimensión fija («vector(1536)» para un vector de «text-embedding-3-small» de OpenAI, «vector(1024)» para uno de Cohere) y guardas los embeddings como valores de columna normales. La similitud se convierte en un «ORDER BY» sobre un operador de distancia. Tres operadores cubren los casos comunes:
- «<=>»: distancia coseno. El default para la mayoría de embeddings de texto, que suelen venir normalizados.
- «<->»: distancia L2 (euclidiana).
- «<#>»: producto interno negativo.
En Supabase la extensión ya viene empaquetada, así que habilitarla es de verdad una sola sentencia. En un Postgres self-hosted instalas primero el binario de la extensión (un paquete o un paso de build) y luego la habilitas en cada base.
create extension if not exists vector;
alter table docs add column embedding vector(1536);
-- construye el índice DESPUÉS de cargar las filas en masa (ver sección 04)
create index on docs using hnsw (embedding vector_cosine_ops);
02 · Por qué importa
El argumento a favor de pgvector no es que sea la búsqueda vectorial más rápida del planeta: no lo es, y un motor dedicado bien afinado le gana en los extremos. El argumento es la colocación, y la colocación te da cosas que de verdad cuesta lograr de otra forma.
Cuando tus embeddings viven en la misma tabla que las filas que describen, una búsqueda semántica filtrada es una sola consulta:
select id, content
from docs
where tenant_id = 42
and status = 'published'
and lang = 'es'
order by embedding <=> :query_vec
limit 5;
Intenta hacer eso mismo contra un almacén vectorial aparte y de una vez heredas los problemas difíciles de los sistemas distribuidos. O denormalizas «tenant_id», «status» y «lang» en la metadata del almacén vectorial y los mantienes en sync en cada escritura (un job de sincronización que tarde o temprano se va a desfasar), o traes un conjunto amplio de candidatos del almacén vectorial y los filtras después en tu app, lo que bota resultados y rompe tu «limit». Con pgvector el planner lo resuelve de forma nativa, y tu row-level security, tus foreign keys y tus transacciones siguen aplicando igual. Borras un documento y su embedding se va en la misma sentencia.
Consejo
La razón más fuerte para empezar aquí es operativa, no técnica. Una sola base significa un solo backup, un solo pool de conexiones, un solo lugar donde mirar cuando algo va lento y una sola cosa que mantener parchada. En un solo VPS esa superficie operativa cuesta de verdad. No la gastes hasta que un benchmark con tus propios datos te diga que no te queda otra.
Hay además un tema de honestidad que me importa: buena parte de la adopción de bases vectoriales se hace por adelantado. Los equipos aprovisionan para una escala que no tienen ni van a tener en un año, y mientras tanto pagan esa complejidad todos los días. Empezar en Postgres te deja posponer esa decisión hasta tener números reales, y "lo medí y pgvector no daba abasto" es una razón mucho mejor para migrar que "supuse que no iba a aguantar".
03 · Cuándo usarlo (y cuándo no)
pgvector es el default correcto en una franja amplia, y una mala elección clara fuera de ella. Sé honesto contigo mismo sobre de qué lado estás.
Tira de pgvector cuando
- Estás en el rango de unos pocos millones de vectores. Con HNSW y hardware razonable, el recall y la latencia se mantienen bien hasta bien entrados los pocos millones.
- Tu recuperación es pesada en filtros: multi-tenant, por estado, por idioma, por fecha. Este es el terreno de pgvector, porque el filtro y la búsqueda comparten el mismo planner.
- Ya estás en Postgres o Supabase y quieres lanzar recuperación hoy sin tener que operar un sistema nuevo.
- Tu ruta de escritura necesita que el embedding y la fila aterricen de forma atómica: consistencia transaccional entre el dato y su vector.
Busca otra cosa cuando
- Ya pasaste de las decenas de millones de vectores, o vas para allá rápido. Un motor dedicado (Qdrant, entre otros) con cuantización y un layout de índice pensado para la memoria va a aprovechar la RAM mucho mejor a esa escala.
- Necesitas un QPS muy alto con metas estrictas de latencia de cola que un Postgres compartido (que además sirve tu tráfico OLTP) no puede cumplir sin pelearse por los recursos.
- Necesitas funciones vectoriales nativas que pgvector no tiene: una interacción sofisticada entre índice y payload, cuantización binaria nativa con rescoring, o sharding diseñado para miles de millones de puntos.
Importante
"Aburrido hasta que duela" es una estrategia real, no un atajo cobarde. El costo de empezar en pgvector y migrar después es una mudanza de datos única y bien acotada, y solo cuando ya tienes evidencia. El costo de empezar en un motor dedicado que no necesitabas se paga sin parar, en sobrecarga operativa, durante todo el tiempo que lo tengas corriendo. Elige la opción en la que salga barato equivocarse.
04 · Quickstart: una tabla de recuperación que funciona
Acá va un montaje mínimo pero ya con forma de producción: una tabla con alcance por tenant, una columna de embedding, un índice HNSW y una consulta de recuperación que filtra y ordena en una sola pasada.
create extension if not exists vector;
create table docs (
id bigint generated always as identity primary key,
tenant_id bigint not null,
status text not null default 'draft',
content text not null,
embedding vector(1536)
);
-- 1. Carga PRIMERO tus filas y embeddings en masa.
-- 2. LUEGO construye el índice. Un HNSW sobre una tabla vacía, llenado
-- de a poco, es más lento de construir y da un grafo peor que uno
-- construido en masa.
create index docs_embedding_idx
on docs using hnsw (embedding vector_cosine_ops);
-- Un índice btree de apoyo abarata el filtro por metadatos.
create index docs_tenant_status_idx on docs (tenant_id, status);
La consulta de recuperación, con un control para ajustar el esfuerzo de la búsqueda y un filtro razonable:
set hnsw.ef_search = 100; -- más alto = mejor recall, consulta más lenta
select id, content,
embedding <=> :query_vec as distance
from docs
where tenant_id = :tenant
and status = 'published'
order by embedding <=> :query_vec
limit 5;
Algunas cosas que vale la pena que se te queden grabadas:
- Construye el índice después de la carga masiva. Insertar en un HNSW que ya existe está bien para las escrituras del día a día, pero el grafo inicial sale notablemente mejor y se construye más rápido cuando pgvector ve todas las filas de una sola vez.
- «ef_search» es tu control de calidad contra velocidad en tiempo de consulta. No cuesta nada cambiarlo por sesión, así que puedes probar varios valores contra un set etiquetado y quedarte con el más bajo que llegue a tu meta de recall.
- Mantén un índice btree en las columnas por las que filtras. El planner lo puede combinar con la búsqueda vectorial; sin él, tu filtro termina degradándose a un escaneo.
Atención
No dejes que las dimensiones se descuadren. La dimensión de la columna está fija en «vector(N)» y tus embeddings tienen que coincidir exactamente. Si algún día cambias de modelo de embeddings (otro modelo, otra N) necesitas una columna (o tabla) nueva y volver a generar los embeddings, no un cambio en caliente. Deja anotados el modelo y la dimensión en los comentarios del schema, para que tu yo del futuro no termine mezclando dos espacios de embeddings en una misma columna y sacando rankings que no tienen sentido.
05 · El detalle que mata tu rendimiento en silencio
Este es el que le cuesta a la gente latencia real sin un solo error que apunte al problema: el operador de distancia de tu consulta tiene que coincidir con la opclass con la que se construyó el índice. No son intercambiables, y un desajuste no falla a gritos: simplemente deja de usar el índice.
Un índice HNSW creado con «vector_cosine_ops» solo acelera el operador coseno «<=>». Si lo construyes para coseno y luego consultas con «<->» (L2) o «<#>» (producto interno), Postgres no puede usar ese índice para el ordenamiento, así que se va a un escaneo secuencial sobre cada fila. En una tabla chica ni lo notas. En unos cientos de miles de filas, tu búsqueda semántica "rápida" se vuelve de golpe un escaneo completo de tabla en cada request, y el único síntoma es que va lenta.
El arreglo es un hábito de treinta segundos: comprueba que el índice realmente se está usando.
explain analyze
select id from docs
order by embedding <=> :query_vec
limit 5;
-- Busca un "Index Scan using docs_embedding_idx".
-- Un "Seq Scan" aquí significa que el operador/opclass no coinciden,
-- o que el planner cree que un escaneo es más barato (a menudo: índice
-- sin construir, o estadísticas viejas — corre ANALYZE).
Dos trampas relacionadas de la misma familia:
- Construye el índice para el operador con el que de verdad vas a consultar. Si tus embeddings están normalizados, coseno y producto interno ordenan igual, pero aun así tienes que construir el índice para el operador que usa tu consulta. Decídelo una vez y constrúyelo acorde.
- Un filtro demasiado selectivo puede hacer que un escaneo sea de verdad más barato, y el planner va a elegirlo con razón. Eso no es un bug, pero si esperabas el índice ANN y no lo obtuviste, EXPLAIN te dice por qué en lugar de dejarte adivinando.
En cada proyecto de Supabase que armo arranco con pgvector, incluyendo las rutas de recuperación de mi propio trabajo; no porque sea la opción más potente, sino porque es la que puedo razonar, respaldar y asegurar con la misma base de datos que ya tengo corriendo. El día que un benchmark con datos reales diga que no alcanza, esa sí es una razón limpia y con evidencia para graduarte a un motor dedicado. Hasta entonces, la opción aburrida mantiene el sistema chico, y los sistemas chicos son justo los que siguen siendo fáciles de debuggear.
Puntos clave
- pgvector convierte el Postgres que ya tienes corriendo en un almacén vectorial capaz: una sola base, un solo backup, un solo conjunto de transacciones, sin job de sincronización.
- Su ventaja es la colocación: búsqueda semántica filtrada en una sola consulta, con RLS y foreign keys aplicando igual. Eso cuesta replicarlo entre dos sistemas.
- Es el default pragmático hasta unos pocos millones de vectores con filtrado por metadatos. Migra solo cuando un benchmark con datos reales (no una suposición) diga que lo superaste.
- Por defecto tira de HNSW, construye el índice después de la carga masiva y afina «ef_search» por consulta para el recall que necesitas.
- El asesino silencioso es un desajuste de operador/opclass que te deja cayendo en un escaneo secuencial. Que EXPLAIN ANALYZE te salga por reflejo.
Preguntas frecuentes
¿pgvector es de verdad lo bastante rápido para producción, o es un juguete al lado de una base vectorial seria?
Es perfectamente apto para producción hasta unos pocos millones de vectores con índice HNSW. Un montón de apps reales sirven búsqueda semántica filtrada sobre pgvector con latencia ANN de pocos milisegundos. Un motor dedicado gana en los extremos (decenas de millones de vectores, QPS muy alto o cuantización agresiva), pero "no es el más rápido a escala de mil millones" no es lo mismo que "un juguete". Haz benchmark con tus propios datos y tu propio tráfico antes de dar por hecho que ya lo superaste.
HNSW o IVFFlat: ¿cuál índice me conviene usar?
Por defecto, tira de HNSW. Da mejor recall contra latencia en la mayoría de las cargas y no hace falta reconstruirlo a medida que crecen los datos. Sus concesiones son que usa más memoria y que el build del índice es más lento. IVFFlat construye más rápido y usa menos memoria, pero su recall depende de un conteo de listas que afinas de antemano y se degrada cuando la distribución de los datos cambia. A menos que tengas poca memoria o estés construyendo índices enormes una y otra vez, HNSW es el punto de partida correcto.
¿pgvector genera los embeddings, o tengo que ponerlos yo?
Los pones tú. pgvector es puramente la capa de almacenamiento y búsqueda: guarda vectores y corre consultas de vecino más cercano. Tú llamas a un modelo de embeddings (OpenAI, Cohere, un modelo local, lo que sea) desde tu aplicación y luego insertas el vector resultante en la columna. Mantén fijos el modelo y su dimensión para cada columna; mezclar embeddings de modelos distintos en una misma columna produce rankings sin sentido.
La búsqueda vectorial pura se pierde con términos exactos como códigos de producto y mensajes de error. ¿Cómo manejo eso?
Hazlo híbrido. Postgres ya trae full-text search incorporado, así que puedes correr una consulta por keyword y otra vectorial en la misma base y combinar sus rankings, sin necesidad de un tercer sistema. Esto importa más de lo que la gente cree: la similitud semántica es excelente para el significado, pero es ciega a los tokens exactos, y muchas consultas reales dependen de un código o un nombre exacto. La colocación vuelve natural la recuperación híbrida acá, otro punto a favor de pgvector.
Mis consultas se pusieron lentas cuando la tabla creció. ¿Qué reviso primero?
Corre EXPLAIN ANALYZE sobre la consulta. Si ves un "Seq Scan" en lugar de un "Index Scan" sobre tu índice vectorial, el índice no se está usando. La causa más común es un operador de distancia que no coincide con la opclass del índice: lo construiste para coseno («<=>») pero consultaste con L2 («<->»), por ejemplo. Otras causas: el índice nunca se construyó, las estadísticas están viejas (corre ANALYZE), o el filtro es lo bastante selectivo como para que el planner prefiera un escaneo, y con razón. EXPLAIN te dice cuál es.
¿Cuándo sé de verdad que llegó la hora de migrar fuera de pgvector?
Cuando tengas un benchmark con datos y tráfico reales que lo diga, no antes. Señales concretas: pasaste de las decenas de millones de vectores y el recall o la latencia empiezan a caer incluso con el «ef_search» afinado; tu búsqueda vectorial se pelea por los recursos con el tráfico OLTP en el mismo Postgres y no puedes separarlos; o necesitas una función vectorial nativa (cuantización binaria con rescoring, sharding de mil millones de puntos) que pgvector no ofrece. "Lo medí y no da abasto" es el disparador correcto. "Supuse que no iba a aguantar" no lo es.
¿Prefieres que lo hagamos por ti?
Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.
Escríbenos por WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Arma un índice RAG en Supabase con pgvector
RAG es búsqueda por vecino más cercano sobre texto que convertiste en embeddings, más un modelo que lee los resultados. Esta guía monta todo sobre Postgres pelado (schema, chunking, un índice HNSW, una función de búsqueda detrás de RLS y la llamada de retrieval a Claude) y te dice sin rodeos dónde pgvector se queda corto.

Qdrant: un motor vectorial dedicado
Llega un punto en que meter los vectores dentro de Postgres deja de dar abasto: el corpus crece demasiado, el QPS sube mucho o el filtrado pesa tanto que el planner ya no puede ir rápido. Qdrant es el siguiente paso, y puedes auto-hospedarlo: una base vectorial en Rust con búsqueda HNSW, filtrado que ocurre dentro de la ruta del índice y cuantización que cambia recall por una memoria que sí te puedes permitir. Aquí te cuento qué es, cuándo justifica el segundo servicio que te cuesta, cómo levantarlo y consultarlo, y las trampas de recall que, sin que te enteres, dejan tu búsqueda incorrecta en lugar de lenta.

El skill de migración de base de datos: generar el forward + rollback con los chequeos de seguridad por delante
Un skill de Claude Code que toma un cambio de esquema descrito y te devuelve una migración forward revisada, un rollback probado y un informe de riesgo de locks y pérdida de datos, para que dejes de correr ALTERs destructivos contra producción a las 11 de la noche.