LLM WikiAccess-protected knowledge portal
← 스터디 홈
2편 · 약 28분

pgvector 심화: HNSW 튜닝, 필터링, 대규모 운영

PostgreSQL 안에 벡터 검색을 넣는다는 것

pgvector는 PostgreSQL 확장(extension)이다. CREATE EXTENSION vector 한 줄로 활성화하면 기존 테이블에 vector 컬럼을 추가하고, HNSW 또는 IVFFlat 인덱스를 붙여 ANN 검색을 수행할 수 있다. 별도의 벡터 DB를 추가하지 않아도 SQL JOIN, 트랜잭션, 접근 제어, 백업이 그대로 동작한다.

이 단순함이 매력이지만, 운영 규모가 커지면 함정이 나타난다. HNSW 인덱스는 메모리를 많이 요구하고, 필터링 쿼리는 의도치 않게 recall이 낮아지며, 수천만 벡터 이상에서는 인덱스 빌드 자체가 수 시간이 걸린다. 이 챕터는 이런 문제들을 파라미터 수준에서 다룬다.


HNSW 파라미터 심화

pgvector의 HNSW 인덱스는 세 가지 파라미터로 동작을 제어한다.

CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

파라미터 역할과 트레이드오프

파라미터적용 시점기본값효과
m인덱스 빌드16노드당 최대 연결 수. 높을수록 recall↑, 인덱스 크기↑, 빌드 시간↑
ef_construction인덱스 빌드64빌드 시 후보 탐색 크기. 높을수록 그래프 품질↑, 빌드 시간↑
hnsw.ef_search쿼리 실행40탐색 시 후보 리스트 크기. 높을수록 recall↑, 쿼리 지연↑

ef_search는 세션 수준에서 바꿀 수 있다.

SET hnsw.ef_search = 100;
SELECT * FROM items ORDER BY embedding <=> '[...]' LIMIT 10;

튜닝 기준선

워크로드mef_constructionef_search
빠른 검색, recall 최소 요건166440
일반 프로덕션1612880
고품질 검색 우선32200200
메모리 절약86440

mef_construction은 인덱스를 재생성하지 않으면 바꿀 수 없다. 프로덕션 전에 representative dataset으로 recall을 측정한 뒤 고정하는 것이 핵심이다.


HNSW vs IVFFlat: 언제 무엇을 선택하는가

pgvector는 두 가지 인덱스 타입을 지원한다.

HNSW 구조: 계층형 그래프 인덱스 빌드 전 데이터 불필요: ✓ (빌드 즉시 사용) Recall: 97~99% (ef_search 100 기준) 쿼리 지연: 낮음 (그래프 탐색) 메모리: 큼 (그래프 엣지 저장) 적합: 대부분의 프로덕션, 업데이트가 잦은 데이터
IVFFlat 구조: 클러스터 기반 역 파일 인덱스 빌드 전 데이터 필요: ✓ (군집화 필요) Recall: 90~96% (nprobe=10 기준) 쿼리 지연: 중간 메모리: 적음 (벡터만 저장) 적합: 메모리 제약, 정적 대규모 데이터셋
HNSW vs IVFFlat 비교

IVFFlat은 CREATE INDEX 전에 데이터가 충분히 들어 있어야 군집화가 의미 있다. 권장: lists = 데이터가 1M 이하일 때 rows/1000, 1M 초과일 때 sqrt(rows).

-- 인덱스 생성 후 쿼리 시 탐색할 클러스터 수
SET ivfflat.probes = 20;

대부분의 신규 서비스는 HNSW를 기본으로 선택하고, IVFFlat은 메모리가 명확히 제약될 때만 고려한다.


필터링과 Iterative Scan

벡터 검색에서 필터링은 까다로운 문제다. "이 사용자의 문서 중에서 가장 유사한 것을 찾아라"처럼 메타데이터 조건이 결합되면 recall이 의도치 않게 낮아진다.

문제: 필터링이 recall을 무너뜨리는 원리

ef_search = 40인 HNSW, 전체 벡터의 10%만 필터 조건 만족

→ 그래프 탐색 후보 40개 중 조건 만족: 평균 4개
→ TOP 10을 요청했는데 4개밖에 반환되지 않음
→ 실제 관련 벡터를 놓침 (silent recall 저하)

