- Published on
Pinecone·Milvus 검색품질 튜닝 - HNSW 파라미터
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 벡터DB를 써도 검색 품질 이슈는 비슷한 패턴으로 반복됩니다. “정확도가 낮다”는 말은 대개 리콜이 떨어진다는 뜻이고, 리콜을 올리면 지연시간과 비용이 늘어납니다. Pinecone와 Milvus는 내부 구현과 노출 파라미터가 조금씩 다르지만, HNSW(Hierarchical Navigable Small World) 기반 인덱스의 핵심 트레이드오프는 동일합니다.
이 글은 HNSW 파라미터를 검색 품질(리콜)·지연시간·메모리 관점에서 해석하고, Pinecone·Milvus에서 실제로 튜닝할 때의 절차와 체크리스트를 제공합니다.
운영에서 튜닝은 결국 “CPU/메모리/동시성”과도 싸우게 됩니다. 클러스터 리소스가 부족하면 튜닝 이전에 병목이 먼저 터집니다. 쿠버네티스 환경이라면 리소스 이슈 트러블슈팅도 함께 보세요: EKS Pod Pending(Insufficient cpu) 원인과 해결
HNSW를 리콜과 지연시간의 관점으로 이해하기
HNSW는 그래프 기반 근사 최근접 탐색(ANN)입니다. 데이터 포인트를 노드로 두고, 노드 간에 “가까운 이웃”을 엣지로 연결합니다. 검색은 완전탐색이 아니라 그래프를 타고 이동하면서 유망한 후보를 확장합니다.
여기서 품질과 성능을 좌우하는 핵심은 두 가지입니다.
- 그래프가 얼마나 촘촘하게 연결되었는가: 촘촘할수록 최단 경로로 정답 근처에 도달할 확률이 올라가 리콜이 좋아지지만, 메모리와 빌드 비용이 증가합니다.
- 검색 시 후보를 얼마나 많이 확장하는가: 많이 확장할수록 리콜이 좋아지지만, 쿼리 시간이 증가합니다.
이 두 축이 각각 HNSW의 대표 파라미터인 M, efConstruction, efSearch로 매핑됩니다.
HNSW 핵심 파라미터 3종: M, efConstruction, efSearch
1) M: 노드당 최대 연결 수(대략적인 밀도)
- 의미: 각 노드가 유지하는 이웃(엣지) 수의 상한. 그래프의 “촘촘함”을 결정합니다.
- 효과
M증가: 리콜 상승, 쿼리 안정성 상승(특히 분포가 복잡할 때), 메모리 사용량 증가, 빌드 시간 증가M감소: 메모리 절약, 빌드 빨라짐, 대신 리콜과 안정성 하락
- 체감 포인트
- 데이터가 클수록, 클러스터링이 강할수록(유사한 벡터가 뭉쳐 있을수록)
M이 너무 작으면 “다른 클러스터로 넘어가는 다리”가 부족해 리콜이 급격히 떨어질 수 있습니다.
- 데이터가 클수록, 클러스터링이 강할수록(유사한 벡터가 뭉쳐 있을수록)
일반적으로 M은 864 범위에서 시작하며, 텍스트 임베딩(예: 768차원)에서도 1632가 흔한 출발점입니다. 다만 “차원이 높다” 자체보다 데이터 분포와 목표 리콜이 더 중요합니다.
2) efConstruction: 인덱스 빌드 시 탐색 폭(구축 품질)
- 의미: 인덱스를 만들 때 각 노드를 삽입하면서 “이웃을 찾기 위해” 얼마만큼 후보를 탐색할지 결정합니다.
- 효과
efConstruction증가: 더 좋은 이웃을 찾고 더 좋은 그래프가 만들어짐, 리콜 상승(특히 동일efSearch에서), 빌드 시간 증가, 메모리 약간 증가efConstruction감소: 빌드 빨라짐, 대신 그래프 품질 저하로 리콜 손실
- 운영 팁
efConstruction은 “한 번 올려두면” 검색 쿼리 비용에는 직접 영향을 거의 주지 않습니다. 즉, 인덱스 재구축이 가능한 워크로드라면efConstruction을 상대적으로 공격적으로 가져가는 것이 유리한 경우가 많습니다.
3) efSearch: 검색 시 탐색 폭(쿼리 품질)
- 의미: 쿼리 시 후보 리스트(우선순위 큐)를 얼마나 크게 유지하며 확장할지 결정합니다.
- 효과
efSearch증가: 리콜 상승, 지연시간 증가, CPU 사용량 증가efSearch감소: 빠르지만 리콜 하락
- 실전 포인트
- 리콜이 낮을 때 가장 먼저 만지는 레버가
efSearch입니다. 재색인 없이도 즉시 효과를 볼 수 있기 때문입니다. - 다만
efSearch를 무작정 올리면 p95/p99 지연시간이 급격히 나빠질 수 있어, 동시성(동시 쿼리 수)과 함께 관찰해야 합니다.
- 리콜이 낮을 때 가장 먼저 만지는 레버가
Pinecone와 Milvus에서 파라미터가 노출되는 방식
두 제품 모두 내부적으로 HNSW를 사용하거나 선택할 수 있지만, 콘솔/SDK에서 파라미터를 노출하는 명칭과 범위가 다를 수 있습니다.
- Milvus
- 인덱스 생성 시
M,efConstruction을 지정 - 검색 시
ef(대개efSearch에 해당)를 지정
- 인덱스 생성 시
- Pinecone
- 인덱스 타입/Pod/Serverless 구성에 따라 노출 파라미터가 다를 수 있습니다.
- 어떤 구성에서는 HNSW 세부 파라미터를 직접 노출하지 않고, 대신 성능-품질을 간접적으로 조절(리소스 스케일, replicas, throughput)하는 형태로 제공될 수 있습니다.
따라서 글의 핵심은 “특정 제품 UI에서 어디를 누르냐”가 아니라, HNSW 파라미터가 의미하는 바를 이해하고 실험 설계를 하는 것입니다.
튜닝 목표를 숫자로 정하기: Recall@k와 Latency(p95)
튜닝은 감으로 하면 끝이 없습니다. 최소한 아래 두 지표를 고정해야 합니다.
- 품질:
Recall@k또는HitRate@k- 예:
Recall@10 >= 0.95
- 예:
- 성능:
p95 latency또는p99 latency- 예:
p95 <= 80ms(워크로드와 SLO에 맞게)
- 예:
정확한 리콜 계산을 위해서는 “정답”이 필요합니다. 보통은 샘플 쿼리에 대해 brute-force(완전탐색)로 Top-k를 구해 정답 세트를 만들고, ANN 결과와 비교합니다.
실전 튜닝 절차(재현 가능한 방식)
1) 데이터/쿼리 샘플을 고정한다
- 벡터 10만~100만 규모에서도 샘플링이 가능합니다.
- 쿼리는 최소 수백~수천 개를 확보합니다.
- 분포가 다른 쿼리(짧은 질의, 긴 질의, 특정 도메인 등)를 섞습니다.
2) 기준선(Baseline)을 만든다
- 초기값 예시
M = 16efConstruction = 200efSearch = 64
이후 변경은 한 번에 한 축씩 합니다.
3) 먼저 efSearch로 리콜 목표를 맞춘다
efSearch를 32, 64, 96, 128… 순으로 올리며Recall@k와 p95를 같이 봅니다.- 목표 리콜을 만족하는 최소
efSearch를 찾습니다.
4) p95가 너무 높으면 M과 efConstruction을 재조정한다
efSearch를 낮추고도 리콜을 유지하려면 그래프 자체 품질이 좋아야 합니다.- 이때
efConstruction을 올려 그래프 품질을 개선하거나,M을 올려 연결성을 높입니다.
권장 우선순위는 보통 다음과 같습니다.
- 재색인이 쉬우면
efConstruction을 먼저 올린다 - 그래도 부족하면
M을 올린다 - 마지막으로
efSearch를 올려 마무리한다
Milvus 예시: HNSW 인덱스 생성과 검색 파라미터
아래 예시는 Milvus에서 자주 쓰는 형태를 단순화한 것입니다. 버전에 따라 API는 조금 다를 수 있지만, 핵심은 M, efConstruction, 검색 시 ef를 분리해서 다룬다는 점입니다.
# 예시: pymilvus 스타일 (개념 예시)
index_params = {
"index_type": "HNSW",
"metric_type": "COSINE",
"params": {
"M": 16,
"efConstruction": 200
}
}
collection.create_index(
field_name="embedding",
index_params=index_params
)
# 검색 시 ef(=efSearch)
search_params = {
"metric_type": "COSINE",
"params": {"ef": 64}
}
results = collection.search(
data=[query_vector],
anns_field="embedding",
param=search_params,
limit=10
)
튜닝 시에는 limit(k)도 고정해야 비교가 됩니다. k가 커질수록 더 높은 efSearch가 필요해지는 경향이 있습니다.
Pinecone에서의 접근: “노출 파라미터”가 없을 때 튜닝하는 법
Pinecone 구성에 따라 HNSW 파라미터를 직접 지정하기 어려운 경우가 있습니다. 이때도 튜닝은 가능합니다. 방법은 두 가지입니다.
- 쿼리 레벨 파라미터가 있다면(예: 탐색 폭, 정확도 관련 옵션) 그 값을 스윕하며
Recall@k와 지연시간을 측정 - 쿼리 레벨 파라미터가 제한적이면
- 인덱스/리소스 스케일(복제본, 파티션, compute tier)을 바꾸며 p95를 맞추고
- 애플리케이션 레벨에서 rerank(재정렬)로 품질을 보강
특히 Pinecone에서는 “ANN 1차 검색 + 소량 후보 rerank” 패턴이 비용 대비 품질이 좋은 경우가 많습니다.
자주 발생하는 튜닝 실패 패턴 7가지
1) 리콜이 낮은데 M만 올리고 efSearch는 그대로 둔다
그래프가 좋아져도 검색이 후보를 충분히 확장하지 않으면 리콜이 기대만큼 오르지 않습니다. 먼저 efSearch 스윕으로 상한을 확인하세요.
2) efSearch를 너무 크게 올려 p99가 폭발한다
평균 latency는 괜찮아 보이는데 tail latency가 터지는 경우가 많습니다. 동시성 구간에서 CPU가 포화되면 큐잉이 생깁니다. 이때는 efSearch를 낮추고 efConstruction 또는 M으로 그래프 품질을 보강하는 쪽이 낫습니다.
3) metric_type 불일치(COSINE vs IP vs L2)
임베딩 모델이 코사인 유사도를 전제로 학습되었는데 L2로 검색하면 품질이 흔들릴 수 있습니다. Pinecone·Milvus 모두 metric 설정을 재확인하세요.
4) 벡터 정규화 누락
코사인 유사도를 쓰면서 벡터 정규화를 하지 않으면, 사실상 내적(IP)과 뒤섞인 동작이 됩니다. 파이프라인에서 정규화를 일관되게 적용하세요.
5) 필터링(메타데이터 조건)과 ANN의 상호작용을 무시한다
강한 필터가 걸리면 후보 풀이 급격히 줄고, ANN이 “가까운 후보를 찾기 어려운” 상황이 됩니다. 이때는 efSearch를 올려도 효과가 제한적일 수 있고, 파티셔닝 전략이나 hybrid 검색(키워드+벡터)을 고려해야 합니다.
6) 인덱스 빌드/컴팩션 중 성능 측정
Milvus는 세그먼트/컴팩션 상태에 따라 성능이 달라질 수 있습니다. Pinecone도 백그라운드 작업의 영향을 받을 수 있습니다. 측정 구간의 시스템 상태를 고정하세요.
7) 애플리케이션에서 재시도 폭주로 더 느려진다
ANN이 느려진 순간 타임아웃-재시도가 폭주하면 더 느려집니다. 서킷 브레이커, 지수 백오프, 동시성 제한을 함께 설계하세요. 분산 시스템 관점의 중복 처리/보상 패턴은 MSA 사가(Saga) 중복처리·보상트랜잭션 설계 실전도 참고할 만합니다.
간단한 리콜 측정 코드(브루트포스 대비)
아래는 NumPy로 “정답 Top-k”를 만든 뒤, ANN 결과의 리콜을 계산하는 최소 예시입니다. 실제로는 쿼리 수천 개에 대해 평균을 내야 합니다.
import numpy as np
def l2_normalize(x: np.ndarray, eps: float = 1e-12) -> np.ndarray:
n = np.linalg.norm(x, axis=1, keepdims=True)
return x / np.maximum(n, eps)
def topk_bruteforce_cosine(base_vectors, query_vector, k: int):
# base_vectors: (N, D), query_vector: (D,)
scores = base_vectors @ query_vector
idx = np.argpartition(-scores, k)[:k]
idx = idx[np.argsort(-scores[idx])]
return idx
def recall_at_k(true_idx, ann_idx):
true_set = set(true_idx)
hit = sum(1 for i in ann_idx if i in true_set)
return hit / max(len(true_set), 1)
# 예시 데이터
N, D = 10000, 768
base = np.random.randn(N, D).astype(np.float32)
q = np.random.randn(D).astype(np.float32)
base = l2_normalize(base)
q = q / max(np.linalg.norm(q), 1e-12)
k = 10
true_idx = topk_bruteforce_cosine(base, q, k)
# ann_idx는 Pinecone/Milvus 검색 결과의 id 목록이라고 가정
ann_idx = true_idx[:7].tolist() + [999999, 888888, 777777] # 예시
print("Recall@10:", recall_at_k(true_idx.tolist(), ann_idx))
이 계산을 기반으로 efSearch를 스윕하면 “리콜-지연시간 곡선”을 얻을 수 있고, 그 곡선이 곧 튜닝의 지도입니다.
권장 시작점과 의사결정 표
운영에서 자주 쓰는 경험적 가이드를 정리하면 다음과 같습니다.
- 리콜이 부족하다
- 1순위:
efSearch증가 - 2순위: (재색인 가능 시)
efConstruction증가 - 3순위:
M증가
- 1순위:
- 지연시간이 높다
- 1순위:
efSearch감소 - 2순위:
M/efConstruction을 올려 같은 리콜을 더 낮은efSearch로 달성 - 3순위: 시스템 스케일(노드 수, replica, CPU)
- 1순위:
- 메모리가 부족하다
- 1순위:
M감소(가장 직접적) - 2순위: 데이터 샤딩/파티셔닝, 차원 축소, 더 작은 임베딩 모델 고려
- 1순위:
마무리: HNSW 튜닝은 “한 번의 정답”이 아니라 “곡선”이다
HNSW 파라미터 튜닝은 특정 숫자를 외워서 끝나는 작업이 아니라, 서비스의 SLO와 데이터 분포에 맞춰 리콜-지연시간 곡선을 원하는 지점으로 옮기는 작업입니다.
- 빠르게 품질을 올리고 싶으면
efSearch - 장기적으로 비용을 낮추고 싶으면
efConstruction과M을 통해 “그래프 품질”을 개선 - 그리고 반드시
Recall@k와 p95/p99를 함께 본다
이 원칙만 지키면 Pinecone든 Milvus든, HNSW 기반 검색 품질 튜닝을 재현 가능하게 진행할 수 있습니다.