- Published on
Milvus HNSW/IVF 파라미터 튜닝으로 리콜 올리기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서치 품질이 기대보다 낮을 때(특히 topK 결과가 “뭔가 비슷한데 안 맞는” 느낌일 때) 가장 먼저 의심해야 하는 건 임베딩 모델만이 아닙니다. Milvus에서 HNSW/IVF 인덱스 파라미터와 검색 파라미터가 리콜(Recall)에 직접적인 영향을 주고, 같은 데이터/모델이라도 설정에 따라 체감 품질이 크게 달라집니다.
이 글은 “리콜을 올리고 싶다”는 목표에 맞춰, HNSW와 IVF 계열 인덱스에서 무엇을 어떻게 올려야 하는지, 그리고 그 대가로 무엇이 늘어나는지(지연시간/메모리/빌드 시간)를 실무 관점에서 정리합니다.
또한 튜닝은 결국 실험입니다. 그래서 마지막에는 **실험 설계(ground truth 구성, 메트릭, 단계적 탐색)**까지 포함합니다.
리콜이 떨어지는 대표 원인 5가지
Milvus 파라미터 튜닝을 시작하기 전에, 리콜 저하의 원인을 빠르게 분류하면 시행착오가 줄어듭니다.
- 검색 파라미터가 너무 보수적
- HNSW의
ef가 낮거나, IVF의nprobe가 낮으면 후보 탐색이 부족해 리콜이 떨어집니다.
- HNSW의
- 인덱스 빌드 파라미터가 너무 낮음
- HNSW의
M,efConstruction, IVF의nlist가 부적절하면 인덱스 자체가 “성글게” 만들어집니다.
- HNSW의
- 데이터 분포/스케일 문제
- 코사인 유사도를 쓰는데 벡터가 정규화되지 않았거나, inner product 기반인데 스케일이 들쭉날쭉하면 품질이 흔들립니다.
- 필터링(Scalar filter)로 후보가 급감
expr필터가 강하면 벡터 후보 풀이 줄어 ANN 근사가 더 불리해집니다. 이때는 파라미터를 더 공격적으로 올려야 합니다.
- 세그먼트/메모리/캐시 상태로 인한 변동
- 워밍업이 안 됐거나, 메모리 압박으로 디스크/캐시 미스가 늘면 같은 설정에서도 품질/지연시간이 출렁일 수 있습니다.
지연시간이 갑자기 늘거나 타임아웃이 생기면 애플리케이션 레벨에서도 관측/진단이 필요합니다. 분산 환경에서 호출 타임아웃이 튜닝을 방해할 정도라면 Go gRPC context deadline exceeded 원인·해결 같은 글의 체크리스트도 함께 참고하면 좋습니다.
Milvus에서 HNSW vs IVF: 리콜 관점의 차이
둘 다 ANN(Approximate Nearest Neighbor)지만 리콜을 올리는 레버가 다릅니다.
HNSW
- 그래프 기반 탐색. 검색 시 그래프를 더 깊게/넓게 탐색하면 리콜이 올라갑니다.
- 주로
ef(검색),M/efConstruction(빌드)이 핵심. - 메모리 사용량이 비교적 큼.
IVF(IVF_FLAT, IVF_SQ8 등)
- 클러스터(버킷) 기반. 검색 시 더 많은 버킷을 뒤지면 리콜이 올라갑니다.
nlist(클러스터 수),nprobe(탐색할 클러스터 수)가 핵심.- 대규모 데이터에서 튜닝 폭이 크고, 압축(SQ8/PQ)과 함께 쓰는 경우가 많음.
리콜만 놓고 보면 “무조건 HNSW”가 답은 아닙니다. 데이터 규모, 메모리 예산, 업데이트 패턴, 필터 사용 여부에 따라 IVF가 더 안정적인 선택일 때도 많습니다.
HNSW 튜닝: 리콜을 올리는 3개의 레버
HNSW에서 리콜은 보통 아래 순서로 올립니다.
1) 검색 파라미터 ef를 올려라 (가장 즉효)
- 의미: 검색 시 유지하는 후보 리스트 크기(탐색 폭)
- 효과:
ef증가=>리콜 증가 - 비용: 지연시간 증가, CPU 사용량 증가
실무적으로는 ef를 먼저 올려서 목표 리콜을 달성한 뒤, 지연시간이 너무 크면 그때 빌드 파라미터를 조정해 “같은 리콜을 더 싸게” 만드는 식이 효율적입니다.
2) 빌드 파라미터 M을 올려라 (그래프 밀도)
- 의미: 노드당 연결(이웃) 수
- 효과: 그래프가 촘촘해져 탐색 경로가 좋아짐
=>리콜 증가/지연시간 감소 가능 - 비용: 메모리 증가(상당히 큼), 빌드 시간 증가
경험적으로 M은 리콜과 지연시간 모두에 영향을 주지만, 메모리 비용이 커서 무턱대고 올리기 어렵습니다.
3) 빌드 파라미터 efConstruction을 올려라 (빌드 품질)
- 의미: 인덱스 생성 시 후보 탐색 폭
- 효과: 더 좋은 그래프 구성
=>리콜 증가 - 비용: 인덱스 빌드 시간 증가
운영에서 인덱스를 자주 재빌드하지 않는다면, efConstruction을 올려 빌드 품질을 확보하는 것이 장기적으로 이득인 경우가 많습니다.
HNSW 추천 튜닝 흐름
- 기준선:
M=16,efConstruction=200,ef=64정도에서 시작 - 목표 리콜까지
ef를 단계적으로 증가:64 -> 128 -> 256 -> 512 - 지연시간이 너무 커지면
M을 소폭 증가:16 -> 24 -> 32 - 재빌드 가능하면
efConstruction상향:200 -> 400 -> 800
IVF 튜닝: 리콜을 올리는 2개의 레버
IVF는 “얼마나 잘 나누고(nlist), 얼마나 많이 뒤지느냐(nprobe)”로 이해하면 쉽습니다.
1) 검색 파라미터 nprobe를 올려라 (가장 즉효)
- 의미: 검색 시 탐색할 클러스터 개수
- 효과:
nprobe증가=>리콜 증가 - 비용: 지연시간 증가(거의 선형에 가까움), CPU 증가
리콜이 낮을 때 가장 빠른 처방은 nprobe를 올리는 것입니다.
2) 인덱스 파라미터 nlist를 조정하라 (클러스터 수)
- 의미: 클러스터(centroid) 개수
- 효과: 적절한
nlist는 같은nprobe에서도 더 좋은 후보를 모음 - 비용: 빌드 시간/메모리 증가(centroid 및 메타데이터)
nlist가 너무 작으면 클러스터가 뭉개져 근사 오차가 커지고, 너무 크면 각 클러스터가 너무 작아져 nprobe를 많이 올려야 하는 상황이 생길 수 있습니다.
IVF 추천 튜닝 흐름
- 기준선
nlist설정 후nprobe를 올려 목표 리콜 달성 nprobe가 너무 커져 지연시간이 부담되면nlist를 재조정- (압축 인덱스 사용 시) SQ8/PQ는 리콜 손실이 있을 수 있으니 먼저 IVF_FLAT으로 상한을 확인
Milvus 인덱스/검색 파라미터 예시 코드 (Python)
아래 예시는 Milvus Python SDK 기준의 “형태”를 보여주는 예시입니다. 실제 프로젝트에서는 사용 중인 Milvus 버전과 SDK(pymilvus) 버전에 맞춰 필드/메서드 이름을 확인하세요.
from pymilvus import (
connections, FieldSchema, CollectionSchema, DataType,
Collection
)
connections.connect(alias="default", host="127.0.0.1", port="19530")
dim = 768
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=dim),
]
schema = CollectionSchema(fields, description="hnsw/ivf tuning demo")
col = Collection(name="demo_vec", schema=schema)
# HNSW 인덱스 생성
hnsw_index = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16,
"efConstruction": 200
}
}
col.create_index(field_name="embedding", index_params=hnsw_index)
# 검색 시 HNSW 파라미터(핵심: ef)
search_params_hnsw = {
"metric_type": "COSINE",
"params": {
"ef": 128
}
}
# IVF_FLAT 인덱스 생성
ivf_index = {
"index_type": "IVF_FLAT",
"metric_type": "COSINE",
"params": {
"nlist": 4096
}
}
# 필요 시 인덱스를 바꿔가며 실험하려면 기존 인덱스 drop 후 생성하는 절차를 사용
# col.drop_index()
# col.create_index(field_name="embedding", index_params=ivf_index)
# 검색 시 IVF 파라미터(핵심: nprobe)
search_params_ivf = {
"metric_type": "COSINE",
"params": {
"nprobe": 32
}
}
# 실제 검색 호출 예시
query_vecs = [[0.0] * dim] # 예시
res = col.search(
data=query_vecs,
anns_field="embedding",
param=search_params_hnsw,
limit=10,
output_fields=[]
)
print(res)
위 코드에서 중요한 포인트는 인덱스 파라미터와 검색 파라미터가 분리되어 있다는 점입니다.
- HNSW: 빌드
M,efConstruction/ 검색ef - IVF: 빌드
nlist/ 검색nprobe
리콜이 낮을 때는 먼저 검색 파라미터(즉효)를 올리고, 비용이 부담되면 빌드 파라미터를 조정해 구조적으로 개선하는 접근이 효율적입니다.
리콜을 수치로 올리려면: 실험 설계가 80%다
“리콜이 올랐다”를 말하려면 기준이 필요합니다. 추천하는 방법은 아래와 같습니다.
1) Ground truth 만들기
- 샘플 쿼리
Q를 준비 (예: 1,000개) - 각 쿼리에 대해 정확 검색(브루트포스) 결과
topK를 ground truth로 저장- Milvus에서 가능한 경우 FLAT 인덱스(또는 별도 오프라인 계산)로 생성
Recall@K는 보통 아래처럼 정의합니다.
Recall@K = |ANN_topK ∩ GT_topK| / K
2) 측정 지표를 함께 본다
리콜만 올리면 비용이 폭증할 수 있으니, 최소한 아래를 같이 기록하세요.
Recall@10,Recall@50p95 latency,p99 latency- QPS(throughput)
- CPU 사용률, 메모리 사용률
3) 파라미터 탐색은 “한 번에 하나씩”
- HNSW:
ef만 올려 곡선을 먼저 얻고, 그 다음M또는efConstruction을 조정 - IVF:
nprobe만 올려 곡선을 먼저 얻고, 그 다음nlist를 조정
이렇게 하면 “리콜-지연시간 곡선”이 깔끔하게 나오고, 의사결정이 쉬워집니다.
튜닝 치트시트: 목표별 빠른 처방
리콜이 낮고 지연시간 여유가 있다
- HNSW:
ef증가 - IVF:
nprobe증가
리콜이 낮고 지연시간도 빡빡하다
- HNSW:
M소폭 증가 +ef는 필요한 만큼만 - IVF:
nlist재조정으로 같은 리콜을 더 낮은nprobe에서 달성하도록 유도
필터가 강해서 리콜이 급락한다
- 필터로 후보 풀이 줄어드는 만큼 ANN 근사가 불리해짐
- 해결:
ef또는nprobe를 평소보다 더 올리고, 가능하면 필터 카디널리티를 낮추는 방향(인덱싱/전처리/쿼리 설계)도 병행
운영에서 간헐적으로 품질/지연시간이 출렁인다
- 워밍업, 세그먼트 상태, 캐시, 네트워크 타임아웃 등 비기능 요인을 점검
- 애플리케이션에서 캐시/타임아웃이 꼬여 관측이 어려우면 Next.js App Router RSC 캐시 꼬임 해결법처럼 “캐시 일관성”을 점검하는 접근도 도움이 됩니다(벡터 검색 결과 캐시를 두는 경우 특히).
결론: 리콜은 ef/nprobe로 올리고, 비용은 빌드 파라미터로 줄인다
Milvus에서 리콜을 올리는 가장 빠른 방법은 단순합니다.
- HNSW는
ef를 올리면 리콜이 오른다. - IVF는
nprobe를 올리면 리콜이 오른다.
하지만 운영은 비용의 게임이기도 합니다. 목표 리콜을 달성한 다음에는:
- HNSW:
M,efConstruction으로 “같은 리콜을 더 빠르게/안정적으로” - IVF:
nlist로 “같은 리콜을 더 낮은nprobe로”
이 순서로 접근하면 튜닝이 감이 아니라 데이터 기반으로 진행됩니다.
다음 단계로는, 여러분의 데이터셋 규모(예: 100만/1천만/1억), 차원 수, 필터 사용 패턴을 알려주면 HNSW/IVF 중 어떤 축으로 튜닝을 시작하는 게 유리한지와 추천 초기값(탐색 범위)까지 더 구체적으로 제안할 수 있습니다.