Published on

Milvus 인덱스 성능 튜닝 - HNSW vs IVF_PQ

Authors

벡터 검색(ANN, Approximate Nearest Neighbor)에서 성능 튜닝은 결국 정확도(recall)·지연시간(latency)·비용(메모리/디스크/빌드 시간) 사이의 균형을 잡는 일입니다. Milvus는 여러 인덱스 타입을 제공하지만, 실무에서 가장 자주 비교되는 조합이 HNSWIVF_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는 두 단계로 생각하면 쉽습니다.

  1. IVF: 전체 벡터를 nlist개의 클러스터로 나누고, 쿼리는 일부 클러스터(nprobe)만 뒤짐
  2. 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 또는 32
  • efConstruction: 200 전후
  • 검색 ef: 64부터 시작해 recall이 부족하면 단계적으로 증가

튜닝 순서

  1. 빌드 품질 확보: MefConstruction을 먼저 적정 수준으로
  2. 서치 품질 조절: 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이 흔한 기본값

튜닝 순서

  1. nlist를 먼저 합리적으로 설정(너무 작으면 후보군이 비대해지고, 너무 크면 관리 비용 증가)
  2. recall이 부족하면 nprobe를 올려 회복
  3. 그래도 부족하면 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 각각에 대해 efnprobe를 스윕하는 간단한 실험표를 만들고, 비용까지 함께 기록해 “정답 파라미터”를 문서화하는 것을 권합니다.