24 de julio de 2026
RDS + pgvector: búsqueda semántica sin salir de Postgres (y cuándo no conviene)
El problema que pgvector resuelve
Cuando un equipo arma su primer sistema de RAG, la arquitectura por default que casi todo tutorial recomienda es: Postgres (o MySQL) para tus datos de negocio, y una base de datos vectorial dedicada aparte —, Weaviate, Qdrant— para los embeddings. Dos sistemas, dos conexiones, dos modelos mentales.
El costo real de esa separación no es solo operativo. Es que tus datos vectoriales y tus
datos relacionales casi siempre están relacionados en el sentido literal de la palabra:
"dame los 5 documentos más similares a esta consulta, pero solo los que pertenecen a
este tenant_id y que no estén archivados" es una query que combina similitud semántica
con filtros relacionales exactos. Con dos bases de datos separadas, eso significa: buscar
en la base vectorial, traer 50 candidatos de más para compensar lo que vas a descartar, y
luego filtrar en tu aplicación cruzando contra Postgres —o mantener los mismos metadatos
duplicados en ambos sistemas para poder filtrar del lado vectorial, con el problema de
sincronización que eso implica cuando algo cambia en uno y no en el otro.
pgvector no es "una base vectorial simplificada". Es una extensión que le agrega un tipo
de dato (vector) y operadores de distancia a Postgres directamente, para que la
búsqueda por similitud sea una cláusula WHERE/ORDER BY más, en la misma transacción,
con los mismos joins, el mismo backup, y las mismas garantías ACID que ya tienes. La
pregunta que este artículo intenta responder no es "¿pgvector o una base vectorial
dedicada?" en abstracto, sino en qué punto de escala y de requisitos esa segunda opción
sí se justifica.
El flujo real: qué pasa entre "texto" y "búsqueda por similitud"
La infografía resume cuatro pasos. Cada uno tiene una decisión técnica detrás que determina qué tan bien funciona el resultado final.
Texto → embedding no es un paso trivial. Un embedding es un vector de N números
flotantes que representa el significado semántico de un texto en un espacio geométrico
—textos con significado parecido terminan en coordenadas cercanas. El modelo que generas
ese embedding importa más de lo que parece: text-embedding-3-small de produce
vectores de 1536 dimensiones, text-embedding-3-large de 3072. No son intercambiables:
un embedding generado con un modelo no es comparable matemáticamente con uno generado
con otro, aunque ambos representen el mismo texto.
Guardarlo junto a tus datos relacionales significa literalmente una columna más en tu tabla:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documentos (
id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
contenido TEXT NOT NULL,
embedding VECTOR(1536),
archivado BOOLEAN DEFAULT FALSE
);
Eso es lo que la infografía llama "una sola base de datos, dos capacidades" —pero la
implicación real es que ese tenant_id y ese archivado pueden entrar directo en el
WHERE de tu búsqueda vectorial, sin una segunda consulta.
Búsqueda por similitud semántica se hace con operadores de distancia, no con =:
SELECT id, contenido
FROM documentos
WHERE tenant_id = 42 AND archivado = FALSE
ORDER BY embedding <=> '[0.012, -0.034, ...]'
LIMIT 5;
<=> es distancia coseno, <-> es distancia euclidiana (L2), <#> es producto interno
negativo. La elección depende del modelo de embeddings que uses —OpenAI recomienda
coseno para los suyos— y usar el operador equivocado no truena, simplemente da resultados
de "similitud" que no significan lo que crees que significan.
Las cuatro piezas detrás de las feature cards
Una sola base de datos, dos capacidades. El beneficio no es solo menos infraestructura que mantener —es que tu búsqueda semántica hereda gratis todo lo que ya resolviste para tus datos relacionales: replicación, backups, connection pooling, permisos a nivel de fila. Nada de eso lo tienes que reconstruir para el caso vectorial.
Búsqueda por significado, no solo texto exacto. La diferencia con LIKE '%texto%'
o full-text search (tsvector) no es de grado, es de naturaleza. Full-text search
encuentra documentos que contienen las palabras que buscaste. Búsqueda semántica
encuentra documentos que significan lo que buscaste, aunque no compartan ni una sola
palabra —"cómo cancelar mi suscripción" puede encontrar un documento que dice "dar de
baja el plan mensual" sin que "cancelar" ni "suscripción" aparezcan ahí. Ese es
literalmente el problema que hace posible el RAG: recuperar contexto relevante sin
depender de que el usuario adivine el vocabulario exacto del documento.
Índices HNSW e IVFFlat para velocidad. Sin índice, una búsqueda vectorial es
exact nearest neighbor: Postgres calcula la distancia contra cada fila, complejidad
lineal. Funciona con miles de filas, se vuelve inutilizable con millones. Los índices
resuelven esto con approximate nearest neighbor —sacrifican precisión perfecta por
velocidad. IVFFlat divide el espacio vectorial en clusters (lists) y en tiempo de
búsqueda solo revisa los clusters más cercanos al vector de consulta; es rápido de
construir y usa poca memoria, pero su precisión (recall) depende de cuántos clusters
revisas (probes) y necesita que ya existan datos representativos antes de construirse.
HNSW (Hierarchical Navigable Small World) construye un grafo de capas donde navegar
es más lento de indexar y consume más memoria, pero da mejor recall a velocidad de
consulta comparable, y no necesita un paso de entrenamiento previo como IVFFlat. La
recomendación general de la infografía —"elige el índice según tu volumen"— en la
práctica casi siempre se resuelve a favor de HNSW salvo que la memoria disponible sea el
límite real.
Compatible con tu stack SQL existente. Tu ORM, tu herramienta de migraciones, tu
sistema de monitoreo, tu estrategia de disaster recovery —todo sigue funcionando sin
adaptadores especiales, porque para Postgres un vector es un tipo de columna más, no
un sistema paralelo que tienes que operar por separado.
Casos de uso reales
Sistemas RAG. El patrón estándar: la pregunta del usuario se convierte en embedding, se buscan los N documentos más cercanos, y esos documentos se inyectan como contexto en el prompt del modelo. hace la mitad de recuperación de ese pipeline.
Búsqueda semántica de documentos. Útil incluso sin un LLM en el flujo —un buscador interno que entiende sinónimos y paráfrasis en vez de depender de coincidencia exacta de palabras, para documentación interna o catálogos de producto.
Recomendaciones por similitud. "Productos parecidos a este" o "usuarios con gustos similares" son, matemáticamente, el mismo problema de vecinos más cercanos que la búsqueda semántica de texto —solo que el embedding representa un producto o un perfil de usuario en vez de un párrafo.
Detección de duplicados. Dos registros que dicen "casi lo mismo" con palabras distintas (dos tickets de soporte reportando el mismo bug con redacción diferente) tienen embeddings cercanos aunque el texto literal no coincida —una búsqueda por distancia por debajo de un umbral los agrupa sin reglas de texto escritas a mano.
Buenas prácticas que casi nadie sigue hasta que algo falla
Elige el índice según tu volumen real, no por default. Con pocos miles de filas ni siquiera necesitas índice —un exact scan es suficientemente rápido y te da 100% de recall. HNSW empieza a justificarse cuando el escaneo lineal se vuelve el cuello de botella, no antes.
No mezcles dimensiones de embeddings distintas. Una columna VECTOR(1536) rechaza
insertar un vector de 3072 dimensiones —eso Postgres lo va a impedir. El error real y
silencioso es otro: migrar de text-embedding-3-small a un modelo distinto y seguir
comparando los vectores nuevos contra los viejos sin regenerar el histórico. La
dimensión puede coincidir por casualidad; el espacio semántico no es el mismo, y las
distancias que arroja dejan de tener significado real.
Monitorea el tamaño del índice. Un índice HNSW puede pesar varias veces el tamaño de los datos crudos, porque guarda la estructura del grafo completa. Es una de las razones por las que "vectoriza todo" no es gratis —cada tabla con embeddings indexados compite por la misma RAM que el resto de tu base de datos, y en RDS eso se traduce directo en qué clase de instancia necesitas pagar.
Normaliza los vectores antes de guardarlos. Si vas a usar producto interno (<#>)
como proxy de similitud coseno por rendimiento, los vectores tienen que estar
normalizados a longitud 1 primero —si no, el producto interno mezcla magnitud con
dirección y el ranking de resultados deja de ser confiable. La mayoría de los SDKs de
embeddings (incluido el de OpenAI) ya regresan vectores normalizados, pero vale la pena
verificarlo, no asumirlo, sobre todo si combinas embeddings de más de una fuente.
Cuándo NO usar pgvector
- Cuando ya estás en una escala donde el cuello de botella es puramente vectorial. Cientos de millones de vectores con requisitos de latencia agresivos es el terreno donde una base vectorial dedicada, diseñada para eso desde cero (sharding nativo, cuantización, índices distribuidos), empieza a ganarle a Postgres corriendo la misma carga con recursos comparables.
- Cuando no tienes ya una base Postgres en la arquitectura. Si tu sistema es puramente un pipeline de embeddings sin datos relacionales que combinar, meter Postgres solo para tener pgvector es adoptar una pieza de infraestructura completa (backups, vacuuming, connection limits) por una capacidad que una base vectorial administrada te da sin ese overhead operativo.
- Cuando el costo de tener la instancia corriendo no se justifica por el uso real. RDS cobra por hora de instancia activa, no por query. Para cargas esporádicas o de bajo volumen, ese costo fijo es real —es exactamente la razón por la que en el proyecto donde lo uso existe un botón para apagar la instancia cuando nadie la está usando en vez de pagarla 24/7.
- Cuando necesitas features avanzadas de un motor vectorial especializado —cuantización agresiva para reducir memoria a gran escala, actualizaciones de índice en streaming sin degradar recall, multi-tenancy nativo a nivel de índice— que pgvector todavía no ofrece con la misma madurez que los motores dedicados a esto exclusivamente.
Ejemplo práctico: RAG agéntico con Lambda, OpenAI y RDS
Este es el patrón real: una Lambda recibe una pregunta, decide qué buscar (a diferencia de RAG clásico, donde siempre se busca una vez antes de responder), consulta pgvector, y solo entonces genera la respuesta.
Esquema e índice:
CREATE TABLE conocimiento (
id SERIAL PRIMARY KEY,
contenido TEXT NOT NULL,
embedding VECTOR(1536) NOT NULL
);
CREATE INDEX ON conocimiento
USING hnsw (embedding vector_cosine_ops);
La Lambda, con el modelo decidiendo si necesita buscar antes de responder:
import openai
import psycopg2
def buscar_contexto(pregunta, conn, k=5):
embedding = openai.embeddings.create(
model="text-embedding-3-small", input=pregunta
).data[0].embedding
with conn.cursor() as cur:
cur.execute(
"""
SELECT contenido FROM conocimiento
ORDER BY embedding <=> %s::vector
LIMIT %s
""",
(embedding, k),
)
return [row[0] for row in cur.fetchall()]
def handler(event, context):
pregunta = event["pregunta"]
conn = get_connection() # abre/reusa conexión a RDS
# El agente decide si esta pregunta necesita recuperación de contexto
decision = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"¿'{pregunta}' necesita consultar la base de conocimiento? responde sí/no"}],
)
contexto = buscar_contexto(pregunta, conn) if "sí" in decision.choices[0].message.content.lower() else []
respuesta = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"Contexto: {contexto}"},
{"role": "user", "content": pregunta},
],
)
return {"respuesta": respuesta.choices[0].message.content}
La diferencia con RAG clásico es esa decisión intermedia: no todas las preguntas necesitan recuperación ("¿cuánto es 2+2?" no la necesita), y forzar una búsqueda vectorial siempre agrega latencia y ruido de contexto irrelevante sin beneficio.
Mi conclusión
pgvector no me convenció por ser "una base vectorial gratis" —me convenció porque la mayoría de los sistemas de RAG que he construido nunca tuvieron un problema de escala vectorial real, tuvieron un problema de mantener sincronizados datos relacionales y datos vectoriales que en realidad siempre estuvieron relacionados. Adoptar una base de datos vectorial dedicada para un caso que cabe cómodo en una tabla de Postgres con un índice HNSW es resolver un problema de escala que todavía no tienes, a cambio de un sistema más que operar, respaldar, y mantener sincronizado. Cuando ese problema de escala llegue de verdad —y vas a saberlo porque el índice va a pesar más que tu RAM disponible— ahí sí vale la pena migrar. No antes.
Este artículo acompaña la infografía de RDS PostgreSQL con pgvector — parte de la serie Tecnolog-IA con Inteligenc-IA.
Dónde lo he usado
En producción
- Un proyecto interno de generación de contenido con agentes de IA (Interco) — la arquitectura corre completa en , sin servidores persistentes, así que los embeddings se generan con la API de en vez de correr un modelo local: no hay un proceso de larga duración donde tener un modelo de embeddings cargado en memoria. con guarda esos vectores junto a los datos relacionales del proyecto, y sirve tanto un pipeline de RAG clásico como flujos de RAG agéntico, donde el agente decide en tiempo de ejecución cuándo y qué consultar, no solo recupera una vez antes de responder. Por el costo de tener una instancia RDS corriendo todo el día, el proyecto incluye un botón para prenderla y apagarla bajo demanda en vez de dejarla activa 24/7.

Infografía generada con ChatGPT usando un prompt personalizado. —

