Published on

Pinecone·Milvus ANN 리콜 급락 원인과 HNSW 튜닝

Authors

서빙 중인 RAG나 검색 서비스에서 Pinecone·Milvus 같은 벡터 DB의 ANN 리콜이 “어느 날 갑자기” 급락하는 경우가 있습니다. 보통은 모델이 바뀌었거나 데이터가 늘어서 그런가 보다 하고 넘어가지만, 실제로는 HNSW의 튜닝 파라미터와 운영 상태(세그먼트/컴팩션, 업데이트 패턴, 필터링, 샤딩)가 결합되면서 리콜이 급격히 무너지는 패턴이 자주 나옵니다.

이 글은 다음을 목표로 합니다.

  • 리콜 급락을 “정량 지표”로 재현하고 원인을 분리하는 방법
  • HNSW의 핵심 파라미터 M, efConstruction, efSearch가 리콜에 미치는 영향
  • Pinecone·Milvus에서 특히 자주 밟는 운영 함정(업서트, 삭제, 컴팩션, 필터, 샤드)
  • 튜닝을 안전하게 롤아웃하는 체크리스트

리콜 급락, 먼저 의심해야 할 4가지

리콜이 떨어졌다고 해서 항상 HNSW가 원인인 것은 아닙니다. 하지만 아래 4가지는 HNSW 기반 인덱스에서 리콜을 체감 급락시키는 대표 요인입니다.

1) efSearch가 낮아진 상태로 고정됨

HNSW 검색은 “얼마나 넓게 탐색할지”를 efSearch로 제어합니다. 값이 낮으면 속도는 빨라지지만, 그래프 탐색이 조기에 멈춰 리콜이 떨어집니다.

  • 배치 성능 튜닝 중 efSearch를 낮춘 뒤 되돌리지 않음
  • 트래픽 증가 대응으로 latency 예산을 맞추려다 efSearch를 낮춤
  • 클라이언트 기본값이 바뀌거나, 쿼리 경로가 달라지면서 낮은 값이 적용됨

특징은 “리콜이 갑자기” 떨어지고, p95 latency는 동시에 개선되는 경우가 많다는 점입니다.

2) 인덱스가 부분적으로만 HNSW에 올라가 있음

Milvus에서는 세그먼트가 인덱싱되는 타이밍, 컴팩션 상태, growing segment 존재 여부에 따라 어떤 데이터는 HNSW, 어떤 데이터는 brute force 또는 다른 경로로 검색될 수 있습니다. Pinecone도 내부적으로는 파티션/샤드/세그먼트 유사 개념이 있고, 인덱스 빌드/머지/리밸런싱 중 품질이 흔들릴 수 있습니다.

이때 리콜이 떨어지는 전형적인 패턴은 아래와 같습니다.

  • 최근에 넣은 데이터만 유독 검색이 안 됨
  • 특정 시간대 이후로만 리콜이 떨어짐(인덱스 빌드/컴팩션 타이밍과 연동)
  • 샤드가 늘거나 리밸런싱 이후 리콜이 흔들림

3) 업데이트·삭제가 많아 그래프 품질이 저하됨

HNSW는 정적 데이터에서 특히 강합니다. 하지만 대량 업서트, 잦은 삭제, 동일 ID 재업서트가 많으면 “그래프가 이상해진다”는 표현이 딱 맞는 상황이 생깁니다.

  • 삭제 마커가 쌓여 유효 이웃이 줄어듦
  • 재업서트로 벡터가 크게 이동했는데, 그래프 연결이 충분히 재구성되지 않음
  • 컴팩션/리빌드가 늦어져 품질 저하 상태가 길게 지속됨

4) 필터링이 리콜을 구조적으로 깎음

메타데이터 필터를 강하게 걸면, HNSW 그래프 탐색 중 후보가 필터로 대거 탈락합니다. 그러면 efSearch를 높여도 “필터 통과 후보 자체가 부족”해 리콜이 떨어집니다.

  • 필터 선택도가 높을수록(통과율이 낮을수록) 리콜 악화
  • 특히 “AND 조건이 많은 필터”에서 급격히 체감

