logo

PostgreSQL con pgvector como base de datos vectorial para RAG

Cómo implementar búsquedas vectoriales y RAG usando PostgreSQL

7 de octubre de 2024

Hemos explorado la búsqueda vectorial y Retrieval-Augmented Generation (RAG) en artículos anteriores, como RAG desde cero y Crea un chatbot con IA alimentado por tus datos. Esta última incluye una implementación completa de un chatbot con RAG usando Redis como base de datos vectorial.

Pero hay otra base de datos muy conocida y que probablemente hayas usado: PostgreSQL. ¿Y si pudiéramos utilizarla también como base de datos vectorial?

PostgreSQL es una base de datos relacional de código abierto ampliamente utilizada. Es increíblemente versátil, permitiendo además almacenar y manipular datos en formato JSON (similar a las bases de datos NoSQL y documentales) y ofreciendo numerosas extensiones con funcionalidades adicionales, como PostGIS para datos geoespaciales o pgcron para programar tareas.

Gracias a la extensión pgvector, Postgres también puede realizar búsquedas de similitud vectorial de forma eficiente. Esto abre grandes posibilidades para aplicaciones de RAG y de IA, con la ventaja añadida de utilizar una base de datos familiar que probablemente ya tengas en tu stack. También significa que puedes combinar datos relacionales, datos JSON y vectores (embeddings) en un mismo sistema, permitiendo consultas complejas que involucren tanto datos estructurados como búsquedas vectoriales.

Existen bases de datos vectoriales especializadas como Qdrant, Pinecone o Weaviate, que ofrecen un rendimiento optimizado para grandes volúmenes de datos así como funcionalidades más avanzadas. Sin embargo, Postgres con pgvector es una alternativa muy interesante si quieres mantener todos los datos de tu aplicación más integrados y minimizar el número de bases de datos en tu infraestructura, reduciendo costes y complejidad.

En este artículo, exploraremos cómo configurar Postgres como base de datos vectorial y cómo utilizarla en búsquedas vectoriales y aplicaciones de RAG en Python.

#Configuración de PostgreSQL y pgvector

Antes de comenzar, necesitamos instalar PostgreSQL, pgvector y las librerías de Python que usaremos:

  1. Descarga e instala PostgreSQL siguiendo las instrucciones oficiales para tu sistema operativo.

  2. Instala la extensión pgvector siguiendo las notas de instalación en el repositorio de pgvector.

  3. Instala las dependencias de Python necesarias. Además de la librería para usar pgvector en Python, utilizaremos SQLAlchemy como ORM y asyncpg como driver para conectarnos a Postgres de forma asíncrona con asyncio:

    pip install sqlalchemy pgvector asyncpg
  4. Crea una nueva base de datos y habilita la extensión pgvector:

    createdb rag_db
    psql rag_db
    CREATE EXTENSION vector;

#Creación de una base de datos vectorial con PostgreSQL

Ahora que la base de datos está configurada, vamos a crear un modelo de SQLAlchemy para representar nuestros datos vectoriales:

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Text
from sqlalchemy.dialects.postgresql import JSONB
from pgvector.sqlalchemy import Vector

class Base(DeclarativeBase):
pass

class Vector(Base):
__tablename__ = 'vectors'

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
text: Mapped[str] = mapped_column(Text)
vector = mapped_column(Vector(1024))
metadata_: Mapped[dict | None] = mapped_column('metadata', JSONB)

def __repr__(self):
return f'Vector(id={self.id}, text={self.text[:50]}..., metadata={self.metadata_})'

Estamos utilizando la versión 2.0 de SQLAlchemy, que nos permite usar las "anotaciones de tipo" (type hints) de Python con Mapped y mapped_column para inferir los tipos de las columnas de la base de datos.

El modelo Vector define las siguientes columnas en la tabla vectors:

⚠️Ten en cuenta que usamos "metadata_" como nombre del atributo en el modelo porque "metadata" es una palabra reservada en los modelos de SQLAlchemy, pero el nombre de la columna en la base de datos sí será "metadata".

Para crear la tabla de la base de datos definida en nuestro modelo, podemos usar el siguiente código de SQLAlchemy:

from sqlalchemy.ext.asyncio import create_async_engine

DB_URL = 'postgresql+asyncpg://admin:postgres@localhost:5432/rag_db'

engine = create_async_engine(DB_URL)

async def db_create():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

El prefijo 'postgresql+asyncpg' en la URL de la base de datos es necesario porque estamos usando el driver asyncpg para habilitar conexiones asíncronas con asyncio.

Una vez creada la tabla vectors, puedes usar pgAdmin para explorar la estructura de la tabla y ejecutar consultas con una interfaz gráfica.

#Almacenando documentos como embeddings en PostgreSQL

El siguiente paso es procesar, vectorizar (generando embeddings) y almacenar la información en nuestra base de datos vectorial Postgres. Si estás familiarizado con sistemas RAG, este proceso incluye los siguientes pasos:

Veamos una función de ejemplo que realiza estos pasos:

engine = create_async_engine(DB_URL)
Session = async_sessionmaker(engine, expire_on_commit=False)

