
문서가 몇만 건만 넘어도 벡터 검색이 갑자기 느려지는 이유가 뭘까요? 대부분은 인덱스 없이 저장된 벡터 전체를 하나하나 비교하는 방식을 그대로 쓰고 있기 때문이고, 이럴 때 HNSW나 IVF 같은 벡터 인덱스를 붙이면 비교 대상 자체를 크게 줄일 수 있습니다. 이 글에서는 두 인덱스가 속도를 버는 원리와 pgvector에서 실제로 걸어보는 방법, 파라미터를 어떻게 잡아야 하는지, 그리고 인덱스를 걸어도 안 통하는 상황까지 순서대로 정리합니다.
왜 문서가 몇만 건만 넘어도 벡터 검색이 느려질까요?
인덱스가 없는 상태에서 벡터 검색을 돌리면, 쿼리 벡터 하나를 저장된 벡터 전부와 거리 계산(코사인 유사도나 L2 거리)해서 비교합니다. 이 방식을 흔히 브루트포스(Flat) 검색이라고 부르는데, 검색 시간이 저장된 문서 수에 비례해서 늘어나는 구조입니다.
문서가 1만 건 정도면 몇 밀리초 안에 끝나지만, 10만 건, 100만 건으로 늘면 계산량도 그만큼 늘어나고, 여기에 임베딩 차원까지 커지면 부담이 배가됩니다. OpenAI의 text-embedding-3-small만 해도 1536차원이라, 벡터 하나 비교에 곱셈·덧셈 연산이 1536번씩 들어갑니다. 문서 수가 늘어난 시점에 검색이 느려졌다면, 이 브루트포스 스캔이 원인일 가능성이 큽니다.
HNSW와 IVF, 두 인덱스가 검색 범위를 줄이는 방식의 차이
HNSW(Hierarchical Navigable Small World)는 벡터들을 여러 층의 그래프로 연결해두고, 검색할 때 위쪽 층부터 시작해 목표 벡터에 가까운 방향으로만 그래프를 타고 내려가는 방식입니다. 전체 데이터를 다 보지 않고 그래프의 일부 경로만 훑기 때문에 속도가 빨라집니다. 이 알고리즘의 원리는 위키백과 HNSW 문서에 그래프 구조와 탐색 절차가 정리되어 있습니다.
IVF(Inverted File Index)는 접근 방식이 다릅니다. k-means로 벡터들을 여러 클러스터로 미리 묶어두고, 검색할 때는 쿼리 벡터와 가까운 클러스터 중심 몇 개만 골라 그 안에 있는 벡터들만 비교합니다. 클러스터를 미리 학습해야 한다는 점에서 HNSW와 근본적으로 다릅니다.
| 구분 | HNSW | IVF(IVFFlat) |
|---|---|---|
| 기본 구조 | 다층 그래프 탐색 | k-means 클러스터링 + 역색인 |
| 인덱스 생성 전 학습 | 불필요, 삽입하면서 그래프 구성 | 필요, 클러스터 중심을 먼저 학습(training) |
| 빌드 시간·메모리 | 상대적으로 오래 걸리고 메모리 사용량 큼 | 상대적으로 빠르고 가벼움 |
| 검색 속도·재현율 | 재현율이 높은 편, 파라미터 튜닝으로 조절 | 클러스터 수와 탐색 클러스터 수에 크게 좌우됨 |
| 데이터 삽입·삭제 | 그래프에 계속 노드가 쌓여 메모리가 늘어남 | 분포가 바뀌면 재학습(rebuild)이 필요할 수 있음 |

