Volver al inicio

Búsqueda Híbrida en PostgreSQL | Configuración de VectorChord

El artículo describe la configuración de infraestructura para búsqueda híbrida utilizando PostgreSQL y VectorChord. Cubre el diseño de tablas, la instalación automática de extensiones y la organización de pipelines de procesamiento de datos. El material está destinado a desarrolladores que crean sistemas de búsqueda avanzados.

Cómo Configurar Búsqueda Híbrida en PostgreSQL con VectorChord
Advertisement 728x90

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:

Google AdInline article slot
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.

Google AdInline article slot

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.

Google AdInline article slot

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 VechordPipeline encapsula 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

Advertisement 728x90

Leer después