LabHub

벡터 데이터베이스 — pgvector 와 Qdrant · ANN 인덱스와 재현율 · 이론

정확도를 팔아 속도를 산다 — ANN 인덱스와 재현율

LabHub 에서 이어서 보기

한 줄 요약

ANN 인덱스는 정확도를 팔아 속도를 사는 장치다. 얼마나 팔았는지를 재는 자가 재현율(recall)이고, 재지 않고 쓰는 인덱스는 무엇을 잃고 있는지 모르는 채로 돌아간다.

왜 이게 필요했나

2천 건은 정확 검색이 1밀리초다. 2백만 건이면 1초가 넘고, 동시 요청 100개면 데이터베이스가 멈춘다. B-tree 는 소용없다 — 거리는 한 열을 정렬하는 문제가 아니라 64차원 공간의 이웃을 찾는 문제이고, 그것을 정확히 찾는 인덱스는 차원이 커지면 순차 탐색보다 나을 게 없다.

그래서 근사(approximate) 로 간다. 진짜 이웃 10개 중 9개쯤을 100분의 1 시간에 찾는 것이고, 어떤 하나를 놓치는지는 인덱스가 정한다.

두 인덱스, 두 손잡이

HNSW 는 벡터를 층층이 쌓은 그래프로 잇는다. 위층은 성긴 지름길, 아래층은 촘촘한 이웃 관계이고, 검색은 위층에서 내려오며 좁힌다. 만들 때의 손잡이가 m(노드마다 잇는 간선 수)과 ef_construction(만들 때 살펴보는 후보 수), 찾을 때의 손잡이가 hnsw.ef_search(찾을 때 살펴보는 후보 수)다. 이 코스 환경에서 잰 값이다(만들 때 무작위성이 있어 만들 때마다 조금씩 다르다).

ef_search  10 → recall@10 0.815ef_search  40 → recall@10 0.985     (기본값)ef_search 100 → recall@10 1.000

IVFFlat 은 벡터를 lists 개의 군집으로 나누고, 찾을 때 질의와 가까운 probes 개 군집만 뒤진다. 만들기가 빠르고 작지만, 군집 경계 근처의 이웃을 놓친다.

lists 40 · probes  1 → recall@10 0.325lists 40 · probes 10 → recall@10 0.935lists 40 · probes 40 → recall@10 1.000   (전부 뒤짐 = 정확 검색)

같은 재현율 0.93 에 HNSW 는 후보 20개, IVFFlat 은 군집 10개(전체의 1/4)를 봐야 했다. 대부분의 운영 환경이 HNSW 를 고르는 이유이고, IVFFlat 은 만들기 비용과 메모리가 빠듯할 때 남는다.

재현율은 어떻게 재나

정답은 정확 검색이다. 질의 20개를 골라 enable_indexscan = off 로 상위 10건을 뽑아 두고, 인덱스로 뽑은 10건과 겹치는 비율을 평균한다. 그게 recall@10 이다. 이 코스의 recall.py 가 하는 일이 이것이며, 질의는 운영에서 실제로 들어오는 것과 닮아야 뜻이 있다.

EXPLAIN 으로 인덱스가 실제로 쓰였는지를 반드시 본다. pgvector 인덱스는 ORDER BY embedding <=> q LIMIT k 꼴에서만 탄다. WHERE embedding <=> q < 0.3 처럼 쓰면 인덱스를 못 쓰고 순차 탐색으로 떨어진다 — 느려서가 아니라 조용히 느려져서 위험하다.

실무에서 진짜 중요한 것

첫째, 목표 재현율을 먼저 정한다. 0.95 면 되는지 0.99 여야 하는지는 제품이 정한다. 그다음 그 목표를 넘는 가장 작은 ef_search 를 찾아 ALTER DATABASE ... SET 으로 못박는다. 세션마다 SET 을 치는 방식은 언젠가 빠뜨린다.

둘째, 인덱스는 데이터가 바뀌면 낡는다. 지운 벡터의 자리는 HNSW 그래프에 남고, IVFFlat 의 군집 중심은 만들 때의 데이터에 고정된다. 대량 갱신 뒤에는 재현율을 다시 재고, 떨어졌으면 REINDEX 한다.

셋째, 재현율을 지표로 남긴다. 검색 품질은 느려지는 것과 달리 아무도 신고하지 않는다. 같은 질의 묶음으로 주기적으로 재어야 조용히 나빠지는 것을 잡는다.