Powrót do strony głównej

Hybrydowe wyszukiwanie w PostgreSQL | Konfiguracja VectorChord

Artykuł opisuje konfigurację infrastruktury dla hybrydowego wyszukiwania z wykorzystaniem PostgreSQL i VectorChord. Omówiono projektowanie tabel, automatyczną instalację rozszerzeń i organizację potoków przetwarzania danych. Materiał jest przeznaczony dla programistów tworzących zaawansowane systemy wyszukiwania.

Jak skonfigurować hybrydowe wyszukiwanie w PostgreSQL z VectorChord
Advertisement 728x90

# Hybrydowe wyszukiwanie w PostgreSQL i VectorChord: konfiguracja infrastruktury

Hybrydowe wyszukiwanie, łączące metody wektorowe i pełnotekstowe, staje się standardem dla nowoczesnych systemów wyszukiwania. W tym artykule szczegółowo omówimy konfigurację infrastruktury opartej na PostgreSQL i VectorChord — rozwiązaniu, które upraszcza implementację hybrydowego wyszukiwania bez konieczności ręcznej instalacji rozszerzeń i skomplikowanej konfiguracji.

Przygotowanie środowiska: Docker i PostgreSQL

Na początek potrzebujemy środowiska konteneryzowanego. Użyjemy Docker Compose do uruchomienia PostgreSQL z preinstalowanymi rozszerzeniami VectorChord. Uwaga: w konfiguracji nie wskazujemy rozszerzeń explicite — zostaną one dodane automatycznie przy pierwszym połączeniu przez VechordRegistry.

Przykład pliku 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:

Kluczowy moment: obraz tensorchord/vchord-suite już zawiera wszystkie niezbędne komponenty, ale rozszerzenia są aktywowane dopiero przy pierwszym obraschenii przez VechordRegistry. Dzięki temu unikamy ręcznej konfiguracji i zapewniamy kompatybilność wersji.

Projektowanie tabel dla hybrydowego wyszukiwania

Struktura danych opiera się na trzech tabelach. Bazowa tabela BaseTable zawiera pola wspólne:

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))

Pole metadata przechowuje dowolne dane w formacie JSONB, a znaczniki czasowe są aktualizowane automatycznie przy tworzeniu rekordu. Uwaga: updated_at nie jest aktualizowane automatycznie przy zmianie rekordu — należy to zrobić ręcznie w metodach aktualizacji.

Google AdInline article slot

Tabela dokumentów 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

Dla klucza głównego używany jest UUID. Tabela chunków Chunk jest powiązana z dokumentem przez klucz obcy:

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

Tutaj content_tsv służy do wyszukiwania pełnotekstowego (BM25), a embedding przechowuje wektorowe predstavlenie chunku. Klucz obcy doc_id automatycznie realizuje kaskadowe usuwanie.

Google AdInline article slot

Ważne: próba wyprowadzenia uid do klasy bazowej powoduje błąd, ponieważ ForeignKey zaczyna szukać pola w BaseTable, a nie w konkretnej tabeli. Dlatego klucz główny jest duplikowany w każdej tabeli.

VechordRegistry: scentralizowane zarządzanie bazą danych

Klasa VechordRegistry służy jako jednolity interfejs do pracy z bazą. Automatyzuje:

  • Instalację rozszerzeń (vchord, vchord_bm25, pg_tokenizer)
  • Tworzenie tabel i tokenizatorów
  • Wykonywanie operacji CRUD i wyszukiwania

Inicjalizacja:

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]
)

Przy pierwszym użyciu (w kontekście async with vr:) uruchamiana jest metoda init_extension, która instaluje rozszerzenia i konfiguruje search_path. To eliminuje konieczność ręcznej konfiguracji i minimalizuje błędy.

Przykład użycia do wstawienia dokumentu:

async with vr:
    doc = Document(title='Note', text='Some text', metadata=Jsonb({}))
    await vr.insert(doc)

Po wykonaniu tego kodu w bazie pojawią się wszystkie niezbędne rozszerzenia, tabele i rekord.

Pipeliny przetwarzania danych

Do złożonych operacji, takich jak tworzenie dokumentu z późniejszym chunkowaniem, używa się VechordPipeline. Pozwala on połączyć kilka kroków w konvejery, gdzie każdy etap automatycznie zapisuje wyniki do bazy.

Krok 1: Tworzenie dokumentu. Dekorator @vr.inject(output=Document) gwarantuje zapis obiektu:

@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

Krok 2: Generowanie chunków. Przyjmuje dane z tabeli Document i zwraca listę chunków:

@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)
    ]

Montaż pipeliny:

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)

To podejście zapewnia atomowość operacji i minimalizuje liczbę zapytań do bazy.

Co ważne: kluczowe aspekty implementacji

Przy wdrażaniu hybrydowego wyszukiwania opartego na VectorChord zwróć uwagę na:

  • Automatyczna inicjalizacja rozszerzeń: VechordRegistry instaluje wszystkie niezbędne komponenty przy pierwszym połączeniu, co upraszcza wdrożenie.
  • Struktura tabel: podział na dokumenty i chunki jest kluczowy dla efektywnego wyszukiwania; klucze obce zapewniają integralność danych.
  • Pipeliny przetwarzania: użycie VechordPipeline pozwala enkapsulować złożone operacje i gwarantować spójność danych.

Te elementy tworzą podstawę skalowalnego systemu wyszukiwania, łączącego precyzję wyszukiwania wektorowego z elastycznością pełnotekstowego.

— Editorial Team

Advertisement 728x90

Czytaj dalej