async def add_document_to_vector_db(doc_path):
text = extract_text(docpath)
doc_name = os.path.splitext(os.path.basename(doc_path))[0]

chunks = []
text_splitter = TextSplitter(chunk_size=512)
text_chunks = text_splitter.split(text)
for idx, text_chunk in enumerate(text_chunks):
chunks.append({
'text': text_chunk,
'metadata_': {'doc': doc_name, 'index': idx}
})

vectors = await create_embeddings([chunk['text'] for chunk in chunks])

for chunk, vector in zip(chunks, vectors):
chunk['vector'] = vector

async with Session() as db:
for chunk in chunks:
db.add(Vector(**chunk))
await db.commit()

En el código anterior:

Para generar estos embeddings, puedes utilizar el modelo que prefieras. Pero asegúrate de que las dimensiones del modelo de embedding coincidan con las dimensiones de la columna vector del modelo (1024 en nuestro ejemplo).

Aquí tienes un ejemplo de implementación usando el modelo text-embedding-3-large de OpenAI:

from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ['OPENAI_API_KEY'])

async def get_embeddings(input):
res = await client.embeddings.create(input=input, model='text-embedding-3-large', dimensions=1024)
return [item.embedding for item in res.data]

#Búsqueda vectorial con PostgreSQL

Ahora que tenemos los documentos vectorizados y almacenados en PostgreSQL, podemos realizar búsquedas vectoriales para extraer la información más relevante para nuestras consultas. Este es un paso clave en la construcción de un sistema de Retrieval-Augmented Generation (RAG).

La siguiente función muestra cómo podemos implementar una búsqueda de similitud vectorial en PostgreSQL con pgvector:

async def vector_search(query_vector, top_k=3):
async with Session() as db:
query = (
select(Vector.text, Vector.metadata_, Vector.vector.cosine_distance(query_vector).label('distance'))
.order_by('distance')
.limit(top_k)
)
res = await db.execute(query)
return [{
'text': text,
'metadata': metadata,
'score': 1 - distance
} for text, metadata, distance in res]

Analicemos esta función y el proceso de búsqueda:

Es importante señalar que, por defecto, pgvector realiza búsquedas exactas de vecinos más próximos (nearest neighbor search), lo que garantiza un recall perfecto (encuentra todos los vecinos más cercanos), pero puede resultar más lento cuando el volumen de datos es elevado.

En esos casos, también podemos crear un índice para acelerar las búsquedas y sacrificar un poco de exactitud a cambio de búsquedas más rápidas. Los índices de este tipo disponibles en pgvector son IVFFlat (Inverted File Flat) y HNSW (Hierarchical Navigable Small World). Puedes leer más sobre estos índices aquí.

#RAG en acción

Con la funcionalidad de búsqueda vectorial lista, podemos ya integrarlo todo para crear un ejemplo básico de RAG, usando el modelo GPT-4o de OpenAI, que responda preguntas sobre los documentos que hemos procesado y almacenado en la base de datos.

Primero, veamos los prompts que vamos a utilizar:

SYSTEM_PROMPT = """
You are an AI assistant that answers questions about documents in your knowledge base.
"""

RAG_PROMPT = """
Use the following pieces of context to answer the user question.
You must only use the facts from the context to answer.
If the answer cannot be found in the context, say that you don't have enough information to answer the question and provide any relevant facts found in the context.

Context:
{context}

User Question:
{question}
"""

Y así es como podemos implementar un sistema RAG básico:

from openai import AsyncOpenAI

client = AsyncOpenAI(api_key=os.environ['OPENAI_API_KEY'])

async def answer_question_with_rag(question):
query_vector = await get_embedding(question)
top_chunks = await vector_search(query_vector, top_k=3)
context = '\n\n---\n\n'.join([chunk['text'] for chunk in top_chunks]) + '\n\n---'
user_message = RAG_PROMPT.format(context=context, question=question)
messages=[
{'role': 'system', 'content': SYSTEM_PROMPT},
{'role': 'user', 'content': user_message}
]
response = await client.chat.completions.create(model='gpt-4o', messages=messages)
return response.choices[0].message.content

Esta función muestra cómo funciona la técnica RAG: convierte la pregunta del usuario en un vector embedding, realiza una búsqueda de similitud vectorial en Postgres y añade la información extraída como contexto para que GPT-4o genere una respuesta fundamentada.

Puedes usarla así:

question = "What are the main challenges in renewable energy adoption?"
answer = await answer_question_with_rag(question)
print(answer)

Ahora puedes adaptar todo el código que hemos visto a tus propias aplicaciones. Puedes procesar y almacenar tus propios documentos y usar la combinación de PostgreSQL con pgvector y GPT-4o (o cualquier otro LLM de tu elección) para responder preguntas basadas en esos documentos.

Y también puedes aprovechar estas ideas para construir aplicaciones más avanzadas, como chatbots o asistentes de IA, con una arquitectura simple que se beneficia de la potencia y versatilidad de PostgreSQL, manteniendo todos tus datos integrados en un único lugar.