Published on

Milvus HNSW/IVF 파라미터 튜닝으로 리콜 올리기

Authors

서치 품질이 기대보다 낮을 때(특히 topK 결과가 “뭔가 비슷한데 안 맞는” 느낌일 때) 가장 먼저 의심해야 하는 건 임베딩 모델만이 아닙니다. Milvus에서 HNSW/IVF 인덱스 파라미터검색 파라미터가 리콜(Recall)에 직접적인 영향을 주고, 같은 데이터/모델이라도 설정에 따라 체감 품질이 크게 달라집니다.

이 글은 “리콜을 올리고 싶다”는 목표에 맞춰, HNSW와 IVF 계열 인덱스에서 무엇을 어떻게 올려야 하는지, 그리고 그 대가로 무엇이 늘어나는지(지연시간/메모리/빌드 시간)를 실무 관점에서 정리합니다.

또한 튜닝은 결국 실험입니다. 그래서 마지막에는 **실험 설계(ground truth 구성, 메트릭, 단계적 탐색)**까지 포함합니다.

리콜이 떨어지는 대표 원인 5가지

Milvus 파라미터 튜닝을 시작하기 전에, 리콜 저하의 원인을 빠르게 분류하면 시행착오가 줄어듭니다.

  1. 검색 파라미터가 너무 보수적
    • HNSW의 ef가 낮거나, IVF의 nprobe가 낮으면 후보 탐색이 부족해 리콜이 떨어집니다.
  2. 인덱스 빌드 파라미터가 너무 낮음
    • HNSW의 M, efConstruction, IVF의 nlist가 부적절하면 인덱스 자체가 “성글게” 만들어집니다.
  3. 데이터 분포/스케일 문제
    • 코사인 유사도를 쓰는데 벡터가 정규화되지 않았거나, inner product 기반인데 스케일이 들쭉날쭉하면 품질이 흔들립니다.
  4. 필터링(Scalar filter)로 후보가 급감
    • expr 필터가 강하면 벡터 후보 풀이 줄어 ANN 근사가 더 불리해집니다. 이때는 파라미터를 더 공격적으로 올려야 합니다.
  5. 세그먼트/메모리/캐시 상태로 인한 변동
    • 워밍업이 안 됐거나, 메모리 압박으로 디스크/캐시 미스가 늘면 같은 설정에서도 품질/지연시간이 출렁일 수 있습니다.

지연시간이 갑자기 늘거나 타임아웃이 생기면 애플리케이션 레벨에서도 관측/진단이 필요합니다. 분산 환경에서 호출 타임아웃이 튜닝을 방해할 정도라면 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 추천 튜닝 흐름

  1. 기준선: M=16, efConstruction=200, ef=64 정도에서 시작
  2. 목표 리콜까지 ef를 단계적으로 증가: 64 -> 128 -> 256 -> 512
  3. 지연시간이 너무 커지면 M을 소폭 증가: 16 -> 24 -> 32
  4. 재빌드 가능하면 efConstruction 상향: 200 -> 400 -> 800

IVF 튜닝: 리콜을 올리는 2개의 레버

IVF는 “얼마나 잘 나누고(nlist), 얼마나 많이 뒤지느냐(nprobe)”로 이해하면 쉽습니다.

1) 검색 파라미터 nprobe를 올려라 (가장 즉효)

  • 의미: 검색 시 탐색할 클러스터 개수
  • 효과: nprobe 증가 => 리콜 증가
  • 비용: 지연시간 증가(거의 선형에 가까움), CPU 증가

리콜이 낮을 때 가장 빠른 처방은 nprobe를 올리는 것입니다.

2) 인덱스 파라미터 nlist를 조정하라 (클러스터 수)

  • 의미: 클러스터(centroid) 개수
  • 효과: 적절한 nlist는 같은 nprobe에서도 더 좋은 후보를 모음
  • 비용: 빌드 시간/메모리 증가(centroid 및 메타데이터)

nlist가 너무 작으면 클러스터가 뭉개져 근사 오차가 커지고, 너무 크면 각 클러스터가 너무 작아져 nprobe를 많이 올려야 하는 상황이 생길 수 있습니다.

IVF 추천 튜닝 흐름

  1. 기준선 nlist 설정 후 nprobe를 올려 목표 리콜 달성
  2. nprobe가 너무 커져 지연시간이 부담되면 nlist를 재조정
  3. (압축 인덱스 사용 시) 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@50
  • p95 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 중 어떤 축으로 튜닝을 시작하는 게 유리한지와 추천 초기값(탐색 범위)까지 더 구체적으로 제안할 수 있습니다.