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

특성HNSWIVFFlat
빌드 메모리높음(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_mem vs 메모리 limit 문제다. halfvec/IVFFlat가 탈출구.
  • 컬렉션을 재생성하면 인덱스가 사라진다. 재적용을 런북 단계로 못 박는다.

관련: pgvector - 대규모 임베딩 적재 튜닝 · 안전한 스키마 마이그레이션 · jsonb 인덱싱