Bases de datos vectoriales: qué son y por qué las usa la IA

D
DanisCh
• 14 min de lectura
Bases de datos vectoriales: qué son y por qué las usa la IA
Conceptos de Base de Datos

Si llevas algún tiempo siguiendo el ecosistema de inteligencia artificial, habrás visto menciones a Pinecone, Weaviate, Chroma o pgvector. Todas son bases de datos vectoriales, y se han convertido en una pieza fundamental de las aplicaciones modernas de IA, especialmente en los sistemas RAG (Retrieval-Augmented Generation) que permiten a modelos de lenguaje como GPT-4 o Claude responder preguntas sobre documentos privados o datos actualizados.

En esta guía vas a entender qué son los vectores en este contexto, por qué las bases de datos tradicionales no sirven para buscar en ellos, cómo funcionan las bases de datos vectoriales y cómo usar las opciones más populares en proyectos reales.

Qué es un embedding (vector)

Un embedding es una representación numérica de un fragmento de texto (o imagen, audio, código) como un vector de números reales. Los modelos de lenguaje como los de OpenAI o Sentence Transformers convierten texto en vectores de entre 384 y 3.072 dimensiones, dependiendo del modelo.

Lo que hace útiles a los embeddings es que capturan el significado semántico: textos con significados similares tienen vectores que están cerca en el espacio matemático, independientemente de si comparten las mismas palabras exactas.

from openai import OpenAI

client = OpenAI()

def obtener_embedding(texto: str) -> list[float]:
    respuesta = client.embeddings.create(
        model="text-embedding-3-small",   # 1536 dimensiones
        input=texto
    )
    return respuesta.data[0].embedding

# Embeddings de textos similares
v1 = obtener_embedding("El gato duerme en el sofá")
v2 = obtener_embedding("Un felino está descansando en el mueble")
v3 = obtener_embedding("La bolsa de valores subió un 3% hoy")

print(len(v1))   # 1536 dimensiones

# v1 y v2 estarán "cerca" en el espacio vectorial
# v1 y v3 estarán "lejos" aunque puedan compartir algunas palabras
import numpy as np

def similitud_coseno(v1: list[float], v2: list[float]) -> float:
    """
    Mide qué tan similares son dos vectores.
    1.0 = idénticos, 0.0 = sin relación, -1.0 = opuestos
    """
    a = np.array(v1)
    b = np.array(v2)
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

# Textos sobre el mismo tema tienen similitud alta
print(similitud_coseno(v1, v2))   # ~0.92 (muy similares)

# Textos sobre temas distintos tienen similitud baja
print(similitud_coseno(v1, v3))   # ~0.18 (poco relacionados)

Por qué las bases de datos tradicionales no sirven

Las bases de datos relacionales están optimizadas para búsquedas exactas o por rangos: WHERE nombre = 'Ana', WHERE precio BETWEEN 10 AND 50. Son perfectas para preguntas del tipo "dame los pedidos del cliente 123 en el mes de junio".

Pero cuando quieres buscar por significado semántico ("dame los documentos relacionados con la política de devoluciones"), necesitas comparar el vector de tu consulta con los vectores de todos los documentos y ordenar por similitud. Con un millón de documentos, comparar el vector de la consulta contra cada uno de los vectores almacenados es un cálculo que un índice SQL convencional no puede hacer eficientemente.

-- En PostgreSQL sin extensión de vectores, una búsqueda de similitud exacta
-- requeriría calcular la distancia contra cada fila:
SELECT id, contenido,
  -- Calcular distancia manualmente con arrays es posible pero muy lento
  -- El motor no puede usar ningún índice para esto
  dot_product(embedding_columna, query_vector) AS similitud
FROM documentos
ORDER BY similitud DESC
LIMIT 10;
-- Con 1 millón de documentos: varios segundos o minutos. Inviable en producción.

