Búsqueda híbrida en PostgreSQL y VectorChord: Configuración de la infraestructura
La búsqueda híbrida, que combina métodos vectoriales y de texto completo, se está convirtiendo en el estándar para los sistemas de búsqueda modernos. En este artículo, profundizaremos en la configuración de la infraestructura basada en PostgreSQL y VectorChord — una solución que simplifica la implementación de búsqueda híbrida sin necesidad de instalar extensiones manualmente ni configuraciones complejas.
Configuración del entorno: Docker y PostgreSQL
Para empezar, necesitaremos un entorno contenedorizado. Usaremos Docker Compose para desplegar PostgreSQL con las extensiones de VectorChord preinstaladas. Nota: en la configuración, no especificamos las extensiones de forma explícita — se añadirán automáticamente en la primera conexión mediante VechordRegistry.
Ejemplo del archivo docker-compose-dev.yml:
services:
postgres:
image: tensorchord/vchord-suite:pg18-latest
environment:
POSTGRES_DB: ${DB__NAME}
POSTGRES_USER: ${DB__USER}
POSTGRES_PASSWORD: ${DB__PASSWORD}
volumes:
- pgdata:/var/lib/postgresql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB__USER} -d ${DB__NAME}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Punto clave: la imagen tensorchord/vchord-suite ya contiene todos los componentes necesarios, pero las extensiones se activan solo en el primer acceso mediante VechordRegistry. Esto evita la configuración manual y garantiza la compatibilidad de versiones.
Diseño de tablas para búsqueda híbrida
La estructura de datos se basa en tres tablas. La tabla base BaseTable contiene campos comunes:
from datetime import datetime, timezone
from functools import partial
import msgspec
from psycopg.types.json import Jsonb
from vechord import Table
class BaseTable(Table, kw_only=True):
metadata: Jsonb
created_at: datetime = msgspec.field(
default_factory=partial(datetime.now, timezone.utc))
updated_at: datetime = msgspec.field(
default_factory=partial(datetime.now, timezone.utc))
El campo metadata almacena datos arbitrarios en formato JSONB, y las marcas de tiempo se actualizan automáticamente al crear el registro. Nota: updated_at no se actualiza automáticamente en cambios de registro — esto debe hacerse manualmente en los métodos de actualización.
La tabla Document:
import msgspec
from vechord.spec import PrimaryKeyUUID
from .base import BaseTable
class Document(BaseTable, kw_only=True):
uid: PrimaryKeyUUID = msgspec.field(default_factory=PrimaryKeyUUID.factory)
title: str
text: str
Se usa UUID para la clave primaria. La tabla Chunk está vinculada al documento mediante una clave foránea:
from vechord.spec import ForeignKey, Keyword, PrimaryKeyUUID, Vector
class Chunk(BaseTable, kw_only=True):
uid: PrimaryKeyUUID = msgspec.field(default_factory=PrimaryKeyUUID.factory)
doc_id: Annotated[UUID, ForeignKey[Document.uid]]
content: str
content_tsv: Keyword
embedding: DenseVector
chunk_index: int
Aquí, content_tsv está destinado a la búsqueda de texto completo (BM25), y embedding almacena la representación vectorial del chunk. La clave foránea doc_id implementa automáticamente la eliminación en cascada.
Importante: intentar mover uid a la clase base provoca un error, ya que ForeignKey comienza a buscar el campo en BaseTable en lugar de en la tabla específica. Por eso, la clave primaria se duplica en cada tabla.
VechordRegistry: Gestión centralizada de la base de datos
La clase VechordRegistry actúa como una interfaz única para trabajar con la base de datos. Automatiza:
- Instalación de extensiones (
vchord,vchord_bm25,pg_tokenizer) - Creación de tablas y tokenizadores
- Operaciones CRUD y búsqueda
Inicialización:
from vechord import VechordRegistry
from core import settings
from .document import Document
from .doc_chunk import Chunk
vr = VechordRegistry(
namespace=settings.db.namespace,
url=settings.db.db_url,
tables=[Document, Chunk]
)
En el primer uso (en el contexto async with vr:), se ejecuta el método init_extension, que instala las extensiones y configura search_path. Esto elimina la necesidad de configuración manual y minimiza errores de configuración.
Ejemplo de uso para insertar un documento:
async with vr:
doc = Document(title='Note', text='Some text', metadata=Jsonb({}))
await vr.insert(doc)
Tras ejecutar este código, aparecerán en la BD todas las extensiones necesarias, tablas y el registro.
Tuberías de procesamiento de datos
Para operaciones complejas como crear un documento seguido de su fragmentación, se usa VechordPipeline. Permite combinar múltiples pasos en una tubería, donde cada etapa guarda automáticamente los resultados en la BD.
Paso 1: Creación del documento. El decorador @vr.inject(output=Document) asegura la persistencia del objeto:
@vr.inject(output=Document)
async def _create_document(doc_data: DocumentCreate) -> Document:
doc = Document(
title=doc_data.title,
text=doc_data.text,
metadata=Jsonb(doc_data.metadata)
)
return doc
Paso 2: Generación de chunks. Toma datos de la tabla Document y devuelve una lista de chunks:
@vr.inject(input=Document, output=Chunk)
async def create_chunks(uid: UUID, text: str) -> list[Chunk]:
chunks = await _chunker.segment(text)
return [
Chunk(
doc_id=uid,
content=chunk,
content_tsv=Keyword(chunk),
embedding=DenseVector(await _embedder.vectorize_chunk(chunk)),
metadata=Jsonb({}),
chunk_index=i
)
for i, chunk in enumerate(chunks, start=1)
]
Ensamblaje de la tubería:
from db_models import vr
from .utils import create_chunks, create_document
class DocumentService:
async def create_document(self, doc_data: DocumentCreate):
pipeline = vr.pipeline(
_create_document,
create_chunks
)
await pipeline(doc_data)
Este enfoque garantiza la atomicidad de las operaciones y minimiza los accesos a la BD.
Puntos clave: Aspectos esenciales de la implementación
Al implementar búsqueda híbrida basada en VectorChord, presta atención a:
- Inicialización automática de extensiones: VechordRegistry instala todos los componentes necesarios en la primera conexión, simplificando el despliegue.
- Estructura de tablas: separar documentos y chunks es crucial para una búsqueda eficiente; las claves foráneas garantizan la integridad de los datos.
- Tuberías de procesamiento: usar
VechordPipelineencapsula operaciones complejas y asegura la consistencia de los datos.
Estos elementos forman la base de un sistema de búsqueda escalable que combina la precisión de la búsqueda vectorial con la flexibilidad de la búsqueda de texto completo.
— Editorial Team
Aún no hay comentarios.