pgvector en PostgreSQL: búsqueda vectorial para aplicaciones de IA sin salir de SQL

pgvector es una extensión de PostgreSQL que añade un tipo de dato vector y operaciones de búsqueda por similitud. Permite guardar embeddings de modelos de lenguaje directamente en PostgreSQL y hacer búsquedas del tipo «encuentra los N documentos más similares a este» con SQL, sin necesitar una base de datos vectorial separada como Pinecone o Qdrant. La versión 0.7 salió en febrero de 2024 y la 0.8 en noviembre de 2024.

Instalación y activación

-- Activar la extension (necesita que pgvector este instalado en el sistema)
CREATE EXTENSION vector;

-- Crear una tabla con una columna de vectores de 1536 dimensiones
-- (1536 es la dimension de los embeddings de text-embedding-3-small de OpenAI)
CREATE TABLE documentos (
  id        BIGSERIAL PRIMARY KEY,
  titulo    TEXT,
  contenido TEXT,
  embedding vector(1536)
);

Los tres operadores de distancia

pgvector define tres operadores para medir la distancia o similitud entre vectores:

  • <->: distancia euclidiana L2. La distancia geométrica en el espacio vectorial.
  • <=>: distancia coseno. Mide el ángulo entre dos vectores, independientemente de su magnitud. La más usada para similitud semántica.
  • <#>: producto escalar negativo. Útil cuando los vectores están normalizados.
-- Buscar los 5 documentos mas similares al embedding de consulta
SELECT titulo, contenido,
       embedding <=> '[0.1, 0.2, -0.3, ...]'::vector AS distancia_coseno
FROM documentos
ORDER BY embedding <=> '[0.1, 0.2, -0.3, ...]'::vector
LIMIT 5;

Índices para búsqueda aproximada

Sin índice, una búsqueda vectorial hace un escaneo secuencial comparando el vector de consulta contra todos los vectores de la tabla. Funciona bien para tablas pequeñas, pero con millones de filas es inviable. pgvector soporta dos tipos de índice para búsqueda aproximada (ANN, Approximate Nearest Neighbor):

IVFFlat

Divide los vectores en clusters y busca solo en los clusters más cercanos al vector de consulta. Requiere que la tabla ya tenga datos antes de crear el índice (necesita los datos para construir los clusters).

-- Indice IVFFlat: listas es la cantidad de clusters
-- Recomendacion: listas = sqrt(numero de filas)
CREATE INDEX idx_docs_ivfflat ON documentos
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

-- Ajustar cuantas listas buscar (mas = mas exacto pero mas lento)
SET ivfflat.probes = 10;

HNSW (desde pgvector 0.5.0)

Hierarchical Navigable Small World. Construye un grafo de navegación jerárquico. Es más lento de construir y usa más memoria, pero las búsquedas son más rápidas y más precisas que IVFFlat. No necesita datos previos, así que puedes crear el índice antes de insertar filas.

-- Indice HNSW
CREATE INDEX idx_docs_hnsw ON documentos
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- m: numero de conexiones por nodo (mas = mas precision, mas memoria)
-- ef_construction: tamano de la lista de candidatos al construir

Integración con embeddings de LLMs

El flujo típico en una aplicación RAG (Retrieval-Augmented Generation) con pgvector es:

  1. Trocear los documentos en chunks de texto.
  2. Llamar a la API del modelo de embeddings (OpenAI, Ollama local, etc.) para obtener un vector por chunk.
  3. Guardar el texto y el vector en PostgreSQL.
  4. En tiempo de consulta, convertir la pregunta del usuario a vector y buscar los documentos más similares.
  5. Pasar esos documentos como contexto al LLM para que genere la respuesta.
import openai
import psycopg2
import numpy as np

# Obtener embedding de un texto
def get_embedding(texto):
    resp = openai.embeddings.create(
        input=texto,
        model="text-embedding-3-small"
    )
    return resp.data[0].embedding

# Guardar documento con su embedding
def guardar_documento(conn, titulo, contenido):
    embedding = get_embedding(contenido)
    with conn.cursor() as cur:
        cur.execute(
            "INSERT INTO documentos (titulo, contenido, embedding) VALUES (%s, %s, %s)",
            (titulo, contenido, embedding)
        )
    conn.commit()

# Buscar documentos similares
def buscar_similares(conn, consulta, top_k=5):
    embedding_consulta = get_embedding(consulta)
    with conn.cursor() as cur:
        cur.execute(
            """SELECT titulo, contenido,
                      embedding <=> %s::vector AS distancia
               FROM documentos
               ORDER BY embedding <=> %s::vector
               LIMIT %s""",
            (embedding_consulta, embedding_consulta, top_k)
        )
        return cur.fetchall()

pgvector frente a bases de datos vectoriales dedicadas

Pinecone, Qdrant, Weaviate y similares están diseñados específicamente para búsqueda vectorial a gran escala, y ofrecen mejor rendimiento en conjuntos de datos muy grandes (cientos de millones de vectores) o con requisitos de latencia muy bajos. pgvector, en cambio, tiene la gran ventaja de que ya tienes PostgreSQL: no necesitas otro servicio que mantener, ni sincronizar datos entre sistemas, ni aprender otra API.

Para la mayoría de las aplicaciones, pgvector con un índice HNSW aguanta perfectamente hasta unos pocos millones de vectores. A partir de ahí, si la latencia o el rendimiento son críticos, tiene sentido evaluar una solución dedicada.

pgvector conecta directamente con el trabajo de LLMs en Python. El artículo sobre Python con LLMs y RAG en 2026 cubre la parte de generación de embeddings y construcción de pipelines RAG, y pgvector es la pieza de almacenamiento que faltaba. Y si te interesa cómo Elixir plantea el ML con modelos locales, el artículo sobre Nx y Livebook para ML en Elixir ofrece un enfoque muy diferente.

Imagen: Pexels / Markus Winkler

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP