AJUAREZ93
WorkTech ExplicadoNewsletterExperienceStackContact
Hire me
Tech Explicado

24 de julio de 2026

RDS + pgvector: búsqueda semántica sin salir de Postgres (y cuándo no conviene)

DatabaseAI / ML

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.
AWS RDS
TrabajoInterco
pgvector
TrabajoInterco

Temas relacionados

Qdrant: cuándo tu RAG ya se le quedó chico a Postgres

Qdrant: cuándo tu RAG ya se le quedó chico a Postgres

Redis: ¿por qué es tan rápido? (y cuándo no usarlo)

Redis: ¿por qué es tan rápido? (y cuándo no usarlo)

RDS + pgvector: búsqueda semántica sin salir de Postgres (y cuándo no conviene)

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

AJUAREZ93

© 2026 Jose Alfonso Guerrero Juarez · Built from Guanajuato, México

github.com/ajuarez93 ↗