Summary
Quartz 기반 기술 블로그를 PostgreSQL 18과 pgvector를 활용하여 벡터화하고 지능형 검색 시스템을 구축하는 프로젝트다. 이전 Qdrant 기반 구현을 개선하여 Dense(벡터) + Sparse(BM25) Hybrid Search를 SQL 네이티브로 구현하고, Parent-Child Document 구조로 문서 계층을 유지하며, 헤딩 기반 의미적 청킹으로 검색 품질을 극대화한다. PostgreSQL 단일 데이터베이스로 벡터, 메타데이터, 관계형 데이터를 통합 관리하여 복잡한 쿼리와 트랜잭션을 지원한다.
프로젝트 배경
벡터 스토어 선택 과정
개인적으로 경험해보고 싶었던 로컬에서 구축 가능한 벡터 스토어는 크게 세 가지가 있다:
- Qdrant: 고성능 벡터 검색에 특화, 현재 진행 중인 다른 두 프로젝트에서 이미 사용 중
- Milvus: 대규모 엔터프라이즈급 벡터 DB, 수백만~수십억 개의 벡터를 다루는 대규모 시스템에 적합
- pgvector: PostgreSQL 익스텐션, 관계형 DB의 장점과 벡터 검색을 결합
이번 블로그 프로젝트에서는 pgvector를 선택했다. Qdrant는 다른 프로젝트에서 이미 사용 중이어서 새로운 기술 스택을 경험하고 싶었고, Milvus는 개인 블로그 규모(수백 개 문서)에는 과도한 스펙이다. 마침 PostgreSQL을 이번 AppHub 프로젝트의 메인 데이터베이스로 사용하기로 했기 때문에, 벡터 검색까지 통합할 수 있는 pgvector가 가장 적합했다.
PostgreSQL 18 + pgvector의 장점
통합 데이터 관리:
- 벡터, 메타데이터, 관계형 데이터를 단일 DB에서 관리
- 별도 벡터 DB 불필요, 운영 복잡도 감소
- ACID 트랜잭션으로 데이터 일관성 보장
진정한 Hybrid Search:
- Dense Search (벡터 코사인 유사도)와 Sparse Search (PostgreSQL FTS)를 SQL 네이티브로 결합
- 단일 쿼리로 의미적 검색과 키워드 검색 동시 수행
- 복잡한 JOIN, 필터링, 집계를 벡터 검색과 함께 활용
PostgreSQL 18의 성능 개선:
- 벡터 연산 성능 향상 (SIMD 최적화)
- 병렬 쿼리 성능 개선
- JSONB 인덱싱 및 쿼리 속도 향상
pgvector의 HNSW 인덱스:
- Approximate Nearest Neighbor (ANN) 검색으로 O(log n) 복잡도 달성
- 수십만 개 벡터도 밀리초 단위로 검색 가능
시스템 아키텍처
전체 구조도
┌─────────────────────────────────────────────────────────┐
│ Quartz 블로그 (Markdown) │
│ content/AI/, content/Study/, content/Tools/ │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. Markdown 파싱 & 전처리 │
│ - Frontmatter 추출 (title, tags, date, description) │
│ - 헤딩 기반 청킹 (LangChain MarkdownHeaderTextSplitter) │
│ - TOC 생성 (계층 구조 파악) │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. 임베딩 생성 (OpenAI API) │
│ - text-embedding-3-large (3072차원) │
│ - Parent: 전체 문서 요약 벡터 │
│ - Child: 각 청크별 벡터 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. PostgreSQL 18 + pgvector 적재 │
│ - parent_documents: 문서 전체 메타데이터 + 요약 벡터 │
│ - child_documents: 청킹된 섹션 + 콘텐츠 벡터 │
│ - HNSW 인덱스: 빠른 ANN 검색 │
│ - FTS 인덱스: BM25 텍스트 검색 │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. Hybrid Search 쿼리 실행 │
│ - Dense Search: 벡터 코사인 유사도 │
│ - Sparse Search: PostgreSQL FTS (BM25) │
│ - Weighted Fusion: 0.7 * Dense + 0.3 * Sparse │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. LLM Reranker & 결과 반환 │
│ - 관련성 평가 (1-10점) │
│ - 불필요한 결과 제거 (5점 미만 필터링) │
│ - Parent 정보 결합 (전체 문서 맥락 제공) │
└─────────────────────────────────────────────────────────┘
Parent-Child Document 구조
Parent-Child 패턴의 핵심 가치
Parent Document: 문서 전체의 메타데이터와 요약을 담은 상위 문서로, 제목, 태그, 카테고리, AI 생성 요약, 목차 구조 등을 포함한다.
Child Document: 의미적으로 분할된 각 섹션으로, 실제 검색 대상이 되는 콘텐츠를 담고 있다. 각 Child는 Parent에 대한 참조를 유지하여 전체 문서 맥락을 제공한다.
장점:
- 문맥 보존: 청크된 섹션에서도 전체 문서 정보 접근 가능
- 계층적 탐색: 관련 섹션을 찾은 후 같은 문서의 다른 섹션 탐색
- 정확한 검색: 섹션 레벨에서 정밀한 매칭, 문서 레벨에서 종합적 이해
- 효율적 임베딩: 전체 문서를 한 번에 임베딩하지 않고 의미 단위로 분할
구조 예시:
Parent: content/AI/2025-09-14-한국자동차���학회-논문-특화-파서-시스템.md
├── title: "한국자동차공학회 논문 특화 파서 시스템 분석"
├── tags: [AI, PDF-Parser, RAG, ...]
├── summary: "한국자동차공학회 논문을 위한 전문 문서 파싱..."
├── toc: {level: 1, title: "프로젝트 개요", children: [...]}
└── children:
├── Child 1: "프로젝트 개요" 섹션 (H1)
├── Child 2: "시스템 구성 요소" 섹션 (H1)
│ ├── Child 2-1: "PDF 파서 클라이언트" 서브섹션 (H2)
│ └── Child 2-2: "DOI 추출 및 크롤링 도구" 서브섹션 (H2)
├── Child 3: "기술 아키텍처" 섹션 (H1)
└── ...
데이터베이스 스키마 설계
1. Parent Documents 테이블
전체 문서의 메타데이터와 요약 정보를 저장하는 핵심 테이블.
컬럼 그룹 | 컬럼명 | 타입 | 설명 |
---|---|---|---|
기본 | id | UUID | Primary Key (자동 생성) |
file_path | TEXT | 파일 경로 (UNIQUE) | |
file_name | TEXT | 파일명 | |
메타데이터 | title | TEXT | 문서 제목 |
date | DATE | 작성일 | |
modified_date | DATE | 수정일 | |
tags | TEXT[] | 태그 배열 | |
category | TEXT | 카테고리 (AI, Study, Tools) | |
subcategory | TEXT | 서브카테고리 (Docker, SeSAC) | |
description | TEXT | 메타 설명 | |
AI 생성 | summary | TEXT | LLM 생성 요약 |
toc | JSONB | 목차 구조 (계층적) | |
통계 | word_count | INTEGER | 단어 수 |
reading_time | INTEGER | 예상 독서 시간 (분) | |
상태 | is_draft | BOOLEAN | 초안 여부 |
enable_toc | BOOLEAN | TOC 활성화 | |
벡터 | summary_embedding | vector(3072) | 문서 요약 벡터 |
full_text_search | tsvector | FTS 벡터 (자동 생성) | |
타임스탬프 | created_at | TIMESTAMP | 생성 시간 |
updated_at | TIMESTAMP | 수정 시간 |
인덱스 구성:
- 일반 인덱스: file_path, category, tags (GIN), date, full_text_search (GIN)
- 벡터 인덱스: HNSW (m=16, ef_construction=64)
m
: 그래프 연결성 (높을수록 정확하지만 메모리 증가)ef_construction
: 인덱스 구축 시 탐색 깊이 (높을수록 정확도 증가)
FTS 가중치:
- 제목 (A) > 설명 (B) > 요약 (C) 순으로 검색 우선순위 부여
2. Child Documents 테이블
헤딩 기반으로 청킹된 각 섹션의 실제 콘텐츠를 저장.
컬럼 그룹 | 컬럼명 | 타입 | 설명 |
---|---|---|---|
기본 | id | UUID | Primary Key |
parent_id | UUID | Parent 문서 참조 (CASCADE 삭제) | |
청킹 | chunk_index | INTEGER | 청크 순서 (0부터) |
heading_level | INTEGER | H1=1, H2=2, H3=3 | |
heading_text | TEXT | 헤딩 텍스트 | |
section_path | TEXT[] | 계층 경로 [“H1”, “H2”, “H3”] | |
콘텐츠 | content | TEXT | 실제 텍스트 내용 |
content_type | TEXT | text, code, table, list | |
char_count | INTEGER | 문자 수 | |
멀티모달 | has_images | BOOLEAN | 이미지 포함 여부 |
has_tables | BOOLEAN | 테이블 포함 여부 | |
has_code | BOOLEAN | 코드 포함 여부 | |
벡터 | content_embedding | vector(3072) | 콘텐츠 벡터 (실제 검색 대상) |
content_fts | tsvector | FTS 벡터 (자동 생성) | |
타임스탬프 | created_at | TIMESTAMP | 생성 시간 |
인덱스 구성:
- parent_id, chunk_index, heading_level, content_fts (GIN)
- HNSW 벡터 인덱스 (m=16, ef_construction=64)
3. Keywords 테이블 (선택적)
문서 간 연관성 분석 및 키워드 기반 추천.
컬럼명 | 타입 | 설명 |
---|---|---|
id | SERIAL | Primary Key |
keyword | TEXT | 키워드 (UNIQUE) |
frequency | INTEGER | 전체 등장 빈도 |
keyword_embedding | vector(3072) | 키워드 벡터 |
created_at | TIMESTAMP | 생성 시간 |
Document-Keyword 연결 테이블:
- document_id (FK) + keyword_id (FK) + tf_idf_score
활용:
- “관련 키워드 문서” 추천
- 키워드 트렌드 분석
- 의미적 연관 키워드 탐색
4. Search Logs 테이블
검색 쿼리와 사용자 피드백 저장하여 시스템 개선.
컬럼명 | 타입 | 설명 |
---|---|---|
id | SERIAL | Primary Key |
query | TEXT | 검색 쿼리 |
query_embedding | vector(3072) | 쿼리 벡터 |
search_type | TEXT | hybrid, dense, sparse |
results_count | INTEGER | 결과 개수 |
clicked_document_id | UUID | 클릭한 문서 (FK) |
user_feedback | TEXT | positive, negative, neutral |
search_time_ms | INTEGER | 검색 소요 시간 (ms) |
created_at | TIMESTAMP | 검색 시간 |
활용:
- 인기 검색어 분석
- 검색 결과 CTR 분석
- Reranker 학습 데이터
- A/B 테스트 (Dense vs Sparse vs Hybrid)
Hybrid Search 구현 상세
Dense Search (벡터 검색)
특징:
- 의미적 유사도: 단어 정확 일치 불필요, 의미가 비슷하면 검색됨
- 다국어 지원: 임베딩 모델의 다국어 지원 활용
- HNSW 인덱스: O(log n) 복잡도로 빠른 ANN 검색
핵심 연산:
-- 코사인 거리 연산자 (<=>)
1 - (c.content_embedding <=> $1::vector) AS cosine_similarity
한계: 특정 키워드나 전문 용어 검색 성능 저하 가능
Sparse Search (BM25 Full Text Search)
특징:
- 정확한 키워드 매칭: 특정 단어가 포함된 문서 검색
- 빠른 성능: GIN 인덱스 활용
- 스테밍 지원: “running”과 “run” 동일하게 취급
핵심 연산:
-- FTS 매칭 연산자 (@@)
WHERE c.content_fts @@ plainto_tsquery('simple', $1)
ORDER BY ts_rank_cd(c.content_fts, query) DESC
한계: 의미적 유사성 이해 불가, 동의어 검색 어려움
Hybrid Search (RRF 결합)
Reciprocal Rank Fusion (RRF) 공식:
RRF_score(doc) = Dense_rank + Sparse_rank
where rank = 1 / (k + position)
구현 요약:
단계 | 설명 | 핵심 쿼리 |
---|---|---|
1. Dense 검색 | 벡터 유사도 Top 20 | ORDER BY embedding <=> $1 |
2. Sparse 검색 | FTS 매칭 Top 20 | WHERE content_fts @@ query |
3. RRF 계산 | 순위 기반 점수 합산 | 1/(60+ROW_NUMBER()) |
4. 결과 결합 | FULL OUTER JOIN | COALESCE(d.id, s.id) |
5. 정렬 반환 | RRF 점수 내림차순 | ORDER BY rrf_score DESC |
가중치 튜닝 전략:
시나리오 | Dense 비중 | Sparse 비중 | 적용 케이스 |
---|---|---|---|
개념적 질문 | 높음 (0.8) | 낮음 (0.2) | “RAG란 무엇인가?” |
특정 키워드 | 중간 (0.5) | 중간 (0.5) | “pgvector 설치 방법” |
전문 용어 | 낮음 (0.4) | 높음 (0.6) | “HNSW 알고리즘” |
일반 검색 | 기본 (0.7) | 기본 (0.3) | 균형잡힌 설정 |
RRF vs Weighted Fusion
RRF는 점수 범위가 다른 검색 방식을 정규화 없이 결합 가능. Supabase 권장 방식으로 상수 60은 경험적 최적값 (조정 가능).
청킹 전략: 헤딩 기반 의미적 분할
LangChain MarkdownHeaderTextSplitter
분할 규칙:
헤딩 레벨 | 마크다운 | 역할 | 예시 |
---|---|---|---|
H1 | # | 주요 섹션 | ”## 프로젝트 개요” |
H2 | ## | 서브섹션 | ”### PDF 파서 클라이언트” |
H3 | ### | 세부 항목 | ”#### 핵심 기능” |
청킹 결과 예시:
Chunk 0:
계층: H1 ["프로젝트 개요"]
내용: 학술 연구를 위한 전문적인 문서 처리...
Chunk 1:
계층: H1 > H2 ["시스템 구성 요소", "PDF 파서 클라이언트"]
내용: 학술 논문 PDF를 구조화된 마크다운으로...
Chunk 2:
계층: H1 > H2 ["시스템 구성 요소", "DOI 추출 및 크롤링"]
내용: 웹 크롤링을 통한 보완 데이터 수집...
청킹 최적화 전략
고려 사항:
요소 | 제약 | 영향 |
---|---|---|
LLM 컨텍스트 | 너무 크면 노이즈, 너무 작으면 맥락 손실 | 검색 품질 |
임베딩 한계 | OpenAI 8191 토큰 제한 | 처리 가능 크기 |
검색 정확도 | 작은 청크는 정밀, 큰 청크는 포괄적 | 트레이드오프 |
권장 설정:
- ✅ 헤딩 기반 분할 우선: 자연스러운 의미 단위 유지
- ⚠️ 최대 길이 제한: 1500자 초과 시 문장 단위 추가 분할
- ⚠️ 최소 길이 보장: 100자 미만 청크는 병합
헤딩 구조 정규화:
- H1 없는 문서: 첫 H2를 H1으로 승격
- H2 없이 H3만 있는 경우: H3를 H2로 승격
- 일관된 계층 구조 유지
Multi-Query + Reranker 구현
Multi-Query 생성 프로세스
목적: 단일 질문을 5개 다양한 관점의 쿼리로 확장하여 검색 범위 극대화
생성 전략:
전략 | 설명 | 예시 |
---|---|---|
동의어 활용 | 같은 의미, 다른 표현 | ”벡터 검색” → “의미적 검색” |
상위 개념 | 더 일반적인 표현 | ”pgvector” → “벡터 데이터베이스” |
하위 개념 | 더 구체적인 표현 | ”RAG” → “Retrieval Augmented Generation” |
관련 개념 | 연관된 주제 | ”임베딩” → “HNSW 인덱스” |
실용적 표현 | 구현 중심 표현 | ”이론” → “구현 방법” |
예시 변환:
원본 쿼리: "pgvector를 사용한 벡터 검색 구현 방법"
생성된 5개 쿼리:
1. PostgreSQL pgvector 익스텐션 설치 및 설정
2. 벡터 데이터베이스 구축 실전 가이드
3. 의미적 검색을 위한 임베딩 저장소 구현
4. HNSW 인덱스를 활용한 빠른 ANN 검색
5. RAG 시스템의 벡터 스토어 아키텍처
병렬 검색 실행
프로세스:
- 임베딩 배치 생성: 5개 쿼리를 한 번에 OpenAI API 호출
- 병렬 검색:
asyncio.gather()
로 5개 Hybrid Search 동시 실행 - 결과 통합: 중복 제거 (문서 ID 기준)
시간 효율성:
- ❌ 순차 실행: 5 × 200ms = 1000ms
- ✅ 병렬 실행: max(5) ≈ 250ms
- 🚀 75% 시간 단축
LLM Reranker: 노이즈 필터링
Reranker의 핵심 목적
Hybrid Search는 많은 후보를 반환하지만 관련성 낮은 결과 다수 포함. LLM Reranker로 불필요한 결과를 걸러내어 품질 > 양 전략 수행.
평가 기준:
점수 | 판정 | 설명 | 처리 |
---|---|---|---|
9-10 | 최우수 | 질문에 직접 답하는 핵심 정보 | ✅ 채택 |
7-8 | 우수 | 관련성 높음, 답변에 도움 | ✅ 채택 |
5-6 | 보통 | 어느 정도 관련, 부차적 정보 | ⚠️ 임계값 |
3-4 | 낮음 | 키워드만 겹침, 실제 관련성 낮음 | ❌ 제거 |
1-2 | 무관 | 질문과 무관 | ❌ 제거 |
필터링 효과:
- 입력: Hybrid Search 50개 결과
- 제거: 5점 미만 약 30개 (60%)
- 출력: 고품질 20개 (40%)
- 결과: LLM 컨텍스트 40% 감소 → 속도/정확도 동시 향상
구현 과정
1. 환경 설정 (PostgreSQL 18 + pgvector)
# PostgreSQL 18 설치 및 pgvector 익스텐션 설치
brew install postgresql@18
brew services start postgresql@18
# pgvector 설치 (최신 버전)
git clone https://github.com/pgvector/pgvector.git
cd pgvector && make && make install
# DB 생성 및 익스텐션 활성화
psql postgres -c "CREATE DATABASE quartz_blog;"
psql quartz_blog -c "CREATE EXTENSION vector;"
PostgreSQL 18의 pgvector 최적화
PostgreSQL 18에서는 벡터 연산이 SIMD 최적화되어 이전 버전보다 약 2-3배 빠른 성능을 보입니다. pgvector 0.7.0 이상 버전을 사용하면 HNSW 인덱스 성능도 대폭 향상됩니다.
2. 데이터 파이프라인 (GitHub Actions 자동화)
Git push 시 변경된 마크다운 파일만 자동으로 재임베딩:
# .github/workflows/update-vectors.yml
name: Update Vector Database
on:
push:
paths:
- 'content/**/*.md'
jobs:
update-vectors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # 이전 커밋과 비교
- name: Get changed markdown files
id: changed-files
run: |
echo "files=$(git diff --name-only HEAD^ HEAD | grep '\.md$' | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install asyncpg openai langchain python-frontmatter
- name: Update embeddings for changed files
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
run: |
python scripts/update_embeddings.py --files "${{ steps.changed-files.outputs.files }}"
처리 과정:
- 변경 감지:
git diff
로 수정/추가된.md
파일만 추출 - Frontmatter 파싱: title, tags, date, description 등 메타데이터 추출
- 헤딩 기반 청킹: MarkdownHeaderTextSplitter로 H1/H2/H3 구조 유지하며 분할
- 임베딩 생성: OpenAI API 배치 처리로 비용 최소화
- DB 업데이트:
- 기존 문서:
ON CONFLICT
절로 UPSERT 처리 - 삭제된 문서:
DELETE CASCADE
로 child_documents도 자동 삭제
- 기존 문서:
3. Hybrid Search 구현 (RRF 방식)
**Reciprocal Rank Fusion (RRF)**을 활용한 Dense + Sparse 결합:
-- Supabase 스타일의 Hybrid Search (PostgreSQL 18 최적화)
WITH dense_search AS (
SELECT
c.id,
c.parent_id,
c.content,
c.heading_text,
1 / (60 + ROW_NUMBER() OVER (ORDER BY c.content_embedding <=> $1)) AS dense_rank
FROM child_documents c
JOIN parent_documents p ON c.parent_id = p.id
WHERE p.is_draft = false
ORDER BY c.content_embedding <=> $1::vector
LIMIT 20
),
sparse_search AS (
SELECT
c.id,
c.parent_id,
c.content,
c.heading_text,
1 / (60 + ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.content_fts, query) DESC)) AS sparse_rank
FROM child_documents c
JOIN parent_documents p ON c.parent_id = p.id,
plainto_tsquery('simple', $2) query
WHERE c.content_fts @@ query
AND p.is_draft = false
ORDER BY ts_rank_cd(c.content_fts, query) DESC
LIMIT 20
)
SELECT
COALESCE(d.id, s.id) AS id,
COALESCE(d.parent_id, s.parent_id) AS parent_id,
COALESCE(d.content, s.content) AS content,
COALESCE(d.heading_text, s.heading_text) AS heading_text,
COALESCE(d.dense_rank, 0.0) + COALESCE(s.sparse_rank, 0.0) AS rrf_score
FROM dense_search d
FULL OUTER JOIN sparse_search s ON d.id = s.id
ORDER BY rrf_score DESC
LIMIT 10;
RRF vs Weighted Fusion
Supabase 공식 문서에서 권장하는 RRF 방식은 점수 범위가 다른 Dense/Sparse 검색을 정규화 없이 결합할 수 있어 더 안정적입니다. 상수 60은 경험적으로 최적화된 값으로, 조정 가능합니다.
4. Multi-Query + Reranker
- Multi-Query 생성: LLM으로 원본 쿼리를 5개 다양한 관점으로 확장
- 병렬 검색: 5개 쿼리를 asyncio로 동시 실행
- 중복 제거: 동일 문서 ID 필터링
- LLM Reranking: GPT-4o-mini로 관련성 평가 (5점 미만 제거)
5. API 및 프론트엔드
FastAPI 검색 API 구축 후 Quartz 블로그에 검색 UI 컴포넌트 통합
고민 사항 및 해결 방안
1. 임베딩 모델 선택
모델 | 차원 | 장점 | 단점 | 권장 사용 |
---|---|---|---|---|
text-embedding-3-large | 3072 | 성능 우수, 다국어 강함 | 비용 높음 | 초기 구축 |
text-embedding-3-small | 1536 | 비용 효율적, 빠른 속도 | 정확도 약간 낮음 | 대규모 운영 |
오픈소스 (bge-m3 등) | 가변 | 무료, 커스터마이징 가능 | 자체 호스팅 필요 | 비용 민감 |
전략: large
로 시작 → 비용 최적화 시 small
로 전환 → 필요시 오픈소스 실험
2. 청킹 전략 세부 조정
문제: 일부 문서는 헤딩 구조가 불규칙 (H1 없음, H2만 다수 등)
해결 방안:
- ✅ H1 없는 문서: 첫 H2를 H1으로 자동 승격
- ✅ H3만 있는 경우: H3를 H2로 승격
- ✅ 일관된 계층 구조 강제 유지
3. 다국어 지원
현재: 영어 FTS (to_tsvector('english', ...)
)
개선 옵션:
방법 | 설명 | 장점 | 단점 |
---|---|---|---|
simple 사전 | to_tsvector('simple', ...) | 한국어 호환, 빠름 | 스테밍 없음 |
한국어 형태소 분석기 | mecab, komoran 통합 | 정확한 토큰화 | 설정 복잡 |
하이브리드 | 언어별 분기 처리 | 최적 성능 | 구현 복잡도 증가 |
권장: simple
사전으로 시작 → 필요 시 형태소 분석기 추가
4. 실시간 업데이트
구현 완료: GitHub Actions로 자동 업데이트
- ✅ 트리거:
content/**/*.md
파일 변경 시 - ✅ 변경 감지:
git diff
로 수정/추가/삭제 파일만 처리 - ✅ 증분 업데이트: 전체 재처리 없이 변경분만 임베딩
- ✅ 비용 최적화: 변경된 파일만 OpenAI API 호출
5. 비용 관리
OpenAI API 비용 예상:
항목 | 단가 | 예상 사용량 | 비용 |
---|---|---|---|
임베딩 생성 | $0.00013/1K tokens | 200개 글 × 2K tokens | $0.052 |
검색 쿼리 | $0.000026/query | 1000 queries/월 | $0.026 |
월 합계 | $0.078 |
절감 방안:
- 🔄 쿼리 임베딩 캐싱: Redis로 동일 쿼리 재사용
- 📦 배치 처리: API 호출 최소화
- 💾 인기 쿼리 사전 계산: 자주 검색되는 쿼리 미리 임베딩
확장 가능성
1. 멀티모달 검색
이미지와 코드 블록도 벡터화하여 검색 대상 확장.
추가 테이블 구조:
테이블 | 주요 컬럼 | 용도 |
---|---|---|
document_images | image_path, description, image_embedding | 이미지 검색 |
code_snippets | language, code, code_embedding | 코드 검색 |
활용 시나리오:
- 이미지 내용 기반 검색 (“차트가 포함된 문서”)
- 특정 언어 코드 검색 (“Python asyncio 예제”)
- 멀티모달 Cross-search (텍스트 → 이미지 결과 포함)
2. 지식 그래프 연동
문서 간 연결 관계를 그래프로 표현하여 탐색 강화.
구현 방안:
요소 | 설명 | 예시 |
---|---|---|
document_links | 문서 간 링크 관계 | internal_link, related, cited_by |
그래프 탐색 | ”이 문서와 관련된 문서들” | 재귀 쿼리 (WITH RECURSIVE) |
시각화 | Neo4j, Graphviz 연동 | 지식 맵 생성 |
활용:
- 관련 문서 추천 (같은 토픽)
- 인용 관계 분석
- 지식 네트워크 시각화
3. 개인화된 검색
사용자별 검색 히스토리 기반 결과 조정.
구현 요소:
기능 | 데이터 | 활용 |
---|---|---|
선호 태그 | preferred_tags[] | 태그 기반 boosting |
클릭 이력 | clicked_documents[] | 관심 문서 우선 순위 |
검색 히스토리 | search_history JSONB | 패턴 분석 |
개인화 전략:
- 자주 보는 태그의 문서 상위 노출
- 이전 클릭 문서와 유사한 결과 우선
- 검색 패턴 학습으로 추천 개선
결론
PostgreSQL 18 + pgvector를 활용한 벡터 검색 시스템은 이전 Qdrant 기반 구현의 한계를 극복하고 다음과 같은 장점을 제공한다:
- 통합 데이터 관리: 벡터, 메타데이터, 관계형 데이터를 단일 DB에서 관리
- 진정한 Hybrid Search: SQL 네이티브로 Dense + Sparse 검색 결합
- 강력한 쿼리 능력: JOIN, 집계, 복잡한 필터링 등 SQL의 모든 기능 활용
- 트랜잭션 지원: ACID 보장으로 데이터 일관성 향상
- 비용 효율성: 별도 벡터 DB 불필요, 운영 복잡도 감소
- 확장성: 멀티모달, 지식 그래프, 개인화 등 다양한 확장 가능
다음 단계는 실제 구현을 진행하면서 성능 측정 및 최적화를 통해 사용자 경험을 극대화하는 것이다.
참고 자료
공식 문서 및 가이드
-
pgvector GitHub: PostgreSQL 벡터 익스텐션 공식 저장소
- HNSW 인덱스 설정 및 최적화 방법
- PostgreSQL 18 최적화 팁
- 다양한 거리 함수 (코사인, L2, 내적) 비교
-
Supabase Hybrid Search Guide: Supabase의 RRF 기반 Hybrid Search 구현 가이드
- Reciprocal Rank Fusion (RRF) 알고리즘 설명
- PostgreSQL FTS와 pgvector 통합 방법
- 실전 SQL 쿼리 예제 및 성능 최적화
-
Growth Coder - pgvector 활용 가이드: 한국어 pgvector 실전 가이드
- Parent-Child Document 패턴 구현
- 임베딩 모델 선택 및 비용 최적화
- 한국어 FTS 설정 방법
관련 프로젝트 및 기술
- 한국자동차공학회 논문 특화 파서 시스템 분석: 이전 Qdrant 기반 구현 (비교 참고용)
- RAG+Groq: 빠른 추론을 위한 Groq 활용 방법
- LangChain: 문서 처리 파이프라인 구축 도구
- 문서(pdf 등) 내 시각 자료와 텍스트의 추출 및 활용: 멀티모달 문서 처리 기법
- Knowledge Graphs for RAG: 지식 그래프 연동 방법
핵심 기술 키워드
- pgvector: PostgreSQL 벡터 익스텐션
- HNSW (Hierarchical Navigable Small World): 효율적인 ANN 검색 인덱스
- RRF (Reciprocal Rank Fusion): Hybrid Search 결과 결합 알고리즘
- PostgreSQL FTS (Full Text Search): 내장 전문 검색 기능
- Parent-Child Document: RAG 청킹 전략