- Published on
pgvector 인덱스 튜닝으로 RAG 검색 2배 빠르게
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스 벡터 DB나 전용 벡터 엔진을 도입하기 전에, 이미 운영 중인 PostgreSQL에서 pgvector만 제대로 튜닝해도 RAG 검색이 눈에 띄게 빨라지는 경우가 많습니다. 특히 top-k 근접 이웃 검색이 병목인 워크로드에서는 인덱스 종류 선택과 파라미터 조정만으로도 지연 시간이 2배 이상 개선되는 케이스가 흔합니다.
이 글은 다음을 목표로 합니다.
- RAG 검색에서
pgvector가 느려지는 전형적인 원인 파악 HNSW와IVFFLAT중 무엇을 언제 선택할지 기준 제시- 인덱스 파라미터와 쿼리 패턴을 바꿔 실제 latency를 줄이는 방법
EXPLAIN (ANALYZE, BUFFERS)로 개선을 검증하는 방법
운영에서 성능 이슈를 다룰 때는 인프라/네트워크 문제도 함께 점검하게 됩니다. 만약 검색 API가 쿠버네티스 위에서 간헐적으로 타임아웃/502로 보인다면 애플리케이션 이전에 인그레스/타겟 타임아웃도 같이 확인하세요: EKS ALB Ingress 502 target timeout 원인·해결
RAG에서 pgvector가 느려지는 4가지 패턴
1) 인덱스 없이 풀스캔
가장 흔한 문제입니다. ORDER BY embedding <-> $query LIMIT k 형태는 인덱스가 없으면 전체 행에 대해 거리 계산을 수행합니다. 데이터가 10만을 넘어가면 급격히 느려집니다.
2) 인덱스는 있는데 쿼리가 인덱스를 못 탐
- 거리 연산자를 잘못 사용하거나
WHERE조건이 복잡해서 플래너가 다른 경로를 선택하거나LIMIT없이 정렬만 수행하는 쿼리
이런 경우 Index Scan이 아니라 Seq Scan 또는 Sort가 튀어나옵니다.
3) IVFFLAT의 lists가 부적절
IVFFLAT은 클러스터(리스트) 기반 근사 검색이라 lists 설정이 품질과 속도를 좌우합니다. 너무 작으면 후보군이 커져 느려지고, 너무 크면 학습/메모리/오버헤드가 증가합니다.
4) HNSW 파라미터가 기본값에 머무름
HNSW는 검색 시 ef_search가, 빌드 시 ef_construction과 m이 성능과 재현율을 크게 바꿉니다. 기본값은 “무난”하지만 목표 latency와 recall에 맞춰 조정해야 합니다.
기본 스키마와 쿼리: 튜닝의 출발점
아래 예시는 문서 청크를 저장하고, RAG 검색에서 top-k를 뽑는 전형적인 구조입니다.
-- 확장 설치
CREATE EXTENSION IF NOT EXISTS vector;
-- 문서 청크 테이블
CREATE TABLE doc_chunks (
id bigserial PRIMARY KEY,
doc_id text NOT NULL,
content text NOT NULL,
embedding vector(1536) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 자주 쓰는 필터가 있다면 컬럼화
ALTER TABLE doc_chunks ADD COLUMN tenant_id text;
CREATE INDEX ON doc_chunks (tenant_id);
검색 쿼리는 보통 이렇게 시작합니다.
SELECT id, doc_id, content
FROM doc_chunks
WHERE tenant_id = $1
ORDER BY embedding <-> $2
LIMIT 10;
여기서 핵심은 WHERE tenant_id = $1 같은 필터와 벡터 인덱스가 함께 있을 때 플래너가 어떤 경로를 선택하느냐입니다. 필터 선택도가 낮으면(대부분 같은 tenant) 벡터 인덱스가 더 중요해지고, 선택도가 높으면(tenant별 데이터가 작음) B-Tree 필터 후 벡터 계산이 더 빠를 수도 있습니다.
HNSW vs IVFFLAT: 선택 기준
HNSW를 추천하는 경우
- 온라인 트래픽이 많고 낮은 지연 시간이 최우선
- 데이터가 지속적으로 늘어나며, 인덱스 재학습 비용을 피하고 싶음
top-k가 작고(예: 5~20) 쿼리 QPS가 높음
IVFFLAT을 추천하는 경우
- 데이터가 매우 크고, 메모리 사용을 상대적으로 통제하고 싶음
- 배치로 인덱스를 재구성/학습할 수 있음
- recall을 조금 희생하고 속도를 크게 얻고 싶음(파라미터로 조절)
실무에서는 “일단 HNSW로 시작하고, 메모리/빌드 비용이 부담되면 IVFFLAT로 전환”하는 흐름이 많습니다.
HNSW 인덱스 튜닝: m, ef_construction, ef_search
1) 인덱스 생성
-- cosine 유사도 기반이면 vector_cosine_ops
CREATE INDEX CONCURRENTLY doc_chunks_embedding_hnsw
ON doc_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
m: 그래프의 연결 정도. 보통 12~32 범위에서 시작합니다. 올리면 recall이 좋아지고 검색이 빨라질 수도 있지만(탐색 경로 안정), 메모리와 빌드 비용이 증가합니다.ef_construction: 인덱스 구축 품질. 64~256 범위에서 튜닝합니다. 올리면 빌드가 느려지지만 recall이 좋아집니다.
2) 검색 시 ef_search로 속도/정확도 트레이드오프
ef_search는 쿼리마다 바꿀 수 있는 레버입니다.
-- 세션 단위로 조정
SET hnsw.ef_search = 40;
SELECT id, doc_id
FROM doc_chunks
ORDER BY embedding <-> $1
LIMIT 10;
ef_search를 낮추면 빨라지지만 recall이 떨어집니다.- RAG는 보통 “정답 1개”가 아니라 “괜찮은 컨텍스트 여러 개”가 중요하므로, 적정 recall만 유지하면 속도를 공격적으로 가져가도 품질이 크게 안 무너지는 경우가 많습니다.
운영 팁:
top-k가 10이면ef_search는 20~100 사이에서 실험하는 경우가 많습니다.- 질의가 다양하고 긴 tail이 있으면
ef_search를 너무 낮추지 마세요.
3) “2배 빠르게”가 나오는 대표 조합
- 기본값 근처에서
ef_search가 과하게 높아 latency가 튀는 경우 m이 너무 작아 탐색이 불안정해 많은 후보를 뒤지는 경우
예를 들어 m = 16에서 m = 24로 올리고, ef_search를 80에서 40으로 내리면 recall을 비슷하게 유지하면서 latency가 크게 줄어드는 케이스가 있습니다(데이터 분포에 따라 다름).
IVFFLAT 인덱스 튜닝: lists와 ivfflat.probes
1) 인덱스 생성 전 주의: ANALYZE와 학습
IVFFLAT은 인덱스 생성 시 클러스터링을 수행합니다. 데이터 분포가 반영되도록 통계가 최신이어야 합니다.
ANALYZE doc_chunks;
CREATE INDEX CONCURRENTLY doc_chunks_embedding_ivf
ON doc_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
lists: 클러스터 개수. 경험칙으로는sqrt(N)근처에서 시작하는 팀도 있고,N/1000같은 규칙을 쓰는 팀도 있습니다.- 중요한 건 “정답은 없고 측정으로 결정”입니다.
2) 검색 시 probes가 사실상 핵심 레버
SET ivfflat.probes = 5;
SELECT id, doc_id
FROM doc_chunks
ORDER BY embedding <-> $1
LIMIT 10;
probes는 몇 개의 리스트를 탐색할지 결정합니다.- 낮을수록 빠르지만 recall이 떨어집니다.
- RAG에서는
probes를 1~10 범위에서 먼저 스윕해보는 것을 추천합니다.
3) IVFFLAT에서 흔한 실수
lists를 너무 크게 잡아 인덱스 생성/유지 비용이 과도해짐probes를 너무 낮춰서 검색 품질이 급격히 하락- 데이터가 계속 늘어나는데 인덱스를 재구성하지 않아 클러스터 품질이 나빠짐
쿼리 패턴 튜닝: “필터 먼저” vs “벡터 먼저”
RAG에서 거의 항상 들어가는 조건이 있습니다. 예를 들면 tenant_id, doc_type, created_at, language 같은 필터입니다.
1) 필터 선택도가 높으면, 후보를 줄인 뒤 벡터 계산이 유리
SELECT id, doc_id
FROM doc_chunks
WHERE tenant_id = $1
AND created_at >= now() - interval '90 days'
ORDER BY embedding <-> $2
LIMIT 10;
이때는 벡터 인덱스가 있어도, 플래너가 필터 인덱스를 타고 들어간 뒤 정렬/거리 계산을 할 수 있습니다. 데이터가 충분히 줄어든다면 이게 더 빠를 수 있습니다.
2) 필터 선택도가 낮으면, 벡터 인덱스가 사실상 전부
tenant가 하나거나 대부분의 데이터가 같은 조건에 걸리면, 필터는 성능에 거의 도움이 안 됩니다. 이때는 HNSW/IVFFLAT 파라미터가 지연 시간을 결정합니다.
3) “2단계 검색”으로 안정적인 latency 만들기
근사 검색으로 넓게 뽑고, 그 안에서 추가 조건/정렬을 수행하는 패턴입니다.
WITH candidates AS (
SELECT id
FROM doc_chunks
WHERE tenant_id = $1
ORDER BY embedding <-> $2
LIMIT 50
)
SELECT d.id, d.doc_id, d.content
FROM doc_chunks d
JOIN candidates c ON c.id = d.id
ORDER BY d.embedding <-> $2
LIMIT 10;
- 1단계에서 인덱스를 강하게 활용해 후보를 제한
- 2단계에서 정확한 정렬(재랭킹)으로 품질 보정
이 패턴은 “recall을 위해 ef_search나 probes를 과도하게 올려야 하는 상황”에서 특히 효과적입니다.
측정과 검증: EXPLAIN (ANALYZE, BUFFERS)로 끝내기
튜닝은 감이 아니라 숫자로 해야 합니다. 아래처럼 실제 실행 시간과 버퍼 히트/리드를 비교하세요.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM doc_chunks
ORDER BY embedding <-> $1
LIMIT 10;
확인 포인트:
Index Scan using ... hnsw또는Index Scan using ... ivfflat이 나오는지Rows Removed by Filter가 과도하게 큰지shared read가 많아 디스크 I/O가 병목인지(캐시 미스)
운영에서 자주 보는 함정은 “인덱스는 잘 타는데, 워킹셋이 커서 페이지 캐시를 못 타는” 경우입니다. 이때는 DB 튜닝뿐 아니라 노드 메모리, 커넥션 풀, 캐시 전략까지 같이 봐야 합니다. 애플리케이션 레벨에서 재시도/백오프가 들어가면 지연이 더 증폭될 수 있으니, 외부 API 호출이 섞인 RAG 파이프라인이라면 OpenAI 429와 Rate Limit 헤더로 재시도 설계도 함께 참고하면 좋습니다.
운영 체크리스트: 성능을 꾸준히 유지하는 방법
1) 통계 최신화
- 데이터가 빠르게 변하면
ANALYZE주기가 중요합니다.
ANALYZE doc_chunks;
2) 인덱스 재구성 전략
- HNSW는 증분 업데이트에 강하지만, 대규모 삭제/갱신이 반복되면 재구성이 유리할 수 있습니다.
- IVFFLAT은 데이터 분포가 바뀌면 성능/recall이 흔들릴 수 있어 주기적 재생성이 필요할 수 있습니다.
3) RAG 파이프라인 관점에서 “검색만” 최적화하지 않기
검색이 2배 빨라져도 전체 응답 시간이 그대로인 경우가 있습니다.
- 임베딩 생성이 동기 호출인지
- 재랭킹 모델 호출이 병목인지
- 컨텍스트 조립(문서 로딩)이 N+1인지
벡터 검색 결과를 TTL 캐시하거나 오래된 벡터를 정리하는 전략도 장기적으로 중요합니다. 벡터 저장이 계속 늘어나면 인덱스 자체가 무거워져 검색이 느려질 수 있으니, 보존 정책을 고민 중이라면 AutoGPT 메모리 누수 막는 벡터DB TTL·압축에서 아이디어를 얻을 수 있습니다.
실전 튜닝 플로우(요약)
- 현재 쿼리에
EXPLAIN (ANALYZE, BUFFERS)를 붙여 풀스캔 여부를 먼저 확인 - 인덱스 선택
- 낮은 latency, 온라인 서비스: HNSW 우선
- 메모리/재학습 가능: IVFFLAT 고려
- 파라미터 스윕
- HNSW:
m16→24,ef_search20~100 범위 테스트 - IVFFLAT:
lists후보 50/100/200,probes1~10 테스트
- HNSW:
- 2단계 검색(
LIMIT 50후보 후 재정렬)로 품질/속도 균형 잡기 - 워킹셋과 I/O를 확인해 “인덱스는 빠른데 캐시 미스” 문제를 제거
마무리
pgvector는 “PostgreSQL에서 벡터 검색을 된다” 수준을 넘어, 인덱스와 파라미터를 제대로 잡으면 RAG 검색의 핵심 병목을 충분히 줄일 수 있는 선택지입니다. 특히 HNSW의 ef_search와 IVFFLAT의 probes는 운영 중에도 조정 가능한 레버라서, 트래픽/품질 요구에 맞춰 동적으로 최적화하기 좋습니다.
다음 단계로는 “우리 데이터셋에서 recall을 어떻게 측정할지”와 “top-k 후보를 어떤 재랭킹으로 보정할지”를 정하면, 속도 개선이 곧바로 답변 품질 안정으로 연결됩니다.