La solución que ofrecen las bases de datos vectoriales son los índices de búsqueda aproximada de vecinos más cercanos (Approximate Nearest Neighbor, ANN). En lugar de calcular la distancia exacta a todos los vectores, usan estructuras de datos especializadas (HNSW, IVF, etc.) que encuentran los vecinos más cercanos de forma aproximada pero muy rápida, sacrificando un pequeño porcentaje de precisión a cambio de velocidad.

Índices ANN: cómo funciona la magia

HNSW (Hierarchical Navigable Small World)

El algoritmo más popular actualmente. Construye un grafo jerárquico de los vectores: en los niveles superiores hay pocos nodos muy conectados (para navegar rápidamente de forma global) y en los niveles inferiores están todos los nodos con conexiones locales. La búsqueda empieza en el nivel superior, va bajando de nivel y en cada nivel se acerca al resultado.

# Parámetros de HNSW que controlan el balance precisión/velocidad:

# M: número de conexiones por nodo en el grafo
# Valores típicos: 8-64
# Mayor M → mejor precisión, más memoria, construcción más lenta
M = 16

# ef_construction: tamaño de la lista de candidatos durante la construcción
# Valores típicos: 64-500
# Mayor ef_construction → mejor calidad del índice, construcción más lenta
ef_construction = 200

# ef_search: tamaño de la lista de candidatos durante la búsqueda
# Valores típicos: 10-500, debe ser >= k (número de resultados que quieres)
# Mayor ef_search → mejor precisión en las búsquedas, más lento
ef_search = 50

# Con estos parámetros, HNSW puede buscar entre 1 millón de vectores
# en milisegundos con una precisión (recall) superior al 95%

Las opciones más populares

pgvector: vectores en PostgreSQL

Si ya usas PostgreSQL, pgvector es la opción más sencilla. Añade un tipo de dato vector y operadores de distancia, y permite crear índices HNSW o IVF sobre columnas vectoriales. No necesitas gestionar otro servicio.

-- Instalar la extensión (en PostgreSQL con pgvector disponible)
CREATE EXTENSION IF NOT EXISTS vector;

-- Crear una tabla con una columna de vectores
CREATE TABLE documentos (
  id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  contenido TEXT NOT NULL,
  metadata  JSONB,
  embedding vector(1536)   -- 1536 dimensiones (text-embedding-3-small de OpenAI)
);

-- Crear un índice HNSW para búsquedas eficientes
-- Usando distancia coseno (la más habitual para embeddings de texto)
CREATE INDEX idx_documentos_embedding
  ON documentos
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- Insertar documentos con sus embeddings (desde Python, el embedding ya calculado)
INSERT INTO documentos (contenido, metadata, embedding)
VALUES (
  'Los gatos son animales domésticos muy independientes',
  '{"categoria": "animales", "fuente": "enciclopedia"}'::jsonb,
  '[0.023, -0.145, 0.089, ...]'::vector   -- vector de 1536 dimensiones
);
-- Buscar los 5 documentos más similares a una consulta
-- (el vector de la consulta se calcula en la aplicación y se pasa como parámetro)
SELECT
  id,
  contenido,
  metadata,
  1 - (embedding <=> '[0.018, -0.132, 0.094, ...]'::vector) AS similitud
FROM documentos
ORDER BY embedding <=> '[0.018, -0.132, 0.094, ...]'::vector   -- <=> = distancia coseno
LIMIT 5;

-- Operadores de distancia disponibles en pgvector:
-- <=>  distancia coseno          (la más usada para texto)
-- <->  distancia euclidiana L2   (usada para imágenes y coordenadas)
-- <#>  producto interno negativo (equivalente a similitud coseno si los vectores están normalizados)

 

-- Filtrado combinado: búsqueda vectorial + filtro SQL normal
-- Buscar documentos sobre animales que sean similares a la consulta
SELECT id, contenido, 1 - (embedding <=> $1::vector) AS similitud
FROM documentos
WHERE metadata->>'categoria' = 'animales'   -- filtro SQL normal
ORDER BY embedding <=> $1::vector
LIMIT 10;

-- pgvector soporta el "pre-filtering": primero aplica el filtro SQL
-- y luego hace la búsqueda vectorial solo en ese subconjunto

 

