- Published on
Milvus HNSW·IVF 튜닝으로 검색속도 5배
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 벡터 검색 워크로드를 Milvus 하나로 받다 보면, 어느 날부터 p95 지연이 튀고 QPS가 급락하는 순간이 옵니다. 대부분은 “서버 증설”로 덮지만, Milvus는 인덱스 파라미터 조합만 제대로 잡아도 체감 성능이 크게 바뀝니다.
이 글은 HNSW와 IVF 계열 인덱스를 대상으로, 검색속도 5배 개선을 목표로 하는 튜닝 절차를 실전 관점에서 정리합니다. 단순히 파라미터 뜻을 나열하는 것이 아니라, 어떤 지표를 보고 어떤 순서로 바꿔야 하는지, 그리고 정확도(Recall)와 지연(Latency)을 같이 맞추는 방법을 중심으로 설명합니다.
전제: Milvus 2.x 기준, 임베딩 차원
dim=768혹은1536, 코사인/내적 기반 검색을 상정합니다.
1) 먼저 확인할 것: “인덱스 문제”가 맞나
인덱스 튜닝은 비용 대비 효과가 크지만, 병목이 다른 데 있으면 아무리 efSearch를 줄여도 체감이 없습니다. 아래를 먼저 분리하세요.
체크리스트
- 쿼리 패턴:
topK가 10인지 200인지,filter(스칼라 필터)가 붙는지 - 데이터 분포: 컬렉션 크기, 샤드/파티션 사용 여부, 업데이트/삭제 빈도
- 리소스 병목
- CPU가 100%인지, 메모리 스왑이 있는지
- 디스크/네트워크 대기(특히 분산 모드) 여부
- 캐시 히트율: 자주 조회되는 세그먼트가 메모리에 상주하는지
운영 환경이 K8s/EKS라면 네트워크/커널 테이블 이슈가 “검색이 느려졌다”로 나타나기도 합니다. 예를 들어 연결 드롭이 있으면 애플리케이션 레벨에서 재시도하며 지연이 커집니다. 이 경우는 인덱스가 아니라 인프라를 봐야 합니다.
2) Milvus 인덱스 선택: HNSW vs IVF 계열
둘 다 ANN(Approximate Nearest Neighbor)이지만 성격이 다릅니다.
HNSW가 유리한 경우
- 중간 규모(수백만~수천만)에서 낮은 지연이 중요
- 메모리를 더 쓰더라도 일관된 p95가 필요
topK가 상대적으로 작고, 필터가 약하거나 없을 때
IVF 계열이 유리한 경우
- 데이터가 매우 크고(수천만~억 단위) 메모리 효율이 중요
nlist기반으로 coarse quantization을 통해 탐색 범위를 줄이고 싶을 때- IVF+PQ/IVF+SQ8 등으로 메모리/속도/정확도 트레이드오프를 강하게 걸고 싶을 때
정리하면:
- 속도만 최우선이면 HNSW가 튜닝 난이도 대비 성과가 잘 나옵니다.
- 메모리 예산이 빡빡하거나 초대형이면 IVF 계열이 더 현실적입니다.
3) “5배”를 만드는 측정 프레임: Latency + Recall
검색속도만 올리면 정확도가 떨어질 수 있습니다. 따라서 튜닝 목표를 다음처럼 잡는 걸 권장합니다.
- 목표:
p95 latency5배 개선 - 제약: Recall@K가 기준 대비
-1%p이내(혹은 서비스 허용치)
최소 측정 셋업
- 고정된 쿼리 셋
N=1000준비 - 기준 정확도는 Brute force(정확 검색) 또는 충분히 높은 파라미터로 근사
topK는 실제 서비스 값으로
아래는 Python에서 Milvus를 호출하고 지연을 재는 예시입니다.
import time
import numpy as np
from pymilvus import connections, Collection
connections.connect(alias="default", host="localhost", port="19530")
col = Collection("docs")
# 예: 1000개 쿼리 벡터 (dim=768)
queries = np.random.randn(1000, 768).astype("float32").tolist()
def bench(search_params, topk=10):
lat = []
for q in queries:
t0 = time.perf_counter()
_ = col.search(
data=[q],
anns_field="embedding",
param=search_params,
limit=topk,
output_fields=["doc_id"],
)
lat.append((time.perf_counter() - t0) * 1000)
lat = np.array(lat)
return {
"p50_ms": float(np.percentile(lat, 50)),
"p95_ms": float(np.percentile(lat, 95)),
"p99_ms": float(np.percentile(lat, 99)),
}
print(bench({"metric_type": "COSINE", "params": {"ef": 64}}, topk=10))
주의: 위 예시는 단일 클라이언트 루프라 서버 QPS가 낮습니다. 실제로는 동시성(예:
locust,k6,wrk2)을 걸어 p95를 보는 게 더 정확합니다.
4) HNSW 튜닝 핵심: M, efConstruction, efSearch
HNSW는 그래프 기반 인덱스입니다. 실무에서는 아래 3개가 거의 전부입니다.
파라미터 의미
M: 노드당 연결 수. 클수록 정확도와 탐색 품질이 좋아지지만 메모리 증가efConstruction: 인덱스 빌드 품질. 클수록 빌드 시간이 늘지만 검색 Recall이 좋아지는 경향efSearch(Milvus에선 검색 파라미터로ef로 노출되는 경우가 많음): 검색 시 탐색 폭. 클수록 Recall 상승, 지연 증가
실전 튜닝 순서(추천)
- 인덱스는 크게 흔들지 말고
efSearch부터 조정 - 목표 Recall이 안 나오면
M을 올리고, 그 다음efConstruction을 올림 - 빌드 시간이 너무 길면
efConstruction을 약간 내리고efSearch로 보정
추천 시작점(자주 쓰는 조합)
M=16,efConstruction=200,efSearch=64- 더 빠르게:
efSearch=32 - 더 정확하게:
efSearch=128
Milvus 인덱스 생성 예시(HNSW)
아래에서 부등호 문자는 MDX에서 오인될 수 있으므로, 필요한 경우는 인라인 코드로 처리합니다.
from pymilvus import Collection
col = Collection("docs")
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16,
"efConstruction": 200
}
}
col.create_index(field_name="embedding", index_params=index_params)
col.load()
search_params = {"metric_type": "COSINE", "params": {"ef": 64}}
5배 가속이 나오는 전형적인 패턴
- 기존:
ef=256로 과도하게 높게 설정되어 p95가 커짐 - 개선:
ef=64혹은ef=48로 낮추고, Recall 손실이 있으면M을16에서24로 올림
이 조합은 “검색 시 매번 넓게 뒤지는 비용”을 줄이고, “그래프 품질을 약간 올려서” Recall을 지키는 방식이라, 지연을 크게 줄이면서 품질을 유지하기 쉽습니다.
- 함께 읽기: HNSW 튜닝 사고방식은 Qdrant에도 유사합니다. Rust+Qdrant RAG - HNSW 튜닝으로 지연 50%↓
5) IVF 튜닝 핵심: nlist와 nprobe
IVF는 벡터 공간을 nlist개의 클러스터로 나눈 뒤, 검색 시 일부 클러스터만 탐색합니다.
파라미터 의미
nlist: 클러스터 개수. 클수록 각 리스트가 작아져 탐색이 빨라질 수 있지만, 학습/빌드 비용 증가 및nprobe튜닝 민감도 증가nprobe: 검색 시 탐색할 클러스터 수. 클수록 Recall 상승, 지연 증가
실전 튜닝 순서(추천)
- 이미 만들어진 IVF 인덱스가 있다면
nprobe부터 조정 - Recall이 잘 안 나오거나, 특정 분포에서만 성능이 흔들리면
nlist재설계
nlist 경험칙
- 데이터 개수
N일 때nlist는 대략sqrt(N)근처에서 시작- 예:
N=1,000,000이면nlist약1000
- 예:
- 필터가 강하게 걸려 후보가 크게 줄어드는 워크로드는
nlist를 너무 크게 잡으면 오히려 손해일 수 있음
Milvus IVF_FLAT 인덱스 생성 예시
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "COSINE",
"params": {
"nlist": 1024
}
}
col.create_index(field_name="embedding", index_params=index_params)
col.load()
search_params = {"metric_type": "COSINE", "params": {"nprobe": 16}}
IVF_PQ로 메모리까지 줄이는 경우
IVF_PQ는 속도와 메모리를 얻는 대신 Recall이 떨어질 수 있어 “5배 가속”은 쉽게 나오지만 품질 관리가 핵심입니다.
index_params = {
"index_type": "IVF_PQ",
"metric_type": "COSINE",
"params": {
"nlist": 2048,
"m": 16,
"nbits": 8
}
}
col.create_index("embedding", index_params)
PQ를 쓸 때는 m과 nbits 조합을 바꿔가며 Recall@K를 꼭 측정하세요. 특히 dim이 768인데 m을 96처럼 과하게 쪼개면 연산/메모리 패턴이 기대와 달라질 수 있습니다.
6) 튜닝을 망치는 흔한 함정 7가지
1) topK가 큰데 ef나 nprobe를 너무 낮춤
topK=200인데 ef=32면 결과가 불안정해지기 쉽습니다. topK가 커질수록 탐색 폭도 키우는 게 안전합니다.
2) Recall 기준 없이 “빠르다”만 보고 적용
검색은 제품 품질과 직결됩니다. 최소한 Recall@10, Recall@50 정도는 자동 리포트로 남기세요.
3) 필터가 있는 쿼리에 HNSW를 동일하게 적용
스칼라 필터가 강하면 후보군이 줄어들어, 인덱스 탐색 전략이 달라집니다. 이때는 파티션 전략이나 scalar index도 같이 보세요.
4) 세그먼트/로드 상태를 무시
인덱스가 있어도 세그먼트가 메모리에 안 올라와 있으면 디스크 I/O가 끼어 지연이 튑니다. load 정책과 메모리 예산을 같이 설계해야 합니다.
5) 동시성 걸었더니 p95가 급상승
ANN은 CPU 바운드가 되기 쉽고, 스레드 경쟁/NUMA 영향도 큽니다. “단일 쿼리 지연”과 “동시성 지연”을 분리 측정하세요.
6) 빌드 파라미터와 검색 파라미터를 섞어 이해
HNSW는 M, efConstruction은 빌드, ef는 검색입니다. IVF는 nlist는 빌드, nprobe는 검색입니다. 검색 튜닝은 재빌드 없이도 가능한 경우가 많습니다.
7) 재시도 폭주로 지연이 더 커짐
Milvus 호출이 타임아웃 나면 클라이언트가 재시도하며 QPS가 폭증해 더 느려질 수 있습니다. 백오프/큐잉을 넣어야 합니다.
7) “5배”를 목표로 한 권장 레시피
아래는 가장 많이 성공하는 접근입니다.
레시피 A: HNSW로 저지연 최적화
- 기준 측정:
M=16,efConstruction=200,ef=128 - 목표 지연이 안 나오면
ef=64로 내리고 Recall 측정 - Recall 손실이 크면
M=24로 올리고 재빌드 - 다시
ef=48까지 낮춰보며 p95를 좁힘
대부분의 서비스에서 이 과정만으로도 p95가 2배~5배까지 줄어드는 경우가 흔합니다(특히 초기값이 보수적으로 큰 경우).
레시피 B: IVF로 대용량을 안정적으로
nlist를sqrt(N)근처로 잡고 IVF_FLAT으로 먼저 검증nprobe를8부터32까지 스윕하며 Recall/Latency 곡선 확보- 메모리가 부담이면 IVF_PQ를 도입하되, 품질 저하를 수치로 관리
8) 운영 적용 체크리스트
- 파라미터 변경은 A/B로: 컬렉션 복제 또는 별도 인덱스 버전 운영
- 튜닝 결과는 숫자로 남기기:
p50/p95/p99, Recall@K, CPU 사용률 - 장애 대비: 타임아웃, 재시도(지수 백오프), 서킷 브레이커
- 배포 후 관찰: 트래픽 피크 시간대 p95가 유지되는지
마무리
Milvus 성능 문제의 상당수는 “인덱스가 느려서”가 아니라 “인덱스 파라미터가 현재 워크로드에 과하게 보수적이어서” 발생합니다. HNSW라면 ef부터, IVF라면 nprobe부터 줄여 탐색 폭을 관리하고, 필요한 만큼만 M이나 nlist를 조정해 품질을 복원하는 방식이 가장 안전합니다.
다음 단계로는, 여러분의 데이터 크기 N, 차원 dim, 목표 Recall@K, topK, 필터 유무를 기준으로 파라미터 스윕 표를 만들어 드릴 수도 있습니다.