- Published on
Pinecone·Milvus HNSW 리콜 급락 원인 6가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 벡터 검색에서 리콜(recall)이 급락하면 대개 “HNSW가 갑자기 멍청해졌다”로 느껴지지만, 실제로는 인덱스 파라미터/데이터 입력/운영 환경 중 하나가 조용히 바뀐 경우가 많습니다. 특히 Pinecone·Milvus 모두 HNSW(또는 HNSW 계열)를 제공하면서, 빌드 단계(그래프 구성) 와 쿼리 단계(탐색 폭) 가 분리되어 있어 한쪽만 바뀌어도 리콜이 크게 흔들릴 수 있습니다.
이 글은 Pinecone·Milvus에서 HNSW 리콜이 급락할 때 자주 만나는 원인 6가지를 “증상 → 원인 → 확인 방법 → 해결” 순서로 정리합니다. 운영 관점에서의 계측/로그 점검 팁도 함께 다룹니다.
먼저: 리콜 급락을 재현 가능한 형태로 고정하기
리콜 점검이 어려운 이유는 “정답(ground truth)”이 없거나, 쿼리/데이터가 계속 변하기 때문입니다. 급락 원인 분석 전에 아래 3가지를 고정하세요.
- 고정된 평가 세트: 쿼리
N개와 각 쿼리의 정답 top-k를 오프라인에서 계산(브루트포스)해 두기 - 고정된 k: 예를 들어
top_k=10을 고정 - 고정된 거리 함수: cosine/L2/dot 중 하나로 통일
아래는 파이썬으로 “브루트포스 top-k”를 만들어 HNSW 결과와 비교하는 최소 예시입니다.
import numpy as np
def brute_topk(query, vectors, k=10, metric="cosine"):
q = query / (np.linalg.norm(query) + 1e-12)
V = vectors / (np.linalg.norm(vectors, axis=1, keepdims=True) + 1e-12)
if metric == "cosine":
scores = V @ q
idx = np.argsort(-scores)[:k]
return idx.tolist()
elif metric == "l2":
d = np.sum((vectors - query) ** 2, axis=1)
idx = np.argsort(d)[:k]
return idx.tolist()
else:
raise ValueError("unsupported")
def recall_at_k(gt, ann):
gt_set = set(gt)
return len(gt_set.intersection(ann)) / max(1, len(gt_set))
이 “고정된 평가 세트”가 있으면, 아래 6가지 원인을 하나씩 제거해가며 리콜 변화를 확인할 수 있습니다.
원인 1) 쿼리 ef(탐색 폭) 하향 또는 제한
HNSW에서 리콜을 가장 빠르게 떨어뜨리는 스위치는 보통 쿼리 단계의 ef(또는 서비스별 search_ef, ef_search)입니다. ef는 탐색 시 후보를 얼마나 넓게 유지할지 결정하며, 낮아질수록 속도는 빨라지지만 리콜이 급락합니다.
자주 발생하는 시나리오
- 트래픽 증가로 p99 latency가 튀자, 운영자가
ef를 낮추거나 자동 튜닝이 개입 - 멀티테넌트 환경에서 쿼리 비용 제한(쿼리 budget) 때문에 내부적으로
ef상한이 걸림 - SDK 버전 변경으로 기본값이 달라짐
확인 방법
- 동일 쿼리에 대해
ef만32 → 64 → 128 → 256으로 올리며 리콜 곡선 확인 - p95/p99 latency와 함께 리콜을 2축 그래프로 저장
해결
- 목표 리콜을 먼저 정하고, 그 리콜을 만족하는 최소
ef를 찾은 뒤 SLO로 고정 - 트래픽 급증 시에도
ef가 임의로 내려가지 않도록 “서치 프로파일”을 분리
아래는 “리콜-지연 시간” 트레이드오프를 간단히 스윕하는 예시입니다.
import time
def sweep_ef(search_fn, queries, gt_topk, ef_list=(32,64,128,256), k=10):
out = []
for ef in ef_list:
t0 = time.time()
recalls = []
for i, q in enumerate(queries):
ann = search_fn(q, top_k=k, ef=ef)
recalls.append(recall_at_k(gt_topk[i], ann))
dt = time.time() - t0
out.append({"ef": ef, "recall": float(np.mean(recalls)), "sec": dt})
return out
원인 2) 인덱스 빌드 파라미터 M, efConstruction 변경(또는 빌드 품질 저하)
HNSW는 그래프를 어떻게 촘촘하게 만들었는지가 리콜의 상한을 결정합니다. 대표 파라미터는 M(노드당 연결 수)과 efConstruction(빌드 시 탐색 폭)입니다.
M이 낮아지면 그래프가 성기게 연결되어 “갈 수 있는 길”이 줄어듭니다.efConstruction이 낮아지면 그래프 품질이 떨어져 로컬 미니멈에 갇히기 쉽습니다.
자주 발생하는 시나리오
- 인덱스를 재생성(rebuild)하면서 비용 절감 목적으로
M/efConstruction을 낮춤 - 샤드 수 변경, 인덱스 타입 변경, 컬렉션 재생성 과정에서 기본값으로 회귀
- 백그라운드 빌드가 리소스 부족으로 느려지고, 내부적으로 빌드 품질이 저하
확인 방법
- “리콜이 떨어진 시점”에 인덱스가 재생성되었는지(배포/마이그레이션/스케일 이벤트) 확인
- 인덱스 메타데이터에서
M,efConstruction(또는 유사 설정)을 추적
해결
- 인덱스 설정을 코드/인프라로 선언(IaC)하여 기본값 회귀 방지
- 리빌드 전후로 고정 평가 세트로 리콜 A/B를 필수화
원인 3) 거리 함수/정규화 불일치(cosine vs dot vs L2)
HNSW 자체 문제가 아니라, 벡터 전처리(정규화) 또는 metric 설정이 바뀌면 리콜이 “급락한 것처럼” 보입니다. 예를 들어 cosine 유사도를 기대했는데 dot product로 검색하면, 벡터 노름(norm)이 큰 항목이 과도하게 상위로 뜰 수 있습니다.
전형적인 실수
- 임베딩 생성 파이프라인에서 L2 정규화를 빼먹음
- 인덱스 metric은 cosine인데, 쿼리 벡터만 정규화하지 않음(또는 반대)
- Milvus에서 컬렉션 metric을 변경하려고 재생성했는데, 앱 쪽 로직은 그대로
확인 방법
- 샘플 벡터 100개 정도에 대해 노름 분포를 출력해 급변 여부 확인
- 동일 쿼리로 “정규화 on/off” 비교
import numpy as np
def norm_stats(vectors):
norms = np.linalg.norm(vectors, axis=1)
return {
"min": float(np.min(norms)),
"p50": float(np.median(norms)),
"p95": float(np.percentile(norms, 95)),
"max": float(np.max(norms)),
}
# cosine을 쓸 거면 보통 아래처럼 정규화
vectors_unit = vectors / (np.linalg.norm(vectors, axis=1, keepdims=True) + 1e-12)
query_unit = query / (np.linalg.norm(query) + 1e-12)
해결
- “임베딩 생성 → 정규화 → 저장”을 단일 모듈로 묶고, 저장 시점에 노름 검증을 넣기
- 인덱스 metric과 앱의 유사도 정의를 문서화하고 테스트로 고정
원인 4) 필터(메타데이터 조건)로 후보 풀이 급격히 축소
Pinecone·Milvus 모두 메타데이터 필터를 지원합니다. 문제는 필터가 들어가는 순간, HNSW가 탐색할 수 있는 후보 자체가 줄어들어 ef를 크게 올려도 리콜이 회복되지 않는 구간이 생긴다는 점입니다.
자주 발생하는 시나리오
tenant_id,lang,is_active같은 조건이 추가되며 후보가 1퍼센트 이하로 감소- 시간 범위 필터(최근 7일) 추가로 분포가 급변
- 필터 조건이 잘못되어 사실상 “거의 빈 집합”에서 검색
확인 방법
- 쿼리당 “필터 통과 후보 수”를 로깅(가능하면)
- 필터 없이 검색했을 때 리콜이 정상인지 먼저 확인
해결
- 필터가 강할수록
ef를 올리는 것만으로 해결이 안 될 수 있으므로, 다음을 고려- 필터별 인덱스 분리(테넌트별/언어별)
- 하이브리드 전략: 필터 조건을 먼저 만족하는 후보를 별도 구조로 좁힌 뒤 ANN 적용
- “최근 데이터” 전용 컬렉션/파티션을 운영
운영에서 이 문제는 “인증/권한 필터”가 추가되며 갑자기 발생하기도 합니다. 네트워크·인증 레이어에서의 변경이 검색 품질로 전이되는 패턴은 다른 장애에서도 흔합니다. 비슷한 점검 루틴은 Nginx JWT 검증 401? auth_jwt 핵심 정리 같은 글의 방식(조건/정책이 바뀌었는지 역추적)과도 통합니다.
원인 5) 업데이트/삭제가 많은 워크로드에서 그래프 품질이 시간에 따라 열화
HNSW는 “정적 데이터에서 강한” 편이지만, 서비스 구현에 따라 업데이트/삭제가 잦으면 그래프가 점점 비효율적으로 변하거나 tombstone이 누적되어 탐색 품질이 떨어질 수 있습니다.
전형적인 증상
- 처음 인덱스 만들고 며칠은 리콜이 좋다가, 점점 떨어짐
- 삭제 비율이 높을수록 리콜과 지연 시간이 함께 나빠짐
확인 방법
- 삭제/업데이트 비율, compaction(정리) 작업 여부, 세그먼트 수 변화를 시계열로 확인
- “리빌드 직후 vs 리빌드 직전” 리콜 비교
해결
- 일정 주기로 리빌드(또는 compaction)하는 운영 정책 수립
- 업데이트는 가능하면 upsert 대신 버전 필드로 append 후, 주기적 정리(워크로드에 따라)
- Pinecone·Milvus의 권장 운영 패턴(세그먼트/컴팩션/인덱스 빌드 정책)을 따르되, 리콜 KPI를 기준으로 주기를 조정
원인 6) 리소스 병목(메모리/CPU)으로 탐색이 조기 종료되거나 타임아웃
리콜 급락이 “특정 시간대”에만 나타난다면, 알고리즘 파라미터보다 운영 리소스 병목을 의심해야 합니다. ANN 탐색은 CPU와 메모리 접근 패턴에 민감하고, 컨테이너 환경에서는 throttling/NUMA/캐시 미스/메모리 압박이 성능과 품질을 동시에 흔들 수 있습니다.
자주 발생하는 시나리오
- 노드 CPU throttling으로 쿼리 타임아웃 증가 → 부분 결과만 반환 → 리콜 하락
- 메모리 압박으로 OS 페이지 캐시가 깨지고 지연이 증가 → 내부적으로 탐색 budget 감소
- 동시성 급증으로 큐잉이 길어져 timeout에 걸림
확인 방법
- 쿼리별 timeout/partial result 여부를 확인(서비스가 제공한다면)
- p99 latency, CPU throttling, 메모리 사용량을 같은 타임라인에서 비교
- 쿠버네티스 환경이면 Metrics API가 제대로 보이는지부터 점검(계측이 없으면 원인 규명이 불가능)
쿠버네티스에서 지표가 비정상이라면, 먼저 계측을 정상화하세요. 예를 들어 kubectl top이 0으로 찍히는 상황은 리콜 문제를 “감”으로만 보게 만듭니다. 관련 점검은 EKS에서 kubectl top이 0%일 때 Metrics API 점검을 참고할 수 있습니다.
해결
- timeout을 무작정 늘리기보다, 병목을 제거하고
ef/동시성/큐 정책을 조정 - HNSW는 메모리 locality가 중요하므로, 가능하면 인덱스가 메모리에 상주하도록 메모리 예산을 재산정
- 스레드 수/동시성 제한을 두고, 과부하 시에는 품질 저하를 “의도적으로” 제어(예:
ef를 단계적으로 낮추되 리콜 하한을 지키기)
Pinecone·Milvus 공통 체크리스트(실전용)
아래 순서로 보면 원인 좁히기가 빠릅니다.
- 평가 세트 고정: 쿼리/정답 top-k를 고정
- 필터 제거 A/B: 필터 없이 리콜이 정상인지 확인
- metric/정규화 검증: 노름 분포와 metric 설정 일치 확인
- 쿼리 파라미터 스윕:
ef(또는 동등 파라미터) 스윕으로 리콜 곡선 확인 - 인덱스 파라미터/재빌드 이력 확인:
M,efConstruction및 재생성 이벤트 확인 - 리소스/timeout 시계열 확인: p99, throttling, 메모리 압박, partial/timeout 여부 확인
마무리: 리콜 급락은 “알고리즘”보다 “변경”에서 시작된다
HNSW 리콜 급락의 대부분은 파라미터 자체의 문제가 아니라, 운영 중 누군가(혹은 자동화)가 바꾼 설정/워크로드/필터/리소스 상태에서 시작합니다. 따라서 해결의 핵심은 다음 두 가지입니다.
- 리콜을 측정 가능한 계약으로 만들기: 고정 평가 세트 + 리콜 SLO
- 변경을 추적 가능하게 만들기: 인덱스 설정, SDK 버전, 필터 정책, 리소스 정책을 모두 변경 이력으로 남기기
이 두 가지가 잡히면, Pinecone든 Milvus든 리콜 문제는 “감”이 아니라 재현 가능한 엔지니어링 문제로 바뀝니다.