pgvector 0.8.0: Iterative Scan

pgvector 0.8.0은 이 문제를 해결하는 iterative scan을 도입했다. 필터 조건을 만족하는 결과가 요청 수(limit)에 미치지 못하면 인덱스 탐색 범위를 자동으로 확장한다.

-- 필터 조건이 있는 쿼리에 iterative scan 적용
SET hnsw.iterative_scan = strict_order;
-- 또는
SET hnsw.iterative_scan = relaxed_order;

SELECT id, content
FROM docs
WHERE user_id = 42
ORDER BY embedding <=> '[...]' LIMIT 10;
모드동작적합한 경우
strict_order거리 순서를 엄격히 유지하며 확장정확한 상위 k 순서가 필요할 때
relaxed_order순서를 다소 완화해 더 빠른 탐색순서보다 recall이 중요할 때

제어 파라미터:

-- 최대 방문 튜플 수 (기본: 20000)
SET hnsw.max_scan_tuples = 50000;
-- 메모리 사용량을 work_mem의 N배로 제한
SET hnsw.scan_mem_multiplier = 2;
pgvector 0.7 — 기본 탐색 HNSW 그래프 탐색 (ef_search=40) 후보 40개 추출 → 필터 적용 → 조건 만족 4개 LIMIT 10 요청 → 4개만 반환 ✗ 전체 10% = 100,000개가 조건 만족 (총 1M 중) 실제 관련 벡터가 충분히 존재하지만 탐색 범위 40개 안에 6개가 없어서 누락 → Silent recall 저하 (사용자는 모름) pgvector 0.8 — Iterative Scan 1차 탐색: 후보 40개 → 필터 4개 (LIMIT=10 미달) 자동 확장: 후보 80개 → 필터 9개 (아직 미달) 재확장: 후보 140개 → 필터 10개 달성 ✓ max_scan_tuples(20000)에 도달하거나 limit 수 달성 시 탐색 종료 → recall 보호됨 비용: 탐색 범위 증가 → 쿼리 지연 증가 운영 권장: 필터 조건이 전체의 10% 이하를 선택할 경우 iterative_scan = strict_order 활성화 max_scan_tuples는 지연 SLA에 맞게 조정. 너무 높으면 최악의 경우 full scan에 가까워짐. 필터 선택률이 1% 미만이면 full table scan (parallel) 쪽이 오히려 빠를 수 있음.
pgvector 필터링: 기본 vs Iterative Scan

양자화 전략: 메모리 절감

대규모 배포에서 메모리는 가장 큰 비용 요인이다. pgvector 0.7.0부터 여러 양자화 방법을 지원한다.

halfvec: 반정밀도 저장

-- 768차원 halfvec HNSW 인덱스
CREATE INDEX ON items USING hnsw ((embedding::halfvec(768)) halfvec_l2_ops);

-- 또는 컬럼을 halfvec으로 정의
ALTER TABLE items ADD COLUMN embedding_half halfvec(768);
  • float32 → float16 변환: 4바이트 → 2바이트 (50% 절감)
  • 최대 4,000차원 지원
  • 대부분의 임베딩 모델에서 recall 저하 미미 (< 1%p)

이진 양자화: 극단적 압축

-- 이진 양자화 후 hamming 거리로 ANN
CREATE INDEX ON items USING hnsw ((binary_quantize(embedding)::bit(1536)) bit_hamming_ops);

-- 쿼리: 이진 인덱스로 후보 추출 후 원본 벡터로 재순위 (re-rank)
SELECT id, embedding <=> query_vec AS score
FROM items
WHERE id = ANY(
  SELECT id FROM items
  ORDER BY binary_quantize(embedding) <~> binary_quantize(query_vec)
  LIMIT 100
)
ORDER BY score
LIMIT 10;
  • float32 → 1비트: 32배 이상 압축
  • 최대 64,000차원 지원
  • recall 손실이 크므로 반드시 원본 벡터로 재순위(re-ranking) 필요
  • OpenAI text-embedding-3-* 모델은 이진 양자화 후 재순위에서도 높은 정확도 유지

메모리 요구량 비교 (10M × 1536d 기준)

방식벡터 크기인덱스 예상 크기
float32 (기본)약 58GB80~120GB
halfvec (float16)약 29GB40~60GB
이진 양자화약 1.8GB2~5GB (단, 원본 벡터 별도 유지)

