pgvector 셋업과 HNSW 인덱스
상위: Database MOC
개요
pgvector는 PostgreSQL 안에서 벡터 유사도 검색을 수행하게 해 주는 확장(extension)이다. 별도의 벡터 DB를 두지 않고 관계형 데이터와 임베딩을 한 트랜잭션 경계 안에서 관리할 수 있다는 것이 최대 장점이지만, 그만큼 확장 설치·인덱스 설계·조회 파라미터를 직접 챙겨야 한다. 이 문서는 RAG 서빙 파이프라인을 PostgreSQL 위에 올릴 때 반복해서 부딪히는 다섯 가지 함정, 즉 (1) 확장 부트스트랩 누락으로 인한 조용한 무력화, (2) 인덱스 없는 seq scan, (3) 메타필터 post-filter가 결과를 비우는 필터 스타베이션, (4) HNSW 빌드 시 OOMKill, (5) 컬렉션 재생성으로 인한 인덱스 유실을 다룬다.
임베딩 차원은 예시로 vector(1024)를 쓰지만 실제로는 사용하는 임베딩 모델의 차원에 맞춘다. 테이블은 일반화하여 document_chunks(청크 본문 + 임베딩), documents(원본 메타데이터)로 표기한다.
원리
확장 부트스트랩과 조용한 무력화
vector 타입과 연산자(<->, <=>, <#>)는 확장이 설치돼야 존재한다. 확장이 없는 DB에 임베딩 컬럼을 쓰려 하면 애플리케이션에 따라 예외가 나기도 하지만, ORM/서비스 계층이 “임베딩 실패 시 skip” 로직을 갖고 있으면 임베딩 경로 전체가 조용히 무력화된다. 검색은 되지만 벡터 필드는 항상 비어 있고, 아무도 에러를 보지 못한 채 fallback 키워드 검색만 동작하는 식이다. 그래서 CREATE EXTENSION vector는 반드시 첫 마이그레이션으로 고정해 두어야 한다.
HNSW 인덱스의 동작
HNSW(Hierarchical Navigable Small World)는 다층 근접 그래프를 만들어 근사 최근접 이웃(ANN)을 탐색한다. 핵심 파라미터는 세 가지다.
| 파라미터 | 위치 | 의미 | 트레이드오프 |
|---|---|---|---|
m | 빌드 시 | 각 노드의 그래프 연결도 | 클수록 재현율·메모리↑, 빌드 느림 |
ef_construction | 빌드 시 | 빌드 중 후보 큐 크기 | 클수록 그래프 품질↑, 빌드 느림 |
hnsw.ef_search | 조회 시 | 탐색 중 후보 큐 크기 | 클수록 재현율↑, 지연↑ |
m=16, ef_construction=64가 일반적 출발점이고, 조회 재현율은 세션 변수 hnsw.ef_search(기본 40)로 사후 조절한다. 인덱스를 다시 빌드하지 않고도 재현율/지연을 조절할 수 있다는 점이 HNSW의 실무적 이점이다.
HNSW vs IVFFlat
| 특성 | HNSW | IVFFlat |
|---|---|---|
| 빌드 메모리 | 높음(maintenance_work_mem 크게 요구) | 상대적으로 낮음 |
| 빌드 속도 | 느림 | 빠름 |
| 조회 재현율/지연 | 우수 | 준수(lists/probes 의존) |
| 데이터 없이 빌드 | 가능 | 불가(대표벡터 학습 위해 데이터 필요) |
메모리가 넉넉하면 HNSW가 기본 선택이고, 빌드 메모리 압박이 크면 IVFFlat 또는 halfvec가 대안이다.
실전
확장과 인덱스 부트스트랩
-- 첫 마이그레이션: 없으면 임베딩 경로가 조용히 죽는다
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 키워드 fallback 병행 시
-- 수백만 행 vector(1024)에 PK만 있으면 유사도 검색은 seq scan + 정렬(수 초~분)
CREATE INDEX ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);vector_cosine_ops는 코사인 거리(<=>)용이다. 내적은 vector_ip_ops(<#>), L2는 vector_l2_ops(<->)를 쓴다. 인덱스의 연산자 클래스와 쿼리의 거리 연산자가 일치해야 인덱스를 탄다.
조회와 재현율 튜닝
SET hnsw.ef_search = 100; -- 세션 단위. 재현율↑ 지연↑
SELECT id, content
FROM document_chunks
ORDER BY embedding <=> $1 -- 반드시 인덱스와 같은 연산자
LIMIT 10;ef_search와 재현율의 대략적 감각:
ef_search | 상대 재현율 | 상대 지연 |
|---|---|---|
| 40 (기본) | 기준 | 기준 |
| 100 | 높음 | 소폭↑ |
| 200 | 매우 높음 | 눈에 띄게↑ |
빌드 메모리 확보
SET maintenance_work_mem = '2GB'; -- 세션 한정으로 크게
CREATE INDEX CONCURRENTLY ... USING hnsw (...);
RESET maintenance_work_mem;함정·트러블슈팅
1. 필터 스타베이션 (엄격 메타필터 + post-filter가 빈 결과)
HNSW는 ANN 후보를 먼저 뽑고 그 위에 WHERE tenant_id = ... 같은 메타필터를 얹는다(post-filter). 필터가 엄격하면 후보 대부분이 걸러져 결과가 비는 현상이 생긴다.
-- pgvector >= 0.8: 후보가 부족하면 인덱스를 반복 스캔
SET hnsw.iterative_scan = relaxed_order; -- 또는 strict_order
-- 또는 후보 풀 자체를 키운다
SET hnsw.ef_search = 200;relaxed_order는 정렬 정확성을 약간 희생하고 더 많은 후보를 확보하며, strict_order는 순서를 보장하되 비용이 더 든다.
2. 빌드 시 OOMKill (Exit 137)
HNSW 빌드는 maintenance_work_mem을 수 GB까지 원한다. 컨테이너/파드 메모리 제한이 이보다 낮으면 커널이 프로세스를 죽여 **OOMKill(Exit 137)**로 나타난다. 빌드가 “이유 없이” 끊긴다면 dmesg/파드 이벤트에서 137을 먼저 확인한다.
| 대응 | 방법 |
|---|---|
| 메모리 확보 | 빌드 전 가용 메모리 vs maintenance_work_mem 대조, 컨테이너 limit 상향 |
| 정밀도 낮추기 | halfvec(1024)(반정밀도)로 저장·인덱싱 → 메모리 절감 |
| 알고리즘 교체 | HNSW 대신 IVFFlat(빌드 메모리 부담↓) |
-- halfvec로 메모리 절감
ALTER TABLE document_chunks
ALTER COLUMN embedding TYPE halfvec(1024);
CREATE INDEX ON document_chunks
USING hnsw (embedding halfvec_cosine_ops) WITH (m = 16, ef_construction = 64);3. 컬렉션 재생성이 인덱스를 날린다
임베딩 컬렉션을 DROP TABLE 후 재생성하는 재적재 스크립트는 그 위의 HNSW 인덱스도 함께 삭제한다. 재적재 직후에는 인덱스가 없어 조회가 다시 seq scan으로 떨어진다. 재적재 런북에 “인덱스 재적용” 단계를 명시해야 한다.
[재적재 런북]
1. DROP/재생성 후 COPY 대량 적재 (인덱스 없이 적재하면 더 빠름)
2. CREATE INDEX ... USING hnsw (...) (인덱스는 적재 뒤에 붙인다)
3. SET maintenance_work_mem, ANALYZE정리
CREATE EXTENSION vector는 첫 마이그레이션으로 고정한다. 누락하면 임베딩 경로가 조용히 죽는다.- 수백만 행 벡터 테이블에는 HNSW 인덱스가 필수다.
m=16, ef_construction=64로 시작하고 재현율은hnsw.ef_search로 사후 조절한다. - 엄격한 메타필터로 결과가 비면
hnsw.iterative_scan = relaxed_order(≥0.8) 또는ef_search상향. - 빌드 OOM(Exit 137)은
maintenance_work_memvs 메모리 limit 문제다.halfvec/IVFFlat가 탈출구. - 컬렉션을 재생성하면 인덱스가 사라진다. 재적용을 런북 단계로 못 박는다.