Contents
see ListPostgreSQL 17의 AI 시대 포지셔닝
2024년 출시된 PostgreSQL 17은 AI/ML 워크로드를 위한 성능 개선을 대거 포함했다. 쓰기 처리량이 이전 버전 대비 두 배로 향상되어 벡터 인덱스 업데이트와 업서트(upsert) 작업이 대폭 빨라졌으며, VACUUM 오버헤드도 줄어들어 대규모 벡터 데이터셋 운영이 한층 수월해졌다. pgvector 0.8.0과 결합하면 PostgreSQL 하나로 관계형 데이터와 AI 임베딩을 함께 관리하는 올인원 솔루션이 완성된다.
PostgreSQL 17 핵심 변경사항
PostgreSQL 17의 주요 개선 사항:
- VACUUM 성능 대폭 향상 — 인덱스 삭제 프로세스 최적화로 대규모 테이블 유지보수 시간 단축
- 쿼리 플래너 개선 — 파티션 테이블 쿼리 최적화, JSON 처리 성능 향상
- 병렬 처리 강화 — DISTINCT와 windowed 집계 함수의 병렬 실행 지원 확대
- pg_basebackup 개선 — 증분 백업(incremental backup) 공식 지원
- 로지컬 리플리케이션 강화 — 실패 슬롯 자동 동기화
-- PostgreSQL 17에서 추가된 JSON 기능
SELECT
data -> 'name' AS name,
data -> 'score' AS score,
-- 새로운 JSON_TABLE 함수 (SQL/JSON 표준)
jt.*
FROM products,
JSON_TABLE(
data -> 'tags',
'$[*]' COLUMNS (
tag_name VARCHAR PATH '$'
)
) AS jt
WHERE (data ->> 'score')::float > 4.5;pgvector 설치 및 설정
pgvector는 PostgreSQL에 벡터 데이터 타입과 유사도 검색 기능을 추가하는 오픈소스 확장이다. HNSW(Hierarchical Navigable Small Worlds)와 IVFFlat 두 가지 근사 최근접 이웃(ANN) 인덱스를 지원하며, halfvec(2바이트 부동소수점, 최대 4,000 차원)와 sparsevec(희소 벡터, 최대 1,000 비제로 차원) 타입도 지원한다.
-- pgvector 확장 설치
CREATE EXTENSION IF NOT EXISTS vector;
-- 벡터 컬럼을 포함한 테이블 생성
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
content TEXT NOT NULL,
source VARCHAR(200),
created_at TIMESTAMPTZ DEFAULT NOW(),
embedding vector(1536), -- OpenAI text-embedding-3-small 차원
embedding_small halfvec(1536) -- 메모리 절약 버전
);
-- HNSW 인덱스 생성 (고성능 검색, 더 많은 메모리)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- IVFFlat 인덱스 (적은 메모리, 약간 느린 검색)
-- 먼저 데이터를 충분히 채운 후 생성 권장
CREATE INDEX ON documents
USING ivfflat (embedding vector_l2_ops)
WITH (lists = 100);임베딩 생성 및 저장
import psycopg2
from openai import OpenAI
import numpy as np
openai_client = OpenAI()
def get_embedding(text: str) -> list[float]:
"""텍스트를 임베딩 벡터로 변환"""
response = openai_client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def insert_document(conn, title: str, content: str, source: str):
"""문서를 임베딩과 함께 저장"""
embedding = get_embedding(f"{title}\n\n{content}")
with conn.cursor() as cur:
cur.execute("""
INSERT INTO documents (title, content, source, embedding)
VALUES (%s, %s, %s, %s)
RETURNING id
""", (title, content, source, embedding))
doc_id = cur.fetchone()[0]
conn.commit()
return doc_id
def semantic_search(conn, query: str, limit: int = 5) -> list[dict]:
"""시맨틱 유사도 검색"""
query_embedding = get_embedding(query)
with conn.cursor() as cur:
cur.execute("""
SELECT
id,
title,
content,
source,
1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_embedding, query_embedding, limit))
results = []
for row in cur.fetchall():
results.append({
'id': row[0],
'title': row[1],
'content': row[2][:200] + '...',
'source': row[3],
'similarity': float(row[4])
})
return results하이브리드 검색: 벡터 + 전문 검색 결합
실제 프로덕션 환경에서는 시맨틱 검색(벡터)과 키워드 검색(BM25/tsvector)을 결합한 하이브리드 검색이 더 높은 품질을 제공한다.
-- 전문 검색용 컬럼 추가
ALTER TABLE documents ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('korean', coalesce(title, '') || ' ' || coalesce(content, ''))
) STORED;
CREATE INDEX ON documents USING GIN (search_vector);
-- 하이브리드 검색 쿼리 (RRF: Reciprocal Rank Fusion)
WITH vector_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS rank
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 20
),
fulltext_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank(search_vector, query) DESC) AS rank
FROM documents, to_tsquery('korean', $2) query
WHERE search_vector @@ query
ORDER BY ts_rank(search_vector, query) DESC
LIMIT 20
)
SELECT
d.id, d.title, d.content,
COALESCE(1.0 / (60 + vs.rank), 0) +
COALESCE(1.0 / (60 + fs.rank), 0) AS rrf_score
FROM documents d
LEFT JOIN vector_search vs ON d.id = vs.id
LEFT JOIN fulltext_search fs ON d.id = fs.id
WHERE vs.id IS NOT NULL OR fs.id IS NOT NULL
ORDER BY rrf_score DESC
LIMIT 10;멀티모달 검색: 이미지 임베딩
-- 이미지 임베딩 테이블 (CLIP 모델 사용, 512차원)
CREATE TABLE product_images (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT REFERENCES products(id),
image_url TEXT NOT NULL,
image_embedding vector(512),
text_embedding vector(1536) -- 제품 설명 임베딩
);
CREATE INDEX ON product_images
USING hnsw (image_embedding vector_cosine_ops);
-- 이미지로 유사 제품 검색
SELECT
p.name,
p.price,
pi.image_url,
1 - (pi.image_embedding <=> $1::vector) AS similarity
FROM product_images pi
JOIN products p ON pi.product_id = p.id
ORDER BY pi.image_embedding <=> $1::vector
LIMIT 10;성능 튜닝 가이드
pgvector 성능을 최대화하기 위한 PostgreSQL 설정 권장값:
# postgresql.conf
# 메모리 설정 (HNSW 인덱스 빌드에 중요)
maintenance_work_mem = 4GB # 인덱스 생성 시 메모리
work_mem = 256MB # 정렬 및 해시 작업
# HNSW 검색 품질 vs 속도 조절
hnsw.ef_search = 100 # 높을수록 정확하지만 느림 (기본값 40)
# 병렬 인덱스 빌드
max_parallel_maintenance_workers = 7
# 효과적인 캐시 크기 설정
effective_cache_size = 16GB-- 세션별 검색 품질 조절
SET hnsw.ef_search = 200; -- 높은 정확도
-- 또는
SET hnsw.ef_search = 40; -- 빠른 속도
-- 인덱스 진행상황 모니터링
SELECT phase, blocks_done, blocks_total,
round(100.0 * blocks_done / NULLIF(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index
WHERE command = 'CREATE INDEX';운영 환경 체크리스트
pgvector를 프로덕션에 배포하기 전 확인사항:
- 임베딩 차원 수 확정 후 변경 불가. 모델 업그레이드 시 재인덱싱 필요
- 대규모 데이터(1M+)에서는 IVFFlat보다 HNSW가 검색 속도가 빠름
- 인덱스는 데이터를 충분히 채운 후 생성해야 품질이 좋음 (IVFFlat의 경우 lists * 50개 이상의 데이터 권장)
- pg_vector 통계 수집을 위해 autovacuum 설정 최적화 필요
- halfvec 타입으로 메모리를 절반으로 줄이면서도 검색 품질 유지 가능