PostgreSQL에서 Full-Text 및 Vector 유사도를 활용한 하이브리드 검색 구현하기
요약
본 가이드는 RAG 시스템의 핵심인 하이브리드 검색을 PostgreSQL 환경에서 구현하는 방법을 다룹니다. 벡터 유사도와 Full-text 검색의 장점을 결합하여, 의미적 이해력과 키워드의 정확한 일치라는 두 마리 토끼를 잡는 것이 목표입니다. Reciprocal Rank Fusion (RRF) 기법을 활용해 성능 높은 하이브리드 쿼리를 구성하는 방법을 상세히 설명합니다.
핵심 포인트
- 하이브리드 검색은 벡터와 Full-text의 약점을 보완하여 RAG 시스템의 정확도를 높인다.
- PostgreSQL과 pgvector 확장을 사용하면 단일 DB에서 두 가지 검색을 결합할 수 있다.
- Reciprocal Rank Fusion (RRF)는 하이브리드 쿼리의 성능을 극대화하는 핵심 기법이다.
- 정확한 키워드(SKU 등)와 의미적 유사성 모두를 포착해야 한다.
우리가 실제로 해결하려는 검색 문제
만약 RAG(Retrieval-Augmented Generation) 파이프라인, 시맨틱 검색 엔진 또는 지능형 문서 검색 시스템 등 어떤 최신 AI 애플리케이션을 구축하고 있다면, 근본적인 긴장 관계에 직면했을 가능성이 높습니다:
Vector 유사도 검색은 _의미_를 이해하지만 정확한 키워드 일치는 놓칠 수 있습니다.
Full-text 검색은 정확한 용어를 포착하지만 시맨틱 관계를 이해하지 못합니다.
만약 이 두 가지를 모두 가질 수 있다면 어떨까요?
이것이 바로 **하이브리드 검색(hybrid search)**의 약속입니다. 벡터 임베딩의 시맨틱 이해력과 전통적인 full-text 검색의 정밀도를 pgvector 확장을 사용하는 단일 PostgreSQL 데이터베이스 내에서 결합하는 것입니다.
본 포괄적인 가이드에서는 작동하는 하이브리드 검색 시스템을 처음부터 구축하고, 그 성능 특성을 분석하며, 왜 이것이 작동하는지—그리고 사용자의 AI 엔지니어링 프로젝트에 언제 구현을 고려해야 하는지 정확히 이해할 것입니다.
목차
- AI 애플리케이션에서 하이브리드 검색이 중요한 이유
- 검색의 두 가지 기둥 이해하기
- PostgreSQL 환경 설정하기
- 데이터셋 구축: 무작위 텍스트부터 임베딩까지
- 인덱싱 전략: GIN 및 HNSW 설명
- Vector 검색 구현하기
- Full-Text 검색 구현하기
- Reciprocal Rank Fusion (RRF)의 마법
- 하이브리드 검색 쿼리 구성하기
- EXPLAIN ANALYZE를 사용한 성능 분석
- 프로덕션 고려 사항 및 트레이드오프
- 다음 단계 및 미래 연구
1. AI 애플리케이션에서 하이브리드 검색이 중요한 이유 {#why-hybrid-search-matters}
RAG 혁명과 그 검색 문제
검색 증강 생성(Retrieval-Augmented Generation, RAG)은 외부 지식에 접근해야 하는 AI 애플리케이션을 구축하는 지배적인 패러다임이 되었습니다. 이 패턴은 겉보기에는 매우 간단합니다:
- 사용자가 질문을 합니다.
- 시스템이 관련 문서를 검색(retrieve)합니다.
- LLM이 검색된 컨텍스트를 사용하여 답변을 생성합니다.
여기서 2단계, 즉 검색(retrieval)의 품질이 전체 시스템의 성공 여부를 결정하는 경우가 많습니다. 그리고 불편한 진실은 다음과 같습니다: 대부분의 RAG 구현체는 벡터 유사도 검색에만 의존하며, 여기에는 상당한 사각지대가 존재합니다.
벡터 검색이 실패하는 경우
순수 벡터 검색이 실망시키는 시나리오들을 고려해 봅시다:
| 시나리오 | 벡터 검색 동작 | 문제점 |
|---|---|---|
| 제품 코드("SKU-12345") | 의미론적 토큰으로 취급 | 정확한 일치 항목을 놓칠 수 있음 |
| ... |
전체 텍스트 검색이 실패하는 경우
전통적인 전체 텍스트 검색 역시 상호 보완적인 약점을 가지고 있습니다:
| 시나리오 | 전체 텍스트 검색 동작 | 문제점 |
|---|---|---|
| 동의어("car" 대 "automobile") | 유의어 사전 없이는 일치하지 않음 | 관련 문서를 놓침 |
| ... |
하이브리드 검색 솔루션
하이브리드 검색은 두 가지 접근 방식을 결합하여, Reciprocal Rank Fusion (RRF)과 같은 기술을 사용하여 결과를 지능적으로 병합합니다. 그 결과: 더 나은 재현율(recall), 더 나은 정밀도(precision), 그리고 다양한 쿼리 유형에 걸쳐 더욱 강력한 검색이 가능해집니다.
2. 검색의 두 기둥 이해하기 {#understanding-the-two-pillars}
구현에 깊이 들어가기 전에, 각 검색 방법이 실제로 무엇을 하는지에 대한 명확한 정신 모델을 확립해 봅시다.
벡터 유사도 검색
벡터 검색은 텍스트를 고차원 임베딩(embeddings)으로 변환합니다. 이는 의미론적 의미를 포착하는 수치적 표현입니다. 비슷한 의미는 비슷한 벡터를 생성하며, 이를 가능하게 합니다:
- 의미론적 매칭: "happy"가 "joyful"을 찾습니다.
- 개념적 검색: "how to fix a leak"가 "plumbing repair guide"를 찾습니다.
- 교차 언어 검색: 다국어 모델을 사용하면, "hello"가 "hola"를 찾습니다.
유사도는 일반적으로 **코사인 거리(cosine distance)**를 사용하여 측정하며, 값이 작을수록 더 큰 유사도를 나타냅니다.
cosine_distance = 1 - cosine_similarity
전체 텍스트 검색 (Full-Text Search)
PostgreSQL의 전체 텍스트 검색은 다음 기능을 사용합니다:
- 토큰화(Tokenization): 텍스트를 단어로 분리하는 과정
- 정규화(Normalization): 어간 추출("running" → "run"), 소문자 변환 등
- 불용어 제거(Stop word removal): 일반적인 단어("the", "a", "is")를 제거하는 것
- tsvector 생성: 위치 정보가 포함된 정규화된 토큰의 정렬된 목록을 만드는 과정
- tsquery 매칭: 검색 용어에 대한 불리언(Boolean) 연산
그 결과는 빠르고 정확한 키워드 매칭을 가능하게 하는 **어휘 기반 인덱스(lexeme-based index)**가 됩니다.
상보성 원칙 (The Complementarity Principle)
핵심 통찰은 다음과 같습니다: 벡터 검색과 전체 텍스트 검색은 서로 다른 방식으로 실패합니다. 이 둘을 결합하면, 한 방법의 실패는 다른 방법의 성공에 의해 보완될 수 있습니다.
┌─────────────────┐
│ 사용자 질의 (User Query) │
└────────┬────────┘
...
3. PostgreSQL 환경 설정 {#setting-up-postgresql}
요구 사항 (Requirements)
따라 하기 위해 다음이 필요합니다:
- PostgreSQL (버전 14 이상 권장)
- pgvector 확장(extension) (v0.5 이상, 본문에서는 v0.7.4 사용 예정)
- 다음 패키지가 설치된 Python 3.8+:
psycopg(PostgreSQL 어댑터)pgvector(Python 통합 라이브러리)faker(테스트 데이터 생성)sentence_transformers(임베딩 생성)
설치 (Installation)
# pgvector 설치 (플랫폼마다 다름)
# Homebrew를 사용하는 macOS의 경우:
brew install pgvector
...
데이터베이스 스키마 (Database Schema)
운영 환경에 적합하도록 주의 깊게 우리의 스키마를 생성해 보겠습니다:
-- 벡터 확장을 활성화합니다.
CREATE EXTENSION IF NOT EXISTS vector;
...
왜 384 차원인가요? multi-qa-MiniLM-L6-cos-v1 모델은 384차원의 임베딩을 생성합니다. 이는 다음 요소들 사이의 균형을 맞춘 의도적인 선택입니다:
- 표현력 (Expressiveness): 의미적 뉘앙스를 포착할 수 있는 충분한 차원
- 저장 효율성 (Storage efficiency): 768차원 또는 1536차원 대안보다 작음
- 쿼리 속도 (Query speed): 더 빠른 거리 계산
4. 데이터셋 구축: 임의 텍스트에서 임베딩으로 {#building-the-dataset}
데이터 생성 전략
이 데모를 위해 Faker를 사용하여 합성 데이터를 생성하고 sentence transformer로 인코딩할 것입니다. 데이터는 가짜이지만, _방법론(methodology)_은 실제 운영 환경에 적용 가능합니다.
from faker import Faker
import psycopg
from pgvector.psycopg import register_vector
...
임베딩 생성
# sentence transformer 모델 로드
# multi-qa-MiniLM-L6-cos-v1은 질문 답변 검색에 최적화되어 있습니다.
model = SentenceTransformer('multi-qa-MiniLM-L6-cos-v1')
...
PostgreSQL에 데이터 로드하기
# 데이터베이스 연결
# 실제 연결 세부 정보로 교체하세요
conn = psycopg.connect(
...
성능 팁 (Performance Tip): COPY 명령어는 대량 로딩 시 개별 INSERT 구문보다 차원이 다르게 빠릅니다. 50,000개 행의 경우, 이 접근 방식은 일반적으로 분 단위가 아닌 몇 초 만에 완료됩니다.
5. 인덱싱 전략: GIN 및 HNSW 설명 {#indexing-strategy}
인덱스 생성
-- GIN(Generalized Inverted Index)을 사용한 Full-text 검색 인덱스
CREATE INDEX products_description_gin_idx ON products
USING GIN (to_tsvector('english', description));
...
심층 분석: GIN 인덱스
to_tsvector('english', description)에 대한 GIN 인덱스는 신중한 설명이 필요합니다:
표현식 인덱스 (Expression Index): 우리는 원본 description 열을 인덱싱하는 것이 아니라, to_tsvector()의 _출력(output)_을 인덱싱하고 있습니다. 이는 다음을 의미합니다:
- 저장 효율성: 별도의
tsvector열이 필요하지 않습니다. - 자동 일관성 (Automatic consistency): 인덱스는 항상 텍스트와 동기화됩니다.
- 쿼리 최적화 (Query optimization): 동일한 표현식을 사용하는 쿼리는 이 인덱스를 사용할 수 있습니다.
'english'를 사용해야 하는 이유? PostgreSQL은 표현식 인덱스(expression indexes)에 **불변 함수(immutable functions)**를 요구합니다. 딕셔너리 인자(dictionary argument)와 함께 사용하는 to_tsvector()는 불변하기 때문에, 이를 명시적으로 지정해야 합니다. 이는 다음을 보장합니다:
- 인덱스 구축 시점과 쿼리 시점의 일관된 토큰화(Consistent tokenization)
- 재현 가능한 결과(Reproducible results)
- 세션 수준 구성 변경으로 인한 예기치 않은 문제 방지
심층 분석: HNSW 인덱스
HNSW (Hierarchical Navigable Small World)는 근사 최근접 이웃 검색(approximate nearest neighbor search)을 위한 그래프 기반 알고리즘입니다:
Layer 3 (가장 거친): A ─────────────────── B
│ │
Layer 2: C ───┼─── D ─── E ──────── F
...
주요 매개변수:
| 매개변수 | 기본값 | 우리의 설정 | 트레이드오프 |
|---|---|---|---|
m | 16 | 16 | 높을수록 재현율(recall)이 좋고, 메모리 사용량이 증가함 |
| ... | |||
ef_construction=256인 이유? 이는 인덱스 구축 과정에서 그래프 구조의 품질을 높입니다. 트레이드오프는 빌드 시간이 길어진다는 것이지만, 쿼리 성능과 재현율이 향상됩니다. |
6. 벡터 검색 구현 {#implementing-vector-search}
쿼리 임베딩 생성하기
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('multi-qa-MiniLM-L6-cos-v1')
...
벡터 검색 쿼리
SELECT
id,
description,
...
연산자 이해하기:
<=>: 코사인 거리 연산자 (0 = 동일, 2 = 반대)rank() OVER (ORDER BY ...): 거리를 기반으로 순위를 할당하는 윈도우 함수(Window function)$1: 임베딩 벡터를 위한 매개변수화된 쿼리 플레이스홀더
샘플 결과
id | description | rank
-------+-------------------------+-------
10578 | ... travel ... computer | 1
...
관찰: 레코드 18548은 정확한 구문
7. Full-Text 검색 구현 {#implementing-full-text-search}
Full-Text 검색 쿼리
SELECT
id,
description,
...
쿼리 분해하기
plainto_tsquery('english', 'travel computer'): 일반 텍스트를 tsquery로 변환합니다.- 결과:
'travel' & 'comput'(어간 추출(stemmed), AND 연결)
- 결과:
to_tsvector('english', description): 문서를 검색 가능한 형태로 변환합니다.- 결과: 위치 정보가 포함된 어간 추출 토큰의 정렬 배열
@@연산자: tsquery가 tsvector와 일치하는지 테스트합니다.ts_rank_cd(): 커버 밀도(Cover density) 순위 지정- 검색어들의 근접성을 고려합니다.
- 서로 가까이 나타나는 용어에 더 높은 점수를 부여합니다.
샘플 결과
id | description | rank
-------+-----------------------------+------
18548 | ... travel computer ... | 1 ← 정확!
...
관찰: Full-text 검색은 18548을 상위 결과로 올바르게 식별하지만, 동일한 순위를 가진 많은 결과를 반환합니다. 이는 전반적인 의미적 관련성(semantic relevance)을 구별하는 능력이 부족합니다.
8. Reciprocal Rank Fusion (RRF)의 마법 {#rrf-explained}
RRF란 무엇인가?
Reciprocal Rank Fusion은 여러 개의 순위 목록을 하나의 순위로 결합하는 **순위 집계 방법(rank aggregation method)**입니다. 이는 2009년 Cormack 등에 의해 소개되었으며, 정보 검색 분야의 표준 기술이 되었습니다.
공식
RRF_score(d) = Σ (1 / (k + rank_i(d)))
여기서:
d= 문서(document)k= 평활화 상수(smoothing constant) (일반적으로 50~60)rank_i(d)= 결과 목록 i에서 문서 d의 순위
RRF가 작동하는 이유
- 스케일 독립적 (Scale-independent): 원시 점수(raw scores)가 아닌 순위(rankings)를 결합합니다.
- Vector 거리는 ts_rank 점수와 스케일이 다릅니다.
- 순위는 보편적으로 비교 가능합니다.
- 강건함 (Robust): 한 목록의 이상치(outliers)가 전체 결과를 지배하지 않습니다.
- 단 하나의 #1 순위만 기여하는 점수는 약 0.02입니다.
- 여러 개의 높은 순위들이 복합적으로 작용합니다.
- 간단함 (Simple): 학습이 필요 없습니다.
- 학습 기반 순위 결정(learning-to-rank) 접근 방식과 달리,
- 결정론적이며 설명 가능합니다.
PostgreSQL 구현
CREATE OR REPLACE FUNCTION rrf_score(rank int, rrf_k int DEFAULT 50)
RETURNS numeric
LANGUAGE SQL
...
COALESCE를 사용하는 이유? 이는 NULL 순위를 우아하게 처리합니다. 문서가 하나의 결과 목록에만 나타나는 경우, '누락된' 순위는 합계에 0을 기여하는 것으로 간주됩니다.
IMMUTABLE PARALLEL SAFE를 사용하는 이유?
IMMUTABLE: 동일한 입력은 항상 동일한 출력을 생성합니다 (인덱스 표현식에 필요).PARALLEL SAFE: 병렬 워커에서 실행될 수 있습니다.
점수 분포 분석
k=50의 경우:
| 순위 | 점수 | 기여도 |
|---|---|---|
| 1 | 1/51 | 0.0196 |
| ... | ||
| 핵심 통찰: 순위 1과 순위 40의 차이는 약 2배에 불과합니다. 이는 두 목록 모두에 나타나는 것이 단지 한 목록에서 #1을 차지하는 것보다 더 가치 있다는 것을 의미합니다. |
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기