Published on

pgvector RAG 인덱스 느림? ivfflat·hnsw 튜닝

Authors

서빙 중인 RAG 시스템에서 pgvector를 붙여두면 초반에는 잘 굴러가다가, 데이터가 늘거나 동시성이 올라가는 순간 갑자기 지연이 튀는 경우가 많습니다. 특히 다음 증상이 자주 같이 나타납니다.

  • 벡터 검색 쿼리가 간헐적으로 수백 ms에서 수 초까지 치솟음
  • 인덱스를 만들었는데도 Seq Scan 비슷한 느낌으로 느림
  • LIMIT 10인데도 CPU가 과하게 타거나 I/O가 늘어남
  • 필터(테넌트, 문서 타입, 날짜 등)를 붙이면 더 느려짐

이 글은 “왜 느린지”를 EXPLAIN으로 분해하고, ivfflat/hnsw 각각에서 실제로 체감되는 튜닝 포인트를 정리합니다. RAG 전체 비용/구조 관점은 AutoGPT 메모리 폭주? 벡터DB로 비용 절감도 함께 보면 연결이 잘 됩니다.

전제: pgvector 인덱스가 느려지는 대표 원인

  1. 인덱스 미사용
  • 연산자/정렬 형태가 인덱스가 기대하는 패턴이 아님
  • ORDER BY embedding가 아니라 ORDER BY (embedding <-> $q) 형태여야 함
  1. 필터 + ANN의 상호작용 문제
  • WHERE tenant_id = ... 같은 조건이 강하면, ANN 후보군을 많이 뽑아도 필터에서 탈락해 재시도성 비용이 커짐
  • 특히 ivfflat는 “리스트 몇 개만 탐색”하는데, 필터로 인해 유효 후보가 부족하면 품질/성능 둘 다 흔들림
  1. 파라미터 기본값이 현재 데이터 규모에 안 맞음
  • ivfflat: lists가 너무 작거나 큼, probes가 너무 작음
  • hnsw: m, ef_construction, ef_search가 부적절
  1. 통계/플래너 문제
  • 통계가 오래되어 플래너가 잘못된 계획을 선택
  • autovacuum이 밀려서 테이블/인덱스 bloat, 캐시 효율 저하
  1. I/O 및 메모리 설정
  • 벡터 인덱스는 랜덤 접근이 많아 shared_buffers, effective_cache_size, work_mem 영향이 큼

기본 테이블/쿼리 패턴(정답 형태 먼저)