pgvector desde Python con psycopg

# pip install psycopg[binary] pgvector openai

import psycopg
from pgvector.psycopg import register_vector
from openai import OpenAI

openai_client = OpenAI()

def obtener_embedding(texto: str) -> list[float]:
    return openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=texto
    ).data[0].embedding

# Conectar a PostgreSQL
conn = psycopg.connect("postgresql://usuario:password@localhost/mi_db")
register_vector(conn)   # registrar el tipo vector de pgvector

# Insertar un documento con su embedding
def insertar_documento(contenido: str, metadata: dict):
    embedding = obtener_embedding(contenido)
    conn.execute(
        "INSERT INTO documentos (contenido, metadata, embedding) VALUES (%s, %s, %s)",
        (contenido, psycopg.types.json.Jsonb(metadata), embedding)
    )
    conn.commit()

# Buscar los documentos más relevantes para una consulta
def buscar_similares(consulta: str, top_k: int = 5) -> list[dict]:
    embedding_consulta = obtener_embedding(consulta)
    resultados = conn.execute(
        """
        SELECT id, contenido, metadata,
               1 - (embedding <=> %s) AS similitud
        FROM documentos
        ORDER BY embedding <=> %s
        LIMIT %s
        """,
        (embedding_consulta, embedding_consulta, top_k)
    ).fetchall()

    return [
        {"id": r[0], "contenido": r[1], "metadata": r[2], "similitud": r[3]}
        for r in resultados
    ]

Chroma: la opción más sencilla para empezar

Chroma es una base de datos vectorial diseñada para ser simple de usar. Puedes ejecutarla en memoria, en disco o en modo cliente-servidor, y tiene una API muy directa. Es ideal para prototipos y proyectos pequeños.

# pip install chromadb openai

import chromadb
from openai import OpenAI

client = chromadb.Client()   # en memoria (reinicio limpia los datos)
# Para persistir en disco:
# client = chromadb.PersistentClient(path="./mi_base_chroma")

# Crear una colección (equivalente a una tabla)
coleccion = client.create_collection(
    name="documentos",
    metadata={"hnsw:space": "cosine"}   # usar distancia coseno
)

# Insertar documentos (Chroma puede calcular los embeddings automáticamente
# si le proporcionas una función de embedding, o puedes pasarlos ya calculados)
coleccion.add(
    documents=[
        "Python es un lenguaje de programación de alto nivel",
        "Los gatos son animales domésticos independientes",
        "El aprendizaje automático requiere grandes cantidades de datos",
        "Django es un framework web para Python",
        "Los perros son leales y fáciles de entrenar",
    ],
    ids=["doc1", "doc2", "doc3", "doc4", "doc5"],
    metadatas=[
        {"categoria": "programacion"},
        {"categoria": "animales"},
        {"categoria": "ia"},
        {"categoria": "programacion"},
        {"categoria": "animales"},
    ]
)

# Buscar documentos similares a una consulta
resultados = coleccion.query(
    query_texts=["frameworks para desarrollo web con Python"],
    n_results=3
)

for i, doc in enumerate(resultados["documents"][0]):
    distancia = resultados["distances"][0][i]
    print(f"[{distancia:.3f}] {doc}")

# Salida (ordenada por relevancia):
# [0.089] Django es un framework web para Python
# [0.234] Python es un lenguaje de programación de alto nivel
# [0.567] El aprendizaje automático requiere grandes cantidades de datos
# Filtrado por metadatos en Chroma
resultados_filtrados = coleccion.query(
    query_texts=["animales que se tienen en casa"],
    n_results=3,
    where={"categoria": "animales"}   # solo buscar en documentos de animales
)

Caso de uso central: RAG (Retrieval-Augmented Generation)

RAG es el patrón que permite a un modelo de lenguaje responder preguntas sobre documentos privados o datos que no estaban en su entrenamiento. El proceso es:

  1. Indexar los documentos: dividirlos en fragmentos, calcular los embeddings y guardarlos en la base de datos vectorial.
  2. Cuando llega una pregunta: calcular el embedding de la pregunta, buscar los fragmentos más relevantes en la base vectorial.
  3. Construir un prompt con la pregunta original + los fragmentos recuperados.
  4. Enviar el prompt al LLM y devolver la respuesta.
