Published on

Milvus IVF_FLAT/HNSW 인덱스 튜닝 - recall↑ 비용↓

Authors

서버 비용은 그대로인데 검색 품질(리콜)이 떨어지거나, 리콜을 올리려고 nprobe/ef를 올렸더니 p95 지연시간과 CPU가 폭증하는 상황은 Milvus 운영에서 흔합니다. 특히 IVF 계열은 nlist(클러스터 수)와 nprobe(탐색할 클러스터 수)의 균형이, HNSW는 M/efConstruction/ef의 균형이 핵심입니다.

이 글은 IVF_FLAT과 HNSW를 동시에 운영하거나 마이그레이션하는 팀을 기준으로, “리콜을 올리되 비용을 낮추는” 방향으로 튜닝을 진행하는 방법을 정리합니다.

관련해서 ANN 리콜 급락의 전형적인 원인과 HNSW 튜닝만 더 깊게 보고 싶다면 아래 글도 같이 참고하세요.

1) 먼저 정리: 비용·품질을 결정하는 3가지 축

Milvus 벡터 검색 튜닝은 결국 아래 3축의 트레이드오프입니다.

  1. Recall(품질): 정답(또는 기준 검색 결과)을 얼마나 잘 맞추는가
  2. Latency(지연시간): p50/p95/p99
  3. Cost(비용): CPU 사용률, 메모리 상주량, 디스크/네트워크 I/O

여기서 실무적으로 중요한 포인트는 “비용”이 단일 값이 아니라는 것입니다.

  • IVF_FLAT은 주로 CPU 스캔량(거리 계산 횟수) 이 비용을 지배합니다.
  • HNSW는 메모리(그래프 엣지)탐색 단계 수(ef) 가 비용을 지배합니다.

즉 같은 리콜 향상이라도, IVF에서는 nprobe를 올리면 CPU가 바로 오르고, HNSW에서는 ef를 올리면 CPU가 오르지만 메모리 구조(M) 를 잘 잡아두면 ef를 과도하게 올리지 않아도 리콜이 나옵니다.

2) IVF_FLAT 핵심 파라미터: nlistnprobe

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가지(리콜↓ 비용↑)

  1. 리콜 기준 없이 p95만 보고 튜닝: 결과적으로 “빠르지만 틀린 검색”이 됨
  2. IVF에서 nlist를 과도하게 키우고 nprobe도 크게 유지: 빌드 비용↑, 런타임 비용↑
  3. HNSW에서 M을 너무 낮게 잡고 ef만 계속 올림: 리콜이 잘 안 오르고 비용만 증가
  4. efConstruction을 너무 낮게 잡아 그래프 품질이 깨짐: 런타임으로 해결 불가한 구간 발생
  5. 워밍업 없이 벤치마크: 캐시/세그먼트 로딩 영향으로 결과가 왜곡
  6. 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를 줄이기

이 과정을 자동화(벤치마크 스크립트 + 리포트)해두면, 임베딩 모델이 바뀌거나 데이터 분포가 바뀌어도 같은 절차로 빠르게 재튜닝할 수 있어 운영 비용을 크게 줄일 수 있습니다.