- Published on
Milvus 인덱스 성능 튜닝 - HNSW vs IVF_PQ
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
벡터 검색(ANN, Approximate Nearest Neighbor)에서 성능 튜닝은 결국 정확도(recall)·지연시간(latency)·비용(메모리/디스크/빌드 시간) 사이의 균형을 잡는 일입니다. Milvus는 여러 인덱스 타입을 제공하지만, 실무에서 가장 자주 비교되는 조합이 HNSW와 IVF_PQ입니다.
이 글에서는 두 인덱스의 내부 동작 차이를 기반으로, 어떤 상황에 무엇을 선택해야 하는지, 그리고 Milvus에서 실제로 손대야 하는 파라미터(빌드/서치) 를 중심으로 튜닝 전략을 정리합니다. 또한 “검색이 느리다”를 감으로 해결하지 않도록, 최소한의 벤치마크 루틴과 관측 지표도 함께 제시합니다.
참고: DB 튜닝이 결국 관측과 가설 검증의 반복이라는 점은 벡터 DB도 동일합니다. 전통 DB에서 인덱스가 안 타는 원인을 추적하는 접근은 아래 글과 결이 비슷합니다.
HNSW vs IVF_PQ 한눈에 보는 선택 기준
HNSW가 유리한 경우
- 낮은 지연시간이 최우선 (온라인 추천, 실시간 검색)
- 높은 recall을 원함 (상위
k결과 품질이 중요) - 데이터가 메모리에 어느 정도 올라갈 수 있음 (메모리 여유)
- 인덱스 빌드 시간이 다소 길어도 감수 가능
HNSW는 그래프 기반 탐색으로, 잘 튜닝하면 매우 낮은 지연시간과 높은 recall을 동시에 얻기 쉽습니다. 대신 메모리 사용량이 커지고, 빌드 비용이 증가합니다.
IVF_PQ가 유리한 경우
- 데이터가 매우 크고 메모리 절약이 중요
- 디스크/메모리 계층을 활용해 비용 대비 성능을 원함
- 약간의 recall 손실을 허용할 수 있음
- 인덱스 빌드/업데이트 파이프라인을 운영적으로 관리할 수 있음
IVF_PQ는 “거친 후보군(IVF) + 압축(PQ)” 조합입니다. 큰 데이터에서 비용 효율이 좋지만, 파라미터 조합에 따라 recall이 민감하게 흔들릴 수 있습니다.
내부 동작 차이로 이해하는 튜닝 포인트
HNSW: 그래프 품질과 탐색 폭의 싸움
HNSW는 벡터들을 다층 그래프에 연결하고, 탐색 시 그래프를 따라 이동하며 근접 이웃을 찾습니다.
M: 노드당 연결 수(그래프 밀도)- 커질수록 그래프 품질이 좋아져 recall이 오르는 경향
- 대신 메모리 증가, 빌드 시간 증가
efConstruction: 빌드 시 탐색 후보 폭- 커질수록 그래프 품질이 좋아짐(대체로 recall 상승)
- 빌드 시간이 늘어남
ef: 검색 시 탐색 후보 폭- 커질수록 recall 상승, 지연시간 증가
즉 HNSW는 빌드 단계에서 그래프를 얼마나 “잘” 만들지(efConstruction, M)와, 서치 단계에서 얼마나 “넓게” 탐색할지(ef)가 핵심입니다.
IVF_PQ: 후보군 크기와 양자화 손실의 싸움
IVF_PQ는 두 단계로 생각하면 쉽습니다.
- IVF: 전체 벡터를
nlist개의 클러스터로 나누고, 쿼리는 일부 클러스터(nprobe)만 뒤짐 - PQ: 각 벡터를 압축 코드로 저장해 메모리를 줄임(대신 정보 손실)
nlist: 클러스터 개수(분할 수)- 커질수록 각 클러스터가 작아져 후보가 정교해짐
- 너무 크면 학습/빌드 비용 증가, 데이터 분포에 따라 비효율 가능
nprobe: 검색 시 뒤질 클러스터 개수- 커질수록 recall 상승, 지연시간 증가
m(PQ 서브벡터 개수),nbits(각 코드북 비트 수)m이 커질수록 표현력이 좋아져 recall이 오르는 경향- 대신 메모리/연산량 증가
IVF_PQ는 nprobe를 올려 recall을 회복하는 패턴이 흔하지만, 근본적으로 PQ 압축 손실이 있으므로 HNSW처럼 끝까지 올리긴 어렵습니다.
Milvus에서의 실전 튜닝 절차
1) 목표를 수치로 고정하기
튜닝 전에 아래를 먼저 정합니다.
- 타깃 recall 예: recall@10
0.95이상 - P95 지연시간 예:
30ms이하 - 비용 제약: 메모리 상한, 디스크 상한, 빌드 허용 시간
이걸 고정하지 않으면, 파라미터를 올릴수록 좋아 보이는(하지만 비용 폭발하는) 방향으로 계속 가게 됩니다.
2) 데이터 분포와 차원 확인
- 차원 수(
dim)가 커질수록 연산량 증가 - 코사인/내적/유클리드 등 metric에 따라 체감 성능이 달라질 수 있음
- 중복 벡터가 많거나 분포가 한쪽으로 몰리면 IVF 클러스터링 효율이 떨어질 수 있음
3) HNSW 튜닝 가이드
추천 시작점(경험적)
M:16또는32efConstruction:200전후- 검색
ef:64부터 시작해 recall이 부족하면 단계적으로 증가
튜닝 순서
- 빌드 품질 확보:
M과efConstruction을 먼저 적정 수준으로 - 서치 품질 조절:
ef로 recall과 latency 트레이드오프 맞추기
ef는 온라인에서 동적으로 바꾸기 쉬운 편이라, 운영 중에도 A/B로 조정하기 좋습니다.
4) IVF_PQ 튜닝 가이드
추천 시작점(경험적)
nlist: 전체 벡터 수를N이라 할 때 대략sqrt(N)근처를 시작점으로(절대 법칙은 아님)nprobe:8또는16부터 시작- PQ
m:dim을 나누기 좋은 값(예:dim=768이면m=96등),nbits=8이 흔한 기본값
튜닝 순서
nlist를 먼저 합리적으로 설정(너무 작으면 후보군이 비대해지고, 너무 크면 관리 비용 증가)- recall이 부족하면
nprobe를 올려 회복 - 그래도 부족하면 PQ 파라미터(
m,nbits)를 조정하거나, 아예 IVF_PQ 대신 다른 인덱스를 검토
IVF_PQ는 특히 nprobe가 낮으면 “왜 이렇게 못 찾지?”가 쉽게 발생합니다. 대신 nprobe를 올리면 latency가 선형에 가깝게 증가할 수 있어 P95를 꼭 봐야 합니다.
PyMilvus 예제로 보는 인덱스 생성과 검색 파라미터
아래 예제는 PyMilvus 기준의 전형적인 흐름입니다. (환경에 따라 import 및 연결 코드는 조정하세요.)
from pymilvus import (
connections, Collection, CollectionSchema, FieldSchema, DataType
)
connections.connect(alias="default", host="localhost", port="19530")
# schema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="vec", dtype=DataType.FLOAT_VECTOR, dim=768),
]
schema = CollectionSchema(fields, description="hnsw vs ivf_pq demo")
col = Collection(name="demo_vectors", schema=schema)
HNSW 인덱스 생성
index_params = {
"index_type": "HNSW",
"metric_type": "IP",
"params": {
"M": 16,
"efConstruction": 200,
},
}
col.create_index(field_name="vec", index_params=index_params)
col.load()
HNSW 검색 시 ef 조정
search_params = {
"metric_type": "IP",
"params": {
"ef": 64,
},
}
results = col.search(
data=[query_vec],
anns_field="vec",
param=search_params,
limit=10,
output_fields=[],
)
IVF_PQ 인덱스 생성
index_params = {
"index_type": "IVF_PQ",
"metric_type": "IP",
"params": {
"nlist": 4096,
"m": 96,
"nbits": 8,
},
}
col.release()
col.drop_index()
col.create_index(field_name="vec", index_params=index_params)
col.load()
IVF_PQ 검색 시 nprobe 조정
search_params = {
"metric_type": "IP",
"params": {
"nprobe": 16,
},
}
results = col.search(
data=[query_vec],
anns_field="vec",
param=search_params,
limit=10,
)
벤치마크를 “제대로” 하는 최소 체크리스트
1) Ground truth 만들기
ANN 품질을 보려면 정답이 필요합니다. 샘플 쿼리에 대해 brute force(정확 검색)로 top k를 구해 ground truth로 삼습니다.
- 샘플 쿼리 수: 최소 수백 개, 가능하면 수천 개
k: 서비스에서 쓰는 값 그대로(예:k=10)
2) 지연시간은 평균이 아니라 P95/P99
IVF_PQ에서 nprobe를 올리거나, HNSW에서 ef를 올리면 평균은 괜찮아 보여도 tail latency가 무너지는 경우가 있습니다.
- P50, P95, P99
- 동시성(동시 쿼리 수)별 측정
3) 필터링이 있으면 반드시 포함
실무에서는 메타데이터 필터(테넌트, 카테고리, 날짜 등)가 들어갑니다. 이때 후보군이 줄어 성능이 좋아질 수도 있고, 반대로 실행 경로가 복잡해져 느려질 수도 있습니다.
전통적인 조인/파이프라인 튜닝처럼 “필터가 성능에 미치는 영향”을 분리해서 봐야 합니다.
운영에서 자주 겪는 함정과 해결 방향
HNSW에서 흔한 문제
- 메모리 급증:
M을 과도하게 키우면 인덱스가 빠르게 비대해집니다.- 해결:
M을 낮추고, 부족한 recall은 검색ef로 보정
- 해결:
- 빌드 시간이 너무 김:
efConstruction이 과도하면 인덱스 생성 시간이 길어집니다.- 해결: 오프라인 빌드 파이프라인을 두거나,
efConstruction을 단계적으로 낮춰 품질-시간 균형점 탐색
- 해결: 오프라인 빌드 파이프라인을 두거나,
IVF_PQ에서 흔한 문제
- recall이 기대보다 낮음:
nprobe가 너무 낮거나, PQ 압축 손실이 큼- 해결: 먼저
nprobe를 올려보고, 그래도 부족하면m을 늘리거나 다른 인덱스 고려
- 해결: 먼저
- 지연시간 변동이 큼: 분포가 불균일하면 특정 클러스터가 비대해져 tail latency가 악화
- 해결:
nlist재조정, 데이터 샤딩 전략 점검, 클러스터링 학습 품질 확인
- 해결:
결론: 어떤 인덱스를 선택해야 하나
- 온라인 실시간 품질과 속도가 핵심이고 메모리를 감당할 수 있으면 HNSW가 1순위인 경우가 많습니다. 튜닝도
ef중심으로 운영 중 조절이 쉬운 편입니다. - 데이터가 크고 비용 제약이 강하며, 약간의 품질 손실을 감수할 수 있으면 IVF_PQ가 좋은 선택입니다. 대신
nlist·nprobe·m조합을 벤치마크로 고정하고 운영해야 흔들리지 않습니다.
마지막으로, “느리다”는 증상은 인덱스만의 문제가 아닐 수 있습니다. 동시성 증가로 인한 리소스 경합, 워커 재시작 루프, OOM 등 시스템 레벨 이슈가 ANN 지연시간을 악화시키기도 합니다. 그런 경우에는 애플리케이션/플랫폼 관측까지 포함해 원인을 좁혀야 합니다.
다음 단계로는, 여러분의 데이터 크기(N), 차원(dim), 목표 recall@k, 목표 P95를 기준으로 HNSW와 IVF_PQ 각각에 대해 ef와 nprobe를 스윕하는 간단한 실험표를 만들고, 비용까지 함께 기록해 “정답 파라미터”를 문서화하는 것을 권합니다.