# Sistema RAG completo y funcional

import psycopg
from pgvector.psycopg import register_vector
from openai import OpenAI
import textwrap

openai_client = OpenAI()
conn = psycopg.connect("postgresql://usuario:password@localhost/mi_db")
register_vector(conn)

# ─── Paso 1: Indexar documentos ──────────────────────────────────────────────

def dividir_en_fragmentos(texto: str, tamanio: int = 500, solapamiento: int = 50) -> list[str]:
    """Divide texto largo en fragmentos con solapamiento para no perder contexto."""
    palabras = texto.split()
    fragmentos = []
    i = 0
    while i < len(palabras):
        fragmento = " ".join(palabras[i:i + tamanio])
        fragmentos.append(fragmento)
        i += tamanio - solapamiento
    return fragmentos

def indexar_documento(titulo: str, contenido: str, fuente: str):
    """Indexa un documento dividiéndolo en fragmentos y calculando sus embeddings."""
    fragmentos = dividir_en_fragmentos(contenido)

    for idx, fragmento in enumerate(fragmentos):
        embedding = openai_client.embeddings.create(
            model="text-embedding-3-small",
            input=fragmento
        ).data[0].embedding

        conn.execute(
            """INSERT INTO documentos (contenido, metadata, embedding)
               VALUES (%s, %s, %s)""",
            (
                fragmento,
                psycopg.types.json.Jsonb({
                    "titulo": titulo,
                    "fuente": fuente,
                    "fragmento_num": idx
                }),
                embedding
            )
        )
    conn.commit()
    print(f"Indexados {len(fragmentos)} fragmentos de '{titulo}'")


# ─── Paso 2: Buscar fragmentos relevantes ────────────────────────────────────

def recuperar_contexto(pregunta: str, top_k: int = 4) -> list[dict]:
    """Busca los fragmentos más relevantes para la pregunta."""
    embedding_pregunta = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=pregunta
    ).data[0].embedding

    resultados = conn.execute(
        """
        SELECT contenido, metadata, 1 - (embedding <=> %s) AS similitud
        FROM documentos
        WHERE 1 - (embedding <=> %s) > 0.70   -- umbral mínimo de relevancia
        ORDER BY embedding <=> %s
        LIMIT %s
        """,
        (embedding_pregunta, embedding_pregunta, embedding_pregunta, top_k)
    ).fetchall()

    return [
        {"contenido": r[0], "metadata": r[1], "similitud": r[2]}
        for r in resultados
    ]


# ─── Paso 3: Generar la respuesta con el LLM ─────────────────────────────────

def responder(pregunta: str) -> str:
    """Pipeline RAG completo: recupera contexto y genera respuesta."""
    fragmentos = recuperar_contexto(pregunta)

    if not fragmentos:
        return "No encontré información relevante para responder tu pregunta."

    # Construir el contexto con los fragmentos recuperados
    contexto = "\n\n---\n\n".join([
        f"[Fuente: {f['metadata']['titulo']} | Relevancia: {f['similitud']:.2f}]\n{f['contenido']}"
        for f in fragmentos
    ])

    # Construir el prompt para el LLM
    prompt_sistema = """Eres un asistente experto que responde preguntas basándote
exclusivamente en el contexto proporcionado. Si la información no está en el contexto,
dilo claramente en lugar de inventar una respuesta."""

    prompt_usuario = f"""Contexto:
{contexto}

Pregunta: {pregunta}

Responde de forma concisa basándote solo en el contexto anterior."""

    respuesta = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": prompt_sistema},
            {"role": "user",   "content": prompt_usuario}
        ],
        temperature=0.1   # baja temperatura para respuestas más precisas
    )

    return respuesta.choices[0].message.content


# ─── Uso ──────────────────────────────────────────────────────────────────────

