HNSW와 IVFFlat은 벡터 유사도 검색을 빠르게 하는 두 가지 근사 최근접 이웃(ANN) 인덱스다. 둘 다 pgvector에서 지원하는데, HNSW는 그래프 기반으로 정확도·속도가 뛰어난 대신 빌드가 무겁고, IVFFlat은 군집 기반으로 가볍지만 파라미터 튜닝이 까다롭다. RAG나 추천 시스템에서 어떤 인덱스를 고를지 정할 때 핵심이 되는 갈림길이다.
벡터 검색이 흔해지면서 "그냥 인덱스 걸면 되는 거 아냐?"라고 생각하기 쉬운데, 두 인덱스는 성격이 꽤 다르다. 나도 처음엔 IVFFlat로 시작했다가 리콜(recall)이 들쭉날쭉해서 한참 헤맸다.
근사 검색이라는 전제
먼저 짚을 것. 이 둘은 정확한 최근접이 아니라 근사다. 100만 개 벡터에서 진짜 가장 가까운 것을 매번 완벽히 찾으려면 전수 비교가 필요한데, 그건 너무 느리다. 그래서 약간의 정확도를 내주고 속도를 얻는다. 이 "약간"을 얼마나 내줄지가 곧 파라미터 튜닝이다.
정확도의 척도가 리콜이다. 진짜 정답 상위 K개 중 몇 개를 실제로 찾아냈는지의 비율. 리콜을 올리면 느려지고, 속도를 올리면 리콜이 떨어진다. 이 트레이드오프를 두 인덱스가 각자 다른 방식으로 조절한다.
IVFFlat — 군집으로 나눠 뒤진다
전체 벡터를 lists개의 군집(centroid)으로 나눠 두고, 검색 시엔 질의 벡터와 가까운 몇 개 군집(probes)만 뒤진다. 나머지는 통째로 건너뛴다.
-- lists: 군집 개수. 대략 행 수의 제곱근 언저리로 시작
CREATE INDEX ON items
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 1000);
-- 검색 시 몇 개 군집을 뒤질지
SET ivfflat.probes = 10;
SELECT id FROM items
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 10;
주의할 점이 하나 있다. IVFFlat 인덱스는 데이터가 이미 들어 있는 상태에서 만들어야 군집이 제대로 잡힌다. 빈 테이블에 인덱스부터 걸고 데이터를 부으면 군집이 엉망이 되어 리콜이 무너진다. 이걸 모르고 삽질했다. probes를 늘리면 리콜이 오르지만 그만큼 느려진다.
HNSW — 계층 그래프를 타고 간다
벡터들을 서로 연결한 다층 그래프를 만들어, 위층에서 대충 가까운 데로 점프하고 아래층으로 내려가며 정밀하게 좁힌다. 빌드는 오래 걸리지만 검색 품질과 속도가 대체로 IVFFlat보다 낫다.
CREATE INDEX ON items
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 검색 시 후보 폭. 클수록 리콜↑ 속도↓
SET hnsw.ef_search = 40;
SELECT id FROM items
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 10;
m은 노드당 연결 수, ef_construction은 빌드 시 탐색 폭이다. 둘을 키우면 인덱스 품질이 좋아지는 대신 빌드 시간과 메모리가 늘어난다. 검색 순간의 리콜은 ef_search로 조절한다. 빈 테이블에 미리 만들어도 되고, 데이터가 계속 추가돼도 점진적으로 반영된다는 점이 IVFFlat과 크게 다르다.
한눈에 비교
| 항목 | IVFFlat | HNSW |
|---|---|---|
| 빌드 속도 | 빠름 | 느림 |
| 검색 속도·리콜 | 보통 | 우수 |
| 메모리 | 적음 | 많음 |
| 데이터 선적재 필요 | 필요 | 불필요 |
| 증분 추가 | 약함(재빌드 권장) | 잘 됨 |
그래서 뭘 쓰나
개인적으로 지금은 대부분 HNSW로 기본을 잡는다. 리콜이 안정적이고, 데이터가 계속 들어오는 서비스에서 재빌드 걱정이 적기 때문이다. 대신 메모리를 넉넉히 확보해야 하고, 초기 빌드가 오래 걸리는 건 감수한다.
IVFFlat은 벡터 수가 많고 메모리가 빠듯하며, 데이터가 배치로 한 번에 적재되는 경우에 여전히 매력적이다. 빌드가 빠르고 인덱스가 가벼우니까. 결국 "메모리와 빌드 시간을 아낄 것이냐(IVFFlat), 안정적 품질과 증분 갱신을 택할 것이냐(HNSW)"의 문제다.
자주 묻는 질문
거리 연산자 vector_cosine_ops는 뭘 뜻하나요?
인덱스가 사용할 거리 척도를 지정합니다. 코사인 유사도면 vector_cosine_ops(<=>), L2 거리면 vector_l2_ops(<->)입니다. 임베딩 모델이 코사인 기준으로 학습됐다면 코사인을 쓰는 게 자연스럽습니다.
ef_search와 probes는 인덱스를 다시 만들어야 바뀌나요?
아니요. 둘 다 검색 시점 파라미터라 세션에서 SET으로 즉시 조절합니다. 리콜이 낮으면 이 값을 올리고, 너무 느리면 내려서 실측으로 균형점을 찾으면 됩니다.
필터 조건과 벡터 검색을 같이 쓰면 리콜이 떨어지던데요?
맞습니다. WHERE category = 'x' 같은 필터를 벡터 정렬과 함께 걸면, 인덱스가 찾은 근사 후보가 필터에 다 걸러져 결과가 부족해질 수 있습니다. 부분 인덱스로 자주 쓰는 필터를 미리 반영하거나, 후보 K를 넉넉히 잡아 필터 후에도 충분히 남게 하는 식으로 대응합니다.
차원이 크면 무엇을 조심해야 하나요?
차원이 커질수록 인덱스 크기와 메모리·계산량이 늘고, 이른바 차원의 저주로 근사 품질이 흔들립니다. 임베딩 차원 축소(예: Matryoshka 계열)나 half-precision 저장을 검토하면 비용을 줄일 수 있습니다.

댓글 0