왜 ef_search가 문제가 되는가
HNSW 기반 벡터 검색에서 색인은 한 번 만들면 끝이지만, 질의 품질과 지연은 매 요청마다 ef_search(질의 시 탐색 후보 큐 크기)에 좌우된다. 이 값을 크게 잡으면 그래프를 더 넓게 탐색해 재현율(recall)이 올라가지만 지연과 CPU 사용량이 함께 커진다. 반대로 작게 잡으면 빠르지만 상위 K개 결과가 실제 최근접 이웃과 어긋난다.
실무에서 자주 겪는 실패는 두 가지다. 색인 시점의 ef_construction만 튜닝하고 질의 파라미터는 기본값으로 방치하거나, 반대로 재현율이 낮다는 이유로 ef_search를 무작정 올려 p99 지연이 SLO를 넘기는 경우다. 둘 다 파레토 관점 없이 한쪽 축만 본 결과다.
파레토 프론티어로 보기
목표는 "최적값 하나"가 아니라 재현율-지연의 파레토 프론티어를 그리는 것이다. 같은 재현율을 더 낮은 지연으로 얻을 수 있으면 그 설정이 우월하다. ef_search를 스윕하며 각 지점의 recall@K와 p95/p99를 측정하면, SLO(예: p99 < 20ms) 선을 그은 뒤 그 아래에서 재현율이 가장 높은 지점을 고르면 된다.
| ef_search | recall@10 | p95(ms) | p99(ms) |
|---|---|---|---|
| 40 | 0.91 | 3.2 | 5.1 |
| 80 | 0.96 | 5.8 | 9.4 |
| 128 | 0.985 | 8.9 | 15.2 |
| 256 | 0.994 | 16.1 | 28.7 |
위 예시에서 p99 < 20ms가 SLO라면 128이 프론티어상의 선택이다. 256은 재현율 0.9%p를 위해 지연을 두 배 가까이 지불한다.
측정 하네스 만들기
정답셋(ground truth)은 brute-force로 미리 구해 두고, 동일 질의 집합을 각 ef_search로 반복 측정한다. 재현율은 상위 K 집합의 교집합 비율로 계산한다.
import time, numpy as np
def recall_at_k(approx, truth, k):
hits = sum(len(set(a[:k]) & set(t[:k])) for a, t in zip(approx, truth))
return hits / (k * len(truth))
def sweep(index, queries, truth, ef_list, k=10):
for ef in ef_list:
index.set_ef(ef) # 라이브러리별 API 차이 있음
lat = []
approx = []
for q in queries:
t0 = time.perf_counter()
ids, _ = index.knn_query(q, k=k)
lat.append((time.perf_counter() - t0) * 1000)
approx.append(ids)
p99 = np.percentile(lat, 99)
print(f"ef={ef:4d} recall={recall_at_k(approx, truth, k):.3f} "
f"p50={np.percentile(lat,50):.2f} p99={p99:.2f}ms")
주의점: 측정 전 워밍업 질의를 수십 회 돌려 캐시/JIT 편차를 줄이고, 부하 조건(동시성)을 프로덕션과 맞춰야 한다. 단일 스레드 지연과 QPS 부하 하의 지연은 크게 다르다.
엔진별 설정 위치
ef_search는 색인이 아니라 질의 시점에 조정 가능한 값이라는 점이 핵심이다. 대부분 엔진이 질의당 오버라이드를 지원한다.
# Elasticsearch / OpenSearch (kNN) — num_candidates가 ef 역할
POST my-index/_search
{
"knn": {
"field": "embedding",
"query_vector": [/* ... */],
"k": 10,
"num_candidates": 128
}
}
# Qdrant — 검색 파라미터로 질의당 지정
# search_params: { "hnsw_ef": 128 }
# pgvector — 세션 단위로 설정
# SET hnsw.ef_search = 128;
질의당 오버라이드가 되면 트래픽 유형별로 값을 다르게 줄 수 있다. 예: 사용자 실시간 검색은 낮게, 배치 재랭킹 후보 생성은 높게.
ef_construction과의 관계
ef_search로 도달할 수 있는 재현율 상한은 색인 품질이 결정한다. ef_construction과 M이 낮아 그래프 연결이 빈약하면, ef_search를 아무리 올려도 특정 재현율에서 정체된다. 스윕 곡선이 조기에 평탄해지면 질의 튜닝이 아니라 재색인 신호다. 반대로 색인이 충분히 좋은데 재현율이 낮다면 그건 순수하게 ef_search로 회수할 수 있는 손실이다.
운영 시 주의점
- 데이터 분포가 바뀌면 프론티어도 이동한다. 임베딩 모델 교체나 대량 삽입 후에는 스윕을 다시 돌린다.
- 필터링(메타데이터 조건)이 붙으면 유효 후보가 줄어 같은
ef_search라도 재현율이 떨어질 수 있다. 필터 있는 질의는 별도로 측정한다. - SLO는 평균이 아니라 p99로 잡는다. 평균 지연은 낮아도 꼬리 지연이 사용자 경험을 결정한다.
- 재현율 목표는 서비스 요구에서 역산한다. 추천 후보 생성이라면 0.95로 충분하지만, 규정 준수 검색이라면 0.99+가 필요할 수 있다.
결국 ef_search 튜닝은 단일 값 찾기가 아니라, 측정 하네스로 프론티어를 그리고 SLO 제약 아래에서 트래픽별 지점을 선택하는 반복 작업이다.