이 경우는 파라미터만으로 해결이 안 되고, 파티셔닝 전략이나 인덱스 구조를 함께 봐야 합니다.

HNSW 핵심 파라미터: 리콜이 왜 무너지는가

HNSW를 리콜 관점에서 보면, 결국 아래 3개의 균형입니다.

  • 그래프를 얼마나 촘촘히 만들까: M
  • 그래프를 얼마나 열심히 만들까: efConstruction
  • 검색 때 얼마나 넓게 탐색할까: efSearch

M: 노드당 최대 이웃 수

  • M이 작으면 그래프가 성기고 지름이 커져 탐색이 어려워집니다.
  • M이 크면 메모리 사용량이 늘고 빌드/업데이트 비용이 증가하지만, 리콜이 안정적입니다.

운영에서 흔한 실수는 “비용 절감”으로 M을 낮춰두고, 트래픽이 늘거나 데이터가 커졌을 때 리콜이 갑자기 흔들리는 상황입니다. 데이터 분포가 복잡해질수록(클러스터가 많아질수록) 낮은 M은 더 빨리 한계에 도달합니다.

efConstruction: 인덱스 구축 품질

  • efConstruction이 낮으면 인덱스 구축이 빨라지지만, 그래프가 지역 최적에 갇혀 품질이 떨어집니다.
  • 구축 품질이 나쁘면, efSearch를 올려도 회복이 제한됩니다.

즉, efSearch는 “탐색 예산”이고 efConstruction은 “지도 자체의 품질”입니다. 지도 품질이 나쁘면 탐색 예산을 늘려도 원하는 곳을 못 찾습니다.

efSearch: 검색 시 후보 풀 크기

  • 리콜과 가장 직접적으로 맞닿아 있고, 실시간으로 조정 가능하다는 점에서 운영 레버리지로 가장 큽니다.
  • 다만 필터링이 강하거나, 인덱스 품질 자체가 낮으면 efSearch만 올려도 한계가 있습니다.

실무적으로는 efSearch를 먼저 올려 리콜이 회복되는지 확인하고, 회복이 미미하면 M/efConstruction 또는 인덱스 재빌드를 의심하는 흐름이 효율적입니다.

리콜 급락을 재현하는 측정 harness 만들기

“리콜이 떨어진 것 같다”는 감각을 지표로 바꾸지 않으면 튜닝이 끝나지 않습니다. 최소한 아래를 준비하세요.

  • 쿼리 샘플 Q (예: 500개)
  • 각 쿼리의 정답 top-k를 brute force로 계산한 레퍼런스
  • ANN 결과와 레퍼런스의 recall@k 계산

파이썬으로 간단한 harness 예시는 다음과 같습니다.

import numpy as np

def recall_at_k(ann_ids, gt_ids, k):
    ann = [set(x[:k]) for x in ann_ids]
    gt = [set(x[:k]) for x in gt_ids]
    hit = [len(a & g) / float(k) for a, g in zip(ann, gt)]
    return float(np.mean(hit))

# ann_ids: (num_queries, k)
# gt_ids:  (num_queries, k)
print("recall@10=", recall_at_k(ann_ids, gt_ids, 10))

레퍼런스 brute force는 FAISS의 IndexFlatIP 또는 IndexFlatL2로 쉽게 만들 수 있습니다.

import faiss
import numpy as np

def brute_force_topk(vectors, queries, k, metric="ip"):
    dim = vectors.shape[1]
    if metric == "ip":
        index = faiss.IndexFlatIP(dim)
    else:
        index = faiss.IndexFlatL2(dim)

    index.add(vectors.astype(np.float32))
    scores, ids = index.search(queries.astype(np.float32), k)
    return ids, scores

이 harness를 만들면, Pinecone·Milvus에서 파라미터를 바꿨을 때 리콜이 얼마나 회복되는지, 그리고 latency 비용이 얼마인지 같은 언어로 대화할 수 있습니다.

Pinecone에서 리콜이 떨어질 때 보는 순서