아래는 문서 청크 RAG에서 흔한 스키마입니다.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE doc_chunks (
  id bigserial PRIMARY KEY,
  tenant_id bigint NOT NULL,
  doc_id bigint NOT NULL,
  chunk_no int NOT NULL,
  content text NOT NULL,
  embedding vector(1536) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- 필터용 보조 인덱스(중요)
CREATE INDEX ON doc_chunks (tenant_id, created_at);
CREATE INDEX ON doc_chunks (tenant_id, doc_id);

쿼리는 “거리 계산으로 정렬 + LIMIT” 형태가 핵심입니다.

-- cosine distance 예시
SELECT id, doc_id, chunk_no, content,
       1 - (embedding <=> $1) AS score
FROM doc_chunks
WHERE tenant_id = $2
ORDER BY embedding <=> $1
LIMIT 10;

ORDER BYembedding <=> $1(또는 embedding <-> $1, embedding <#> $1)처럼 pgvector 연산자 기반이어야 인덱스가 제대로 붙습니다.

느린지부터 확인: EXPLAIN으로 “인덱스가 쓰이는지” 보기

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, doc_id, chunk_no
FROM doc_chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;

여기서 확인할 포인트:

  • Index Scan using ... 또는 Bitmap Index Scan이 보이는지
  • Buffers: shared hit/read에서 read 비중이 과도하지 않은지
  • Rows Removed by Filter가 지나치게 큰지(필터로 후보가 많이 탈락)

만약 벡터 인덱스가 안 잡히면, 우선 “인덱스 타입/연산자 클래스/거리 연산자”가 일치하는지부터 점검해야 합니다.

ivfflat 튜닝: lists/probes가 전부다(거의)

ivfflat 인덱스 생성 기본

코사인 거리 기준 예시:

CREATE INDEX doc_chunks_embedding_ivf
ON doc_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

ANALYZE doc_chunks;

여기서 lists는 클러스터(버킷) 개수입니다. 너무 작으면 한 리스트에 데이터가 많이 몰려 검색 시 후보가 커지고, 너무 크면 학습/탐색 오버헤드가 커질 수 있습니다.

lists 추천 가이드(경험칙)

정답은 데이터 분포/차원/필터에 따라 달라지지만, 운영에서 시작점으로는 아래가 무난합니다.

  • 데이터 N이 수십만 이하: lists = 100 ~ 1000
  • 데이터 N이 백만 단위: lists = 1000 ~ 5000

너무 러프하다고 느껴질 수 있는데, ivfflat은 결국 probes로 “몇 개 리스트를 열어볼지”를 조정하면서 정밀도와 지연을 트레이드오프합니다.

probes: 쿼리마다 조절 가능한 가장 강력한 레버

SET LOCAL ivfflat.probes = 5;

SELECT id, doc_id
FROM doc_chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
  • probes가 작을수록 빠르지만 recall이 떨어질 수 있음
  • probes가 클수록 느리지만 recall이 올라감

실전 팁:

  • RAG에서는 “top-10을 완벽하게”보다 “top-10이 의미 있게”가 중요한 경우가 많습니다.
  • 따라서 probes를 무작정 키우지 말고, **정답 문서 포함률(offline eval)**과 지연을 같이 보고 결정하세요.

필터가 강하면 ivfflat이 더 느려질 수 있다

WHERE tenant_id = 42가 전체의 1%라면, ivfflat이 뽑은 후보 대부분이 다른 테넌트일 수 있습니다. 그러면 Rows Removed by Filter가 커지고, LIMIT 10을 채우기 위해 더 많은 후보가 필요해집니다.

대응 전략:

  1. 테넌트별 파티셔닝(강력)
CREATE TABLE doc_chunks_t42 PARTITION OF doc_chunks
FOR VALUES IN (42);

-- 파티션별로 벡터 인덱스 생성
CREATE INDEX ON doc_chunks_t42
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200);
  1. 테넌트별 테이블 분리(운영 복잡도 상승)

  2. 후보군 확장: probes를 키워 필터 통과 후보를 늘리기(지연 증가)

ivfflat은 “인덱스 생성 후 데이터 대량 변경”에 취약할 수 있음

대량 적재 후 인덱스를 만들거나, 적재 패턴이 바뀌었으면 아래를 습관처럼 붙이세요.

VACUUM (ANALYZE) doc_chunks;
REINDEX INDEX doc_chunks_embedding_ivf;

특히 RAG는 주기적으로 문서를 재임베딩/재수집하는데, 이때 bloat와 통계 부정확성이 체감 지연으로 이어질 수 있습니다. 락/대기까지 같이 보이면 PostgreSQL 락 대기 폭증? deadlock 진단·해결도 같이 점검하세요.

hnsw 튜닝: m/ef_search/ef_construction의 균형

hnsw는 보통 ivfflat보다 recall 대비 지연이 안정적이고, 필터가 약한 전역 검색에서 강한 편입니다. 대신 인덱스 크기와 빌드 비용이 커질 수 있습니다.

hnsw 인덱스 생성

CREATE INDEX doc_chunks_embedding_hnsw
ON doc_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

ANALYZE doc_chunks;
  • m: 그래프에서 노드당 연결 수(대체로 8~32 범위에서 시작)
  • ef_construction: 인덱스 구축 품질(클수록 빌드 느림/인덱스 커짐/검색 recall 개선)

ef_search: 쿼리 지연과 recall의 핵심

SET LOCAL hnsw.ef_search = 40;

SELECT id, doc_id
FROM doc_chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
  • ef_search를 올리면 recall이 좋아지지만 지연이 증가
  • 운영에서는 LIMIT이 10~50인 경우가 많으니, ef_searchLIMIT보다 약간 큰 값에서 시작해 점진 조정하는 방식이 안전합니다.

hnsw가 느린데 CPU가 높다면

  • ef_search가 과도하게 큰지 확인
  • 동시 쿼리가 많다면 커넥션/워크로드 제어(풀링, 큐잉)로 tail latency를 줄이는 게 더 효과적인 경우가 많습니다.

RAG에서 자주 터지는 “인덱스는 빠른데 전체 쿼리는 느림” 패턴

벡터 검색 자체는 빨라도, 아래 때문에 전체가 느려집니다.

1) 불필요한 컬럼을 먼저 읽는다