pgvector로 실제 HNSW·IVF 인덱스를 만들어 봅니다
PostgreSQL 확장인 pgvector는 0.5.0 버전부터 HNSW를 지원하고, 그 이전부터 IVFFlat을 지원해왔습니다. 두 인덱스 모두 지원되는 최신 버전(0.7.x대) 기준으로 예시를 들면 다음과 같습니다.
-- pgvector 확장 설치 (PostgreSQL 12+, pgvector 0.5.0 이상에서 HNSW 지원)
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text,
embedding vector(1536) -- OpenAI text-embedding-3-small 기준
);
-- HNSW 인덱스 생성
-- m: 노드당 최대 연결 수(기본값 16), ef_construction: 빌드 시 탐색 폭(기본값 64)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 검색 시 탐색 폭 조정 (기본값 40, 값이 클수록 정확하지만 느려짐)
SET hnsw.ef_search = 100;
SELECT id, content
FROM documents
ORDER BY embedding <=> '[0.012, -0.034, 0.087]'
LIMIT 10;
IVFFlat은 클러스터 중심을 학습해야 하므로, 데이터가 어느 정도 쌓인 뒤 생성하는 편이 좋습니다.
-- IVFFlat 인덱스 생성
-- lists: 클러스터 개수. 10만 행 이하는 rows/1000, 그 이상은 sqrt(rows) 정도가 권장값
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 316); -- 10만 행 기준 sqrt(100000) ≈ 316
-- 검색 시 탐색할 클러스터 수 (기본값 1, 값을 올릴수록 재현율은 오르지만 느려짐)
SET ivfflat.probes = 10;
인덱스 옵션과 기본값에 대한 더 자세한 설명은 pgvector 공식 저장소 문서에서 확인할 수 있습니다. 참고로 IVFFlat은 테이블이 비어 있는 상태에서 인덱스를 만들면 클러스터 중심이 제대로 잡히지 않으니, 어느 정도 데이터를 넣은 뒤 생성해야 합니다.
이 기준으로 인덱스와 파라미터를 골라보세요
문서 수가 1만~5만 건 사이라면, 인덱스 없이 시퀀셜 스캔을 유지해도 체감 속도 차이가 크지 않을 수 있습니다. 이 구간에서는 인덱스를 걸었을 때의 재현율 손실과 관리 부담을 먼저 따져보는 편이 낫습니다.

10만 건을 넘어서면서 삽입·수정이 잦은 서비스(예: 실시간으로 쌓이는 로그나 대화 기록)라면 HNSW 쪽이 다루기 쉬운 경우가 많습니다. 그래프에 새 노드를 계속 붙이는 구조라 재학습 절차가 없기 때문입니다. 반대로 데이터가 한 번에 대량으로 적재되고 이후 변경이 적은 배치성 컬렉션이라면, 빌드가 가볍고 메모리를 덜 쓰는 IVFFlat이 운영 비용 면에서 유리할 수 있습니다.
정확도가 중요한 검색(법률·의료 문서 검색처럼 상위 결과 누락이 치명적인 경우)이라면 ef_search나 probes 값을 높여 재현율을 우선하고, 응답 속도가 더 중요한 서비스(자동완성, 챗봇 컨텍스트 검색)라면 값을 낮춰 지연시간을 줄이는 방향으로 잡아보세요.
인덱스를 걸었는데도 느리거나 결과가 이상하게 나올 때는?
HNSW와 IVF는 모두 근사 최근접 이웃(Approximate Nearest Neighbor) 방식이라, 정확도를 100% 보장하지 않습니다. ef_search나 probes 값이 너무 낮으면 실제로는 더 가까운 문서가 있는데도 놓치는 경우가 생길 수 있습니다. 검색 결과가 기대와 다르다면 가장 먼저 이 값을 올려보고 속도와 재현율 사이에서 균형점을 찾아야 합니다.
문서 수가 애초에 적은데(수천 건 이하) IVF를 걸면 오히려 역효과가 날 수 있습니다. 클러스터당 벡터 수가 너무 적어져 클러스터 경계에서 결과가 튀거나, 빌드 자체가 불안정해지기 때문입니다. 이런 경우엔 인덱스 없이 브루트포스로 두는 편이 낫습니다.
WHERE 조건을 함께 거는 필터링 검색에서는 인덱스 효율이 떨어지는 경우도 있습니다. 최근 pgvector 릴리스에서는 필터 조건과 함께 쓸 때 결과가 LIMIT보다 적게 나오는 문제를 보완하는 기능이 추가되고 있으니, 필터를 많이 쓰는 쿼리라면 사용 중인 pgvector 버전의 릴리스 노트를 한 번 확인해보는 편이 좋습니다.
결과 정리: 문서 규모와 운영 조건에 따라 인덱스를 다르게 고르면 됩니다
문서가 늘면서 벡터 검색이 느려지는 원인은 대부분 브루트포스 스캔이고, 해결책은 HNSW나 IVF 같은 벡터 인덱스를 붙여 비교 대상을 줄이는 것입니다. HNSW는 그래프 기반이라 삽입이 잦은 서비스에, IVF는 빌드가 가벼워 배치성 데이터에 잘 맞고, 두 방식 모두 재현율과 속도를 조절하는 파라미터(ef_search, probes)를 갖고 있습니다.
지금 서비스가 느리다고 느껴진다면, 먼저 EXPLAIN ANALYZE로 현재 쿼리가 시퀀셜 스캔을 타는지 확인해보세요. 인덱스가 없다는 게 확인되면 문서 수와 삽입 빈도를 기준으로 HNSW부터 시도해보고, ef_search 값을 40, 100, 200 정도로 바꿔가며 응답 시간과 결과 품질을 직접 비교해보는 것이 가장 확실한 다음 단계입니다.