Pinecone은 관리형이라 내부 인덱스 상태를 Milvus만큼 세세히 보긴 어렵습니다. 대신 “증상 기반”으로 접근하는 게 빠릅니다.

1) 쿼리 파라미터에서 top_k와 검색 예산 확인

  • top_k를 크게 올리면 recall@k는 올라갈 수 있지만, 이것은 평가 지표를 바꾸는 것에 가깝습니다.
  • 가능하면 검색 예산(제품이 제공하는 검색 파라미터, 예: ef 류 옵션)이 있는지 확인하고 점진적으로 올려봅니다.

만약 제공 파라미터가 제한적이라면, 다음을 우선 점검합니다.

  • 필터 선택도: 필터 통과율이 낮아졌는지
  • 네임스페이스/파티션 전략: 특정 그룹에 데이터가 몰렸는지
  • 최근 업서트 패턴: 동일 ID 업데이트 폭증 여부

2) 업서트 폭증 이후 리콜 저하라면 “리빌드 또는 재인덱싱” 관점

HNSW는 업데이트가 누적될수록 품질이 흔들릴 수 있습니다. Pinecone에서는 인덱스 재구성 옵션이 제한적이므로, 운영적으로는 아래 전략이 자주 쓰입니다.

  • 새 인덱스를 병렬로 만들고 스위칭(blue/green)
  • 특정 기간 단위로 네임스페이스를 분리해 “변동이 큰 구간”을 격리

이 접근은 DB 관점에서 “테이블이 비대해져 성능이 무너질 때 정리 작업이 필요”한 것과 유사합니다. 관계형 DB에서 bloat를 다루는 접근과 사고방식이 닮아 있으니, 운영 감각을 맞추는 데는 이 글도 도움이 됩니다: PostgreSQL VACUUM 안 먹을 때 - bloat·autovacuum 튜닝

Milvus에서 리콜 급락의 단골 원인: 세그먼트·컴팩션·인덱스 상태

Milvus는 “데이터가 어떻게 세그먼트로 쌓이고, 언제 인덱스가 만들어지며, 컴팩션이 어떻게 진행되는지”가 리콜과 성능에 직접 영향을 줍니다.

1) growing segment가 많으면 검색 경로가 섞인다

  • sealed segment는 인덱스(HNSW 등)를 붙이기 좋습니다.
  • growing segment는 실시간 삽입을 받는 대신, 인덱스가 없거나 제한적일 수 있습니다.

따라서 최근 데이터가 검색이 안 되는 느낌이라면, “최근 데이터가 growing에만 있고 sealed로 넘어가지 않았다”는 가능성을 먼저 봐야 합니다.

2) 컴팩션 지연은 삭제·업데이트 누적을 방치한다

삭제가 많으면 tombstone이 쌓이고, 유효 데이터 대비 오버헤드가 커집니다. 이는 단지 성능 문제가 아니라, 필터링과 결합될 때 리콜 체감도 떨어뜨릴 수 있습니다.

운영적으로는 다음을 체크합니다.

  • 삭제율이 일정 수준을 넘었는지
  • 컴팩션이 따라가고 있는지
  • 세그먼트가 과도하게 쪼개져 있는지

3) 인덱스 파라미터가 데이터 분포에 비해 보수적이다

Milvus에서 HNSW를 쓴다면, 보통 아래 조합이 리콜 안정성에 영향을 줍니다.

  • M이 낮음
  • efConstruction이 낮음
  • 검색 파라미터 ef가 낮음

특히 데이터가 커질수록 “예전엔 됐는데 지금은 안 됨”이 발생합니다. 데이터 규모가 커지면 그래프에서 더 많은 지역 최소가 생기고, 탐색 예산이 부족해지기 때문입니다.

튜닝 절차: efSearch부터, 그 다음 M/efConstruction

실무에서 가장 비용 대비 효과가 좋은 순서는 아래입니다.

1) efSearch 스윕으로 리콜-지연 곡선 얻기

동일한 쿼리 셋으로 efSearch를 여러 값으로 바꿔가며 recall@k와 p95 latency를 같이 기록합니다.

  • 리콜이 빠르게 회복된다: 인덱스 품질은 괜찮고 탐색 예산이 부족했던 것
  • 리콜이 거의 안 오른다: 인덱스 품질 자체가 낮거나, 필터링 구조 문제