대규모 인덱스 빌드: 메모리와 병렬화

HNSW 인덱스는 빌드 시 그래프 전체를 메모리에 올려야 한다. maintenance_work_mem이 부족하면 디스크 기반 빌드로 폴백되어 10~50배 느려진다.

-- 인덱스 빌드 전 세션에서 설정
SET maintenance_work_mem = '8GB';

-- 병렬 빌드 (worker 수 + 리더 1개)
SET max_parallel_maintenance_workers = 7;
-- max_parallel_workers가 기본 8이므로 맞춰 올릴 것
ALTER SYSTEM SET max_parallel_workers = 16;
SELECT pg_reload_conf();

메모리 부족 징후: 인덱스 빌드 로그에 다음 메시지가 뜨면 디스크 폴백 중이다.

NOTICE:  hnsw graph size (4096 MB) exceeds maintenance_work_mem (1024 MB)
DETAIL:  Building on disk and reading from disk.

빌드 전략: 데이터를 먼저, 인덱스는 나중에

-- 1. 인덱스 없이 대량 삽입
COPY items (id, embedding) FROM '/data/vectors.csv' CSV;

-- 2. 삽입 완료 후 인덱스 생성
SET maintenance_work_mem = '16GB';
CREATE INDEX CONCURRENTLY ON items USING hnsw (embedding vector_cosine_ops);

CREATE INDEX CONCURRENTLY는 테이블 잠금 없이 빌드하지만, 빌드 시간이 1.5~2배 더 걸린다.

인덱스 크기 추정

HNSW 인덱스 크기 ≈ N × (D × 4) × 1.5 ~ 2.0
(N: 벡터 수, D: 차원, 1.5~2.0: 그래프 오버헤드 계수)

예: 10M × 768d = 10,000,000 × 768 × 4 × 1.7 ≈ 52GB
예: 10M × 1536d ≈ 104GB

EXPLAIN으로 벡터 쿼리 분석

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM items
ORDER BY embedding <=> '[...]' LIMIT 10;

출력에서 주목할 항목:

항목의미
Index Scan using hnsw_idxHNSW 인덱스 사용 중
Buffers: shared hit=N, read=Mhit: 캐시, read: 디스크 IO. read↑ = 인덱스 캐시 미스
Seq Scan인덱스 미사용. 필터 조건이 너무 선택적이거나 인덱스 누락
Parallel Seq Scan병렬 전체 스캔. 소규모에서는 합리적

인덱스를 생성했는데도 Seq Scan이 나타나면:

-- 인덱스 스캔 강제 (테스트 용도)
SET enable_seqscan = off;

운영 패턴과 체크리스트

인덱스 건강 모니터링

-- 인덱스 크기 확인
SELECT pg_size_pretty(pg_relation_size('hnsw_idx'));

-- 인덱스 사용 통계
SELECT idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE indexrelname = 'hnsw_idx';

빈번한 문제와 대응

문제증상대응
recall 저하검색 품질 불만, 평가 지표 하락ef_search↑, iterative_scan 활성화
인덱스 빌드 지연수 시간 이상 소요maintenance_work_mem↑, 병렬 worker↑
쿼리 지연 급등캐시 미스 후 P99 급등shared_buffers↑, 인덱스 크기 줄이기(halfvec)
필터 후 결과 부족LIMIT 미달 반환iterative_scan 활성화, max_scan_tuples 조정
대용량 테이블 VACUUM 차단autovacuum 중 인덱스 참조 오류VACUUM ANALYZE 주기 조정, dead tuple 비율 감시

pgvector 적합/부적합 판단 기준

pgvector 계속 유지 기준:
  ✓ 벡터 수 < 1,000만
  ✓ 기존 PostgreSQL 인프라와 JOIN이 핵심
  ✓ 필터 조건의 선택률 > 10%
  ✓ 인덱스가 available RAM(shared_buffers)에 들어감

전용 벡터 DB(Qdrant, Milvus) 전환 고려 기준:
  ✗ 벡터 수 > 5,000만
  ✗ 인덱스가 메모리를 초과해 지속적인 cache miss
  ✗ 필터 조건 선택률 < 1% + 초저지연 요구
  ✗ 수평 스케일아웃이 필수

References