홈으로 돌아가기

PostgreSQL에서의 하이브리드 검색 | VectorChord 설정

이 기사는 PostgreSQL과 VectorChord를 사용한 하이브리드 검색 인프라 설정을 설명합니다. 테이블 설계, 자동 확장 설치, 데이터 처리 파이프라인 구성 등을 다룹니다. 고급 검색 시스템을 개발하는 개발자를 위한 자료입니다.

VectorChord를 사용한 PostgreSQL 하이브리드 검색 설정 방법
Advertisement 728x90

PostgreSQL과 VectorChord를 활용한 하이브리드 검색: 인프라 설정

벡터 검색과 전체 텍스트 검색을 결합한 하이브리드 검색은 현대 검색 시스템의 표준으로 자리 잡고 있습니다. 이 글에서는 PostgreSQL과 VectorChord를 기반으로 인프라를 설정하는 방법을 자세히 살펴보겠습니다. 이는 수동 확장 설치나 복잡한 설정 없이 하이브리드 검색 구현을 간편하게 만들어주는 솔루션입니다.

환경 설정: Docker와 PostgreSQL

시작하기 위해 컨테이너화된 환경이 필요합니다. Docker Compose를 사용해 VectorChord 확장이 미리 설치된 PostgreSQL을 배포하겠습니다. 주의: 설정에서 확장을 명시적으로 지정하지 않습니다. 첫 연결 시 VechordRegistry를 통해 자동으로 추가됩니다.

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:

핵심 포인트: tensorchord/vchord-suite 이미지는 모든 필수 구성 요소를 포함하고 있지만, 확장은 VechordRegistry를 통한 첫 접근 시에만 활성화됩니다. 이를 통해 수동 설정을 피하고 버전 호환성을 보장합니다.

하이브리드 검색을 위한 테이블 설계

데이터 구조는 세 개의 테이블로 구성됩니다. 기본 테이블 BaseTable은 공통 필드를 포함합니다:

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

metadata 필드는 JSONB 형식으로 임의 데이터를 저장하며, 타임스탬프는 레코드 생성 시 자동 업데이트됩니다. 주의: updated_at은 레코드 변경 시 자동 업데이트되지 않으므로 업데이트 메서드에서 수동으로 처리해야 합니다.

Google AdInline article slot

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

기본 키로 UUID를 사용합니다. Chunk 테이블은 외래 키를 통해 문서와 연결됩니다:

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

여기서 content_tsv는 전체 텍스트 검색(BM25)을 위해 사용되며, embedding은 청크의 벡터 표현을 저장합니다. 외래 키 doc_id는 자동으로 캐스케이드 삭제를 구현합니다.

Google AdInline article slot

중요: uid를 기본 클래스에 이동하려 하면 오류가 발생합니다. ForeignKeyBaseTable에서 필드를 찾기 때문입니다. 따라서 각 테이블마다 기본 키를 중복 정의해야 합니다.

VechordRegistry: 중앙화된 데이터베이스 관리

VechordRegistry 클래스는 데이터베이스 작업을 위한 단일 인터페이스를 제공합니다. 이를 통해 자동화되는 작업은 다음과 같습니다:

  • 확장 설치(vchord, vchord_bm25, pg_tokenizer)
  • 테이블 및 토크나이저 생성
  • 데이터 생성/조회/수정/삭제 작업 및 검색

초기화:

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

첫 사용 시(async with vr: 컨텍스트에서) init_extension 메서드가 실행되어 확장을 설치하고 search_path를 설정합니다. 이를 통해 수동 설정이 필요 없으며 구성 오류를 최소화합니다.

문서 삽입 예시:

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

이 코드를 실행하면 모든 필수 확장, 테이블, 레코드가 데이터베이스에 생성됩니다.

데이터 처리 파이프라인

문서 생성 후 청킹 같은 복잡한 작업을 위해 VechordPipeline을 사용합니다. 여러 단계를 파이프라인으로 결합하며, 각 단계에서 결과가 자동으로 데이터베이스에 저장됩니다.

단계 1: 문서 생성. @vr.inject(output=Document) 데코레이터가 객체를 자동 저장합니다:

@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

단계 2: 청크 생성. Document 테이블의 데이터를 가져와 청크 목록을 반환합니다:

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

파이프라인 조립:

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)

이 접근 방식은 작업의 원자성을 보장하고 데이터베이스 접근을 최소화합니다.

핵심 포인트: 필수 구현 사항

VectorChord 기반 하이브리드 검색 구현 시 주의할 점:

  • 자동 확장 초기화: VechordRegistry가 첫 연결 시 모든 필수 구성 요소를 설치하여 배포를 간소화합니다.
  • 테이블 구조: 문서와 청크를 분리하는 것이 효율적인 검색에 핵심이며, 외래 키가 데이터 무결성을 보장합니다.
  • 처리 파이프라인: VechordPipeline을 사용해 복잡한 작업을 캡슐화하고 데이터 일관성을 유지합니다.

이러한 요소들은 벡터 검색의 정밀성과 전체 텍스트 검색의 유연성을 결합한 확장 가능한 검색 시스템의 기반을 형성합니다.

— Editorial Team

Advertisement 728x90

다음 읽기