# Indexar documentos
indexar_documento(
    titulo="Manual de empleados",
    contenido="La política de vacaciones permite 22 días laborables por año...",
    fuente="RRHH"
)

# Hacer preguntas
print(responder("¿Cuántos días de vacaciones tengo?"))
print(responder("¿Cuál es el proceso para pedir una baja médica?"))

 

Otras bases de datos vectoriales populares

Pinecone es un servicio gestionado en la nube: no tienes que operar ninguna infraestructura. Tiene una API simple, escala automáticamente y es la elección habitual para proyectos que no quieren gestionar servidores. El plan gratuito es suficiente para prototipos.

# pip install pinecone-client openai

from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key="tu-api-key")

# Crear un índice
pc.create_index(
    name="documentos",
    dimension=1536,   # debe coincidir con las dimensiones del modelo de embedding
    metric="cosine",
    spec=ServerlessSpec(cloud="aws", region="us-east-1")
)

indice = pc.Index("documentos")

# Insertar vectores
indice.upsert(vectors=[
    {"id": "doc1", "values": embedding1, "metadata": {"texto": "...", "fuente": "manual"}},
    {"id": "doc2", "values": embedding2, "metadata": {"texto": "...", "fuente": "blog"}},
])

# Buscar
resultados = indice.query(
    vector=embedding_consulta,
    top_k=5,
    include_metadata=True,
    filter={"fuente": {"$eq": "manual"}}   # filtrado por metadatos
)

Weaviate es una base de datos vectorial open source que también se puede usar como servicio gestionado. Tiene un esquema más estructurado que Chroma, soporta múltiples módulos de embedding integrados y ofrece un lenguaje de consulta propio (GraphQL) además de REST.

Qdrant es otra opción open source escrita en Rust, muy rápida y con soporte para filtros avanzados en las búsquedas vectoriales. Tiene buena integración con LangChain y LlamaIndex.

Cómo elegir la base de datos vectorial para tu proyecto

OpciónCuándo usarlaDesventajas
pgvectorYa usas PostgreSQL, quieres datos vectoriales junto a datos relacionales en el mismo sitioMenor rendimiento que soluciones dedicadas a muy gran escala
ChromaPrototipo, desarrollo local, proyecto pequeñoNo pensado para producción a gran escala
PineconeProducción sin querer gestionar infraestructura, proyectos con crecimiento rápidoCoste, vendor lock-in
QdrantOpen source, producción, necesitas filtros avanzadosHay que operar la infraestructura
WeaviateOpen source, esquema estructurado, integración con múltiples modelosMayor complejidad de configuración

Para la mayoría de proyectos nuevos que ya usan PostgreSQL, pgvector es el punto de partida más pragmático: simplifica la arquitectura al no añadir otro servicio, y cuando el proyecto crezca y necesite más escala, migrar a una solución dedicada es manejable.

Resumen

  • Los embeddings son representaciones numéricas de texto (o imágenes, audio) como vectores de cientos o miles de dimensiones. Textos semánticamente similares tienen vectores cercanos en el espacio matemático.
  • Las bases de datos tradicionales no pueden buscar eficientemente por similitud semántica porque no tienen índices para comparar vectores de alta dimensión.
  • Las bases de datos vectoriales usan índices ANN (como HNSW) que encuentran los vecinos más cercanos de forma aproximada pero muy rápida, sacrificando un pequeño porcentaje de precisión a cambio de milisegundos de respuesta.
  • pgvector añade soporte vectorial a PostgreSQL con el tipo vector, índices HNSW/IVF y operadores de distancia (<=>, <->). Es la opción más simple si ya usas PostgreSQL.
  • Chroma es la opción más sencilla para prototipos. Pinecone es la opción gestionada para producción. Qdrant y Weaviate son las mejores opciones open source para producción.
  • El caso de uso más importante actualmente es RAG: indexar documentos privados y recuperar los fragmentos relevantes para enriquecer el contexto de un LLM, permitiéndole responder preguntas sobre datos que no estaban en su entrenamiento.
Etiquetas: NoSQL

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a