content는 길고 TOAST로 빠질 수 있어 I/O가 커집니다. 먼저 id만 뽑고 조인으로 내용을 가져오면 개선되는 경우가 많습니다.

WITH topk AS (
  SELECT id
  FROM doc_chunks
  WHERE tenant_id = $2
  ORDER BY embedding <=> $1
  LIMIT 10
)
SELECT c.id, c.doc_id, c.chunk_no, c.content
FROM doc_chunks c
JOIN topk t ON t.id = c.id;

2) rerank(재정렬) 단계가 병목

보통 RAG는 ANN으로 top-k를 뽑고, cross-encoder 또는 추가 스코어링으로 rerank합니다. 이때 k를 너무 크게 잡으면 DB는 빨라도 애플리케이션에서 터집니다.

  • ANN LIMIT은 작게(예: 20~100)
  • rerank는 배치 처리/캐시/타임아웃을 명확히

3) 필터 컬럼 인덱스가 없다

tenant_id, doc_id, created_at 같은 필터가 자주 붙는데 B-tree 인덱스가 없으면, 벡터 인덱스 후보를 가져와도 필터 처리에서 흔들립니다.

운영 튜닝 체크리스트(빠르게 점검)

쿼리/인덱스 정합성

  • 거리 연산자와 opclass가 일치하는가
    • 코사인: vector_cosine_ops + <=>
    • L2: vector_l2_ops + <->
    • inner product: vector_ip_ops + <#>
  • ORDER BY embedding ... LIMIT ... 패턴인가
  • EXPLAIN (ANALYZE, BUFFERS)에서 인덱스 스캔이 보이는가

ivfflat

  • lists가 데이터 규모 대비 너무 작지 않은가
  • SET LOCAL ivfflat.probes = ...로 recall/지연을 맞췄는가
  • 테넌트 필터가 강하면 파티셔닝을 고려했는가

hnsw

  • m을 무작정 키우지 않았는가(인덱스 크기/빌드 비용 증가)
  • ef_search가 과도하지 않은가(특히 동시성에서 tail latency 증가)

DB 운영

  • VACUUM (ANALYZE)가 제때 도는가
  • bloat 의심 시 REINDEX를 고려했는가
  • 캐시/메모리 설정이 워크로드에 맞는가

벤치마크 방법: “recall vs latency”를 수치로 잡기

튜닝은 감으로 하면 끝이 없습니다. 최소한 아래 두 축을 수치화하세요.

  1. Latency
  • p50/p95/p99로 측정(특히 p95/p99)
  • 동시성 수준별로 측정(1, 5, 20, 50 등)
  1. Recall(또는 hit-rate)
  • 기준: brute-force(정확 검색) 결과의 top-k와 ANN top-k의 교집합 비율
  • 또는 “정답 문서가 top-k에 포함되는 비율”

오프라인 평가를 자동화해두면 probes/ef_search를 올릴지 말지 논쟁이 사라집니다. 관련해서 더 깊은 실전 튜닝 흐름은 pgvector RAG 지연↓ - IVF/HNSW 튜닝 실전도 참고하면 좋습니다.

결론: 어떤 상황에 ivfflat vs hnsw?

  • ivfflat

    • 장점: 구조가 단순, 파라미터가 직관적(lists, probes)
    • 주의: 강한 필터/테넌트 분리에서 설계(파티셔닝 등)가 없으면 성능이 요동칠 수 있음
  • hnsw

    • 장점: recall 대비 지연이 안정적인 편, 운영에서 튜닝 성공률이 높음
    • 주의: 인덱스 크기/빌드 비용, ef_search 과대 설정 시 CPU/지연 급증

실무에서 가장 흔한 해결 루트는 다음 순서입니다.

  1. EXPLAIN (ANALYZE, BUFFERS)로 인덱스 사용/필터 탈락/TOAST I/O를 확인
  2. ivfflat.probes 또는 hnsw.ef_search를 “필요한 만큼만” 올려 품질을 맞춤
  3. 필터가 강하면 파티셔닝/테넌트 분리로 ANN 후보 낭비를 줄임
  4. VACUUM (ANALYZE)/REINDEX로 통계와 bloat를 관리

이 4단계를 밟으면, 대부분의 pgvector RAG 지연 문제는 재현 가능하게 설명되고, 튜닝도 반복 가능한 작업으로 바뀝니다.