본문 바로가기
AI2026년 8월 24일7분 읽기

HNSW 인덱스 튜닝 — ef_search·M으로 벡터 검색 속도-정확도 잡기

YS
김영삼
조회 14
HNSW 인덱스 튜닝 — ef_search·M으로 벡터 검색 속도-정확도 잡기

HNSW(Hierarchical Navigable Small World)는 벡터 검색에서 가장 널리 쓰이는 근사 최근접 이웃(ANN) 인덱스다. 전체를 다 뒤지지 않고 그래프를 따라 몇 다리만 건너 "충분히 가까운" 벡터를 빠르게 찾는다. 핵심 파라미터는 M, ef_construction, ef_search 셋이고, 이 숫자들이 검색 속도와 정확도(재현율)의 균형을 결정한다.

RAG를 붙이면 다들 임베딩 모델만 신경 쓰는데, 정작 검색이 느리거나 엉뚱한 걸 물어오는 원인이 인덱스 파라미터일 때가 많다. 나도 pgvector에 기본값으로 인덱스를 만들어 두고 "왜 관련 문서를 자꾸 놓치지?" 하며 임베딩만 바꿔보다가 며칠을 날렸다. 범인은 ef_search였다.

HNSW가 검색하는 방식

HNSW는 벡터들을 여러 층의 그래프로 쌓는다. 위층은 노드가 듬성듬성해 멀리 점프하고, 아래층으로 갈수록 촘촘해 정밀하게 좁힌다. 검색은 맨 위에서 시작해 목표에 가까워지는 이웃으로 계속 이동하다 아래층으로 내려가며 후보를 좁힌다. 전수 비교(brute force)가 O(N)인 데 비해 로그에 가깝게 준다. 대신 "근사"라 100% 정확하진 않다 — 진짜 최근접을 가끔 놓친다. 그 놓치는 정도를 파라미터로 조절한다.

세 파라미터의 역할

파라미터언제 작용올리면비용
M인덱스 생성그래프 연결↑ 재현율↑메모리↑ 빌드 느림
ef_construction인덱스 생성그래프 품질↑빌드 시간↑
ef_search질의 시점재현율↑질의 지연↑

중요한 구분: Mef_construction인덱스를 만들 때 한 번 정해지고 나중에 바꾸려면 재구축해야 한다. 반면 ef_search질의할 때마다 바꿀 수 있다. 그래서 튜닝의 1순위는 언제나 ef_search다. 재구축 없이 속도-정확도 다이얼을 돌릴 수 있으니까.

pgvector에서 실제로 돌려보기

-- 인덱스 생성: M, ef_construction 지정
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 200);

-- 질의 시점에 세션 단위로 ef_search 조절
SET hnsw.ef_search = 100;   -- 기본값(40)보다 높여 재현율 확보
SELECT id, content
FROM docs
ORDER BY embedding <=> :query_vec   -- 코사인 거리
LIMIT 10;

내가 놓쳤던 게 바로 저 SET hnsw.ef_search다. 기본값 40으로는 후보 탐색 폭이 좁아 재현율이 낮았다. 100~200으로 올렸더니 놓치던 문서를 잡기 시작했다. 물론 질의는 조금 느려진다. 공짜는 없다.

재현율을 실제로 측정하는 법

감으로 튜닝하면 안 된다. 정답 집합을 만들어 재현율을 숫자로 재야 한다. 방법은 전수 검색으로 진짜 top-k를 구해 놓고, HNSW 결과가 그중 몇 개를 맞혔는지 보는 것이다.

def recall_at_k(hnsw_results, exact_results, k):
    hit = len(set(hnsw_results[:k]) & set(exact_results[:k]))
    return hit / k

# ef_search를 바꿔가며 recall@10과 지연을 함께 기록
for ef in [40, 80, 120, 200]:
    set_ef_search(ef)
    r, ms = eval_queries(sample_queries)  # 평균 recall, 평균 ms
    print(ef, round(r, 3), round(ms, 1))

이렇게 표를 뽑으면 "recall@10이 0.98을 넘기는 최소 ef_search는 얼마"인지가 보인다. 그 지점이 스위트 스폿이다. 무작정 크게 잡으면 지연만 늘고 재현율은 이미 포화라 이득이 없다.

ANN 튜닝의 본질은 "얼마나 놓쳐도 괜찮은가"를 정하는 일이다. 100% 재현율이 필요하면 애초에 근사 인덱스가 아니라 전수 검색을 써야 한다. HNSW는 약간의 누락을 대가로 속도를 사는 거래다.

현장에서 자주 밟는 지뢰

  • 기본 ef_search로 방치: 재현율이 낮은 가장 흔한 원인. 먼저 이것부터 올려봐라.
  • 필터링과의 충돌: WHERE 조건으로 대부분을 걸러내면 HNSW가 후보를 다 쳐내 결과가 텅 빈다. 사전 필터가 강하면 ef_search를 크게 잡거나 다른 전략이 필요하다.
  • 대량 삭제 후 방치: 삭제·갱신이 쌓이면 그래프가 열화한다. 주기적 재인덱싱을 고려.
  • 메모리 착각: M을 키우면 재현율은 오르지만 인덱스가 메모리에 다 올라가야 빠르다. RAM을 넘기면 오히려 느려진다.

정리하면 순서는 이렇다. 먼저 재현율 측정 하니스를 만들고, ef_search를 올려가며 목표 재현율을 만족하는 최소값을 찾는다. 그래도 부족하면 그때 Mef_construction을 올려 재구축한다. 임베딩 모델을 갈아엎는 건 대개 그 다음이다.

자주 묻는 질문

ef_search와 ef_construction은 뭐가 다른가요?

ef_construction은 인덱스를 만들 때 그래프의 품질을 결정하는 값으로, 한 번 정해지면 바꾸려면 재구축이 필요합니다. ef_search는 질의할 때마다 조절 가능한 값으로, 탐색 후보의 폭을 넓혀 재현율을 높이는 대신 질의 지연을 늘립니다. 재구축 없이 속도-정확도를 조절할 수 있어 튜닝의 1순위입니다.

M은 얼마로 잡는 게 좋나요?

대개 16 정도가 무난한 출발점입니다. 고차원 임베딩이나 높은 재현율이 필요하면 32~48까지 올리기도 합니다. M을 키우면 그래프 연결이 촘촘해져 재현율이 오르지만 메모리 사용량과 빌드 시간이 늘고, 인덱스가 RAM을 넘기면 오히려 느려질 수 있으니 메모리 여유를 함께 봐야 합니다.

재현율이 낮을 때 제일 먼저 뭘 봐야 하나요?

대부분 ef_search가 기본값으로 방치된 경우입니다. 이 값을 100~200으로 올려 재측정해 보세요. 그래도 부족하면 M과 ef_construction을 키워 인덱스를 재구축합니다. 임베딩 모델 교체는 그 뒤에 고려하는 게 순서상 효율적입니다. 무엇보다 전수 검색 대비 recall@k를 숫자로 재면서 튜닝해야 합니다.

메타데이터 필터와 함께 쓰면 왜 결과가 비나요?

HNSW가 먼저 근사 후보를 좁힌 뒤 필터를 적용하는 방식이면, 강한 필터가 후보 대부분을 쳐내 결과가 부족해질 수 있습니다. 이때는 ef_search를 크게 잡아 후보 폭을 넓히거나, 선택도가 높은 필터에는 사전 필터링을 지원하는 엔진·전략을 쓰는 편이 안전합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록