- Published on
Milvus IVF_FLAT/HNSW 인덱스 튜닝 - recall↑ 비용↓
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 비용은 그대로인데 검색 품질(리콜)이 떨어지거나, 리콜을 올리려고 nprobe/ef를 올렸더니 p95 지연시간과 CPU가 폭증하는 상황은 Milvus 운영에서 흔합니다. 특히 IVF 계열은 nlist(클러스터 수)와 nprobe(탐색할 클러스터 수)의 균형이, HNSW는 M/efConstruction/ef의 균형이 핵심입니다.
이 글은 IVF_FLAT과 HNSW를 동시에 운영하거나 마이그레이션하는 팀을 기준으로, “리콜을 올리되 비용을 낮추는” 방향으로 튜닝을 진행하는 방법을 정리합니다.
관련해서 ANN 리콜 급락의 전형적인 원인과 HNSW 튜닝만 더 깊게 보고 싶다면 아래 글도 같이 참고하세요.
1) 먼저 정리: 비용·품질을 결정하는 3가지 축
Milvus 벡터 검색 튜닝은 결국 아래 3축의 트레이드오프입니다.
- Recall(품질): 정답(또는 기준 검색 결과)을 얼마나 잘 맞추는가
- Latency(지연시간): p50/p95/p99
- Cost(비용): CPU 사용률, 메모리 상주량, 디스크/네트워크 I/O
여기서 실무적으로 중요한 포인트는 “비용”이 단일 값이 아니라는 것입니다.
- IVF_FLAT은 주로 CPU 스캔량(거리 계산 횟수) 이 비용을 지배합니다.
- HNSW는 메모리(그래프 엣지) 와 탐색 단계 수(
ef) 가 비용을 지배합니다.
즉 같은 리콜 향상이라도, IVF에서는 nprobe를 올리면 CPU가 바로 오르고, HNSW에서는 ef를 올리면 CPU가 오르지만 메모리 구조(M) 를 잘 잡아두면 ef를 과도하게 올리지 않아도 리콜이 나옵니다.
2) IVF_FLAT 핵심 파라미터: nlist와 nprobe
nlist: 클러스터(centroid) 개수
nlist가 크면: 각 클러스터가 작아져서 정확히 들어맞는 클러스터를 찾기 쉬워 리콜에 유리할 수 있음- 하지만 너무 크면: 학습/빌드 비용이 증가하고, 분포가 나쁘면 오히려 리콜이 흔들릴 수 있음
경험적으로는 데이터 수 N에 대해 nlist를 대략 sqrt(N) 근방에서 시작하는 경우가 많습니다(절대 규칙은 아님).
- 예:
N=1,000,000이면sqrt(N)≈1000이므로nlist=1024부터 시작
nprobe: 검색 시 탐색할 클러스터 수
nprobe가 커지면: 더 많은 클러스터를 뒤져서 리콜이 증가- 대신 비용은 거의 선형으로 증가: 거리 계산량이 증가
IVF_FLAT에서 비용을 낮추는 가장 강력한 레버는 결국 nprobe를 낮추는 것입니다. 따라서 목표는 아래처럼 바뀝니다.
- 같은 리콜을 얻기 위해 필요한
nprobe를 줄이기 - 즉
nlist를 포함한 인덱스/데이터 분포를 개선해 “적은 클러스터만 봐도 맞추는” 상태 만들기
3) HNSW 핵심 파라미터: M, efConstruction, ef
M: 노드당 연결(엣지) 수
M이 크면: 그래프가 촘촘해져서 탐색이 쉬워 리콜에 유리- 대신 메모리 사용량이 증가(엣지 저장)
대략적인 감각은 이렇습니다.
M증가: 메모리↑, 빌드 시간↑, 같은 리콜에서ef를 낮출 여지가 생김
efConstruction: 빌드 품질(시간)
- 크면: 더 좋은 그래프를 만들 가능성이 높아 리콜에 유리
- 대신 인덱스 빌드 시간이 증가
인덱스를 자주 재빌드하지 않는 서비스라면 efConstruction을 충분히 주고, 런타임 ef를 낮춰 비용을 줄이는 전략이 잘 먹힙니다.
ef: 검색 시 후보 확장 폭
ef가 커질수록 리콜이 증가- 대신 CPU 비용과 지연시간이 증가
HNSW 비용 최적화의 핵심은 ef를 무작정 올리는 게 아니라,
M/efConstruction으로 그래프 품질을 확보하고- 목표 리콜을 만족하는 최소
ef를 찾는 것
입니다.
4) “리콜 기준”을 먼저 고정해야 튜닝이 됩니다
튜닝 전에 반드시 정답(ground truth) 을 정의해야 합니다.
- 가장 쉬운 방법: 동일 데이터에서
FLAT인덱스로 topK를 구해 기준으로 삼기 - 또는 오프라인에서 exact kNN(브루트포스) 결과를 저장
그리고 평가 지표를 고정합니다.
Recall@K(예:Recall@10)nDCG@K가 필요한 경우도 있지만, 벡터 검색 인프라 튜닝은 보통Recall@K로 충분합니다.
간단한 측정 루프(의사 코드)
아래는 “튜닝 루프”의 형태를 보여주는 의사 코드입니다. 실제 SDK 호출은 환경에 맞게 바꾸면 됩니다.
# pseudo-code
configs = [
{"index": "IVF_FLAT", "nlist": 1024, "nprobe": 8},
{"index": "IVF_FLAT", "nlist": 2048, "nprobe": 8},
{"index": "HNSW", "M": 16, "efConstruction": 200, "ef": 64},
]
for cfg in configs:
build_index(cfg)
warmup_queries()
metrics = run_benchmark(
queries=eval_queries,
topk=10,
ground_truth=flat_results,
measure=["recall@10", "p95_ms", "cpu", "rss_mb"],
)
print(cfg, metrics)
이 루프가 있어야 “리콜↑ 비용↓”가 숫자로 검증됩니다.
5) IVF_FLAT 튜닝 절차: nlist를 먼저 잡고 nprobe를 최소화
Step 1. nlist 후보를 2~3개만 고릅니다
너무 많은 조합을 돌리면 시간이 끝없이 늘어납니다. 아래처럼 2~3개만 잡고 시작하세요.
nlist:1024,2048,4096(데이터 규모에 따라 스케일)
Step 2. 각 nlist에서 nprobe-리콜 곡선을 뽑습니다
nprobe를 1, 2, 4, 8, 16, 32… 식으로 올리면서 Recall@K와 p95를 함께 기록합니다.
관찰 포인트:
- 리콜이 초반에 급상승하다가 어느 순간 완만해지는 “무릎(knee)”이 나타나는지
- 그 무릎 지점의
nprobe가 비용 대비 효율이 가장 좋습니다
Step 3. 목표 리콜을 만족하는 최소 nprobe를 선택
예를 들어 목표가 Recall@10 >= 0.95라면,
nlist=2048에서nprobe=12에 도달nlist=4096에서nprobe=8에 도달
같은 식의 결과가 나올 수 있습니다.
이때는 다음을 비교합니다.
- 인덱스 빌드/메모리 오버헤드(대개
nlist가 큰 쪽이 불리) - 검색 시 CPU/지연시간(대개
nprobe가 작은 쪽이 유리)
즉 “빌드 비용 vs 런타임 비용”의 균형으로 결정합니다.
6) HNSW 튜닝 절차: M으로 바닥을 만들고 ef를 최소화
Step 1. M을 2~3개만 고릅니다
M:8,16,32정도를 후보로
메모리가 빡빡한 환경이면 M=16을 상한으로 두는 편이 많고, 리콜이 중요하고 메모리가 충분하면 M=32가 유리한 경우가 많습니다.
Step 2. efConstruction은 “빌드 여유”에 맞춰 고정
- 자주 재빌드하지 않는다면
efConstruction=200~400같은 범위에서 시작 - 자주 재빌드한다면
100~200으로 타협
핵심은 efConstruction을 너무 낮게 잡아 그래프 품질을 망치면, 런타임 ef를 올려도 리콜이 잘 안 오르는 구간이 생긴다는 점입니다.
Step 3. ef-리콜 곡선을 뽑고, 목표 리콜의 최소 ef를 선택
ef:16,32,64,128,256식으로 증가
일반적으로 ef는 리콜과 지연시간이 매우 직관적으로 교환됩니다. 목표 리콜을 만족하는 최소 ef 가 곧 비용 최적점입니다.
7) IVF_FLAT vs HNSW: 어떤 상황에서 무엇이 유리한가
IVF_FLAT이 유리한 경우
- 메모리를 아껴야 함(특히 HNSW의 그래프 오버헤드가 부담)
- 데이터가 매우 크고, 디스크/세그먼트 전략과 함께 운영 최적화를 하고 싶음
- “리콜을 조금 희생하고 비용을 크게 줄이는” 운영을 선호
다만 IVF는 nprobe를 올리면 비용이 바로 늘기 때문에, 목표 리콜이 높아질수록 비용이 빠르게 증가할 수 있습니다.
HNSW가 유리한 경우
- 높은 리콜이 필요하고, p95를 안정적으로 맞춰야 함
- 메모리를 더 써도 되는 대신 CPU를 아끼고 싶음(적절한
M/ef조합) - 데이터 분포가 복잡해서 IVF의 클러스터링이 잘 안 먹히는 경우
8) 운영에서 자주 하는 실수 6가지(리콜↓ 비용↑)
- 리콜 기준 없이 p95만 보고 튜닝: 결과적으로 “빠르지만 틀린 검색”이 됨
- IVF에서
nlist를 과도하게 키우고nprobe도 크게 유지: 빌드 비용↑, 런타임 비용↑ - HNSW에서
M을 너무 낮게 잡고ef만 계속 올림: 리콜이 잘 안 오르고 비용만 증가 efConstruction을 너무 낮게 잡아 그래프 품질이 깨짐: 런타임으로 해결 불가한 구간 발생- 워밍업 없이 벤치마크: 캐시/세그먼트 로딩 영향으로 결과가 왜곡
- topK가 바뀌는데 파라미터를 그대로 사용:
topK가 커지면 필요한nprobe/ef도 달라짐
9) Milvus에서 인덱스 생성/검색 파라미터 예시
아래 예시는 “어떤 필드를 어디에 넣는지” 감을 주기 위한 샘플입니다. 사용하는 SDK 버전에 따라 키 이름이 다를 수 있으니, 실제 적용 전에는 Milvus 문서/SDK 타입을 확인하세요.
IVF_FLAT 인덱스 생성 예시
index_params = {
"metric_type": "COSINE",
"index_type": "IVF_FLAT",
"params": {
"nlist": 2048
}
}
collection.create_index(
field_name="embedding",
index_params=index_params
)
IVF_FLAT 검색 시 nprobe 지정 예시
search_params = {
"metric_type": "COSINE",
"params": {
"nprobe": 12
}
}
results = collection.search(
data=query_vectors,
anns_field="embedding",
param=search_params,
limit=10,
output_fields=["doc_id"]
)
HNSW 인덱스 생성 예시
index_params = {
"metric_type": "COSINE",
"index_type": "HNSW",
"params": {
"M": 16,
"efConstruction": 200
}
}
collection.create_index(
field_name="embedding",
index_params=index_params
)
HNSW 검색 시 ef 지정 예시
search_params = {
"metric_type": "COSINE",
"params": {
"ef": 64
}
}
results = collection.search(
data=query_vectors,
anns_field="embedding",
param=search_params,
limit=10
)
10) 추천 시작점(워크로드별)
아래 값들은 “정답”이 아니라 첫 벤치마크를 빨리 돌리기 위한 시작점입니다.
(A) 비용 민감 + 중간 리콜(예: Recall@10 0.9 내외)
- IVF_FLAT:
nlist=1024~2048,nprobe=4~12 - HNSW:
M=16,efConstruction=200,ef=32~64
(B) 고리콜(예: Recall@10 0.95~0.98)
- IVF_FLAT:
nlist=2048~4096,nprobe=12~32 - HNSW:
M=16~32,efConstruction=200~400,ef=64~128
(C) p95 안정성이 최우선(온라인 서빙)
- HNSW 쪽이 유리한 경우가 많음
M을 올려ef를 낮추는 방향으로 p95를 안정화
11) 결론: “리콜 곡선”을 뽑으면 비용은 자연히 내려갑니다
IVF_FLAT과 HNSW 모두, 튜닝의 본질은 같습니다.
- (1) 정답 기준(FLAT) 을 만들고
- (2)
nprobe또는ef를 변화시키며 리콜-비용 곡선을 뽑고 - (3) 목표 리콜을 만족하는 최소 비용 지점을 고른다
그리고 IVF/HNSW 각각에서 “최소 비용 지점”을 더 낮추는 방법은 다음으로 요약됩니다.
- IVF_FLAT:
nlist를 적절히 잡아 필요nprobe를 줄이기 - HNSW:
M/efConstruction으로 그래프 품질을 확보해 필요ef를 줄이기
이 과정을 자동화(벤치마크 스크립트 + 리포트)해두면, 임베딩 모델이 바뀌거나 데이터 분포가 바뀌어도 같은 절차로 빠르게 재튜닝할 수 있어 운영 비용을 크게 줄일 수 있습니다.