예시 형태의 기록 포맷을 남겨두면 팀 내 커뮤니케이션이 쉬워집니다.

efSearch=32  recall@10=0.78  p95=35ms
efSearch=64  recall@10=0.86  p95=49ms
efSearch=128 recall@10=0.92  p95=72ms
efSearch=256 recall@10=0.94  p95=120ms

2) 인덱스 재빌드가 가능하면 MefConstruction을 상향

리콜 급락이 “지속적”이고 efSearch로 회복이 제한적이라면, 인덱스 구축 품질을 올려야 합니다.

  • M을 올리면 그래프 연결성이 좋아져 리콜이 안정화됩니다.
  • efConstruction을 올리면 구축 시간이 늘지만, 같은 efSearch에서도 리콜이 올라갈 수 있습니다.

단, M 상향은 메모리 증가를 동반합니다. 메모리 부족은 또 다른 형태의 품질 저하나 성능 문제로 이어질 수 있으니, 노드 리소스와 함께 계획해야 합니다.

3) 필터가 강하면 “필터 친화적 설계”로 전환

efSearch를 올려도 필터 통과 후보가 부족하면, 리콜은 구조적으로 제한됩니다. 이때는 다음을 고려합니다.

  • 메타데이터 값으로 컬렉션/파티션을 분리해 검색 공간을 먼저 줄이기
  • 자주 쓰는 필터 조합을 기준으로 인덱스를 분리하기
  • 필터 선택도가 높은 조건은 사전 후보군을 별도 테이블로 관리하기

이 과정은 프론트엔드 성능에서 INP가 떨어질 때 “단순 최적화가 아니라 병목 원인을 분해”하는 접근과 유사합니다. 원인 분해 방식이 익숙하지 않다면 다음 글의 분석 흐름도 참고할 만합니다: Chrome INP 급락 원인 찾기 - Long Task·TBT 분석

운영에서 자주 놓치는 체크리스트

메트릭

  • recall@k (오프라인)
  • p50/p95/p99 latency
  • QPS 대비 timeout 비율
  • 필터 통과율(가능하면)
  • 업데이트/삭제 비율

데이터/임베딩

  • 정규화 여부(코사인 유사도 계열이면 L2 normalize가 필요한 경우가 많음)
  • 거리 함수와 임베딩 학습 목표 일치 여부(IP vs L2)
  • 차원 수 변경 여부

인덱스 라이프사이클

  • 대량 업서트 이후 인덱스 품질이 유지되는지
  • 삭제 누적 시 컴팩션/리빌드 전략이 있는지
  • blue/green으로 인덱스 교체가 가능한지

배포 안정성

튜닝은 실험 코드와 운영 코드가 섞이면서 사고가 납니다. 설정을 JSON으로 관리하거나, 파라미터 변경을 스키마로 고정해두면 실수가 크게 줄어듭니다. 툴 호출이나 설정 검증을 엄격히 하고 싶다면 이 글의 접근이 힌트가 됩니다: Claude Tool Use 400 에러, JSON Schema로 해결

결론: 리콜 급락은 대부분 “탐색 예산” 또는 “그래프 품질” 문제다

Pinecone·Milvus에서 ANN 리콜이 급락하면, 감으로 대응하지 말고 다음 순서로 좁히는 것이 가장 빠릅니다.

  1. 오프라인 harness로 recall@k를 재현
  2. efSearch를 올려 리콜이 회복되는지 확인
  3. 회복이 제한적이면 M/efConstruction 상향 및 인덱스 재빌드 검토
  4. 업데이트·삭제·컴팩션·필터 선택도 같은 운영 요인을 함께 점검

HNSW는 튜닝이 단순해 보이지만, 운영 상태와 결합되면 리콜이 “계단식으로” 무너질 수 있습니다. 리콜-지연 곡선을 확보하고, 인덱스 라이프사이클까지 포함해 관리하는 순간부터 품질은 안정화됩니다.