Published on

pgvector HNSW RAG 튜닝 - ef/IVFFlat로 지연↓

Authors

RAG에서 체감 지연을 만드는 병목은 대개 LLM이 아니라 벡터 검색 단계입니다. 특히 PostgreSQL + pgvector 조합은 운영 편의성이 뛰어난 대신, 기본 설정으로는 검색 지연이 쉽게 튀고(콜드 캐시, VACUUM/통계, 인덱스 파라미터 미스매치), 리콜이 부족하면 답변 품질도 흔들립니다.

이 글은 pgvector의 대표 인덱스인 HNSWIVFFlat을 RAG에 맞게 튜닝해 지연을 낮추면서도 리콜을 유지하는 방법을 정리합니다. 핵심은 다음 3가지를 측정 기반으로 맞추는 것입니다.

  • HNSW: ef_search, ef_construction, m
  • IVFFlat: lists, probes
  • RAG 파이프라인: 후보 수 k와 리랭커(또는 cross-encoder) 전략

관련해서 HNSW 튜닝 감을 잡는 데는 Qdrant 사례도 도움이 됩니다. 개념은 유사하고(그래프 탐색 폭과 리콜/지연 트레이드오프), 측정 방법도 비슷합니다: RAG 검색품질 2배 - Qdrant HNSW 튜닝 실전

전제: RAG에서 “빠른 검색”의 정의

RAG 검색은 보통 다음 목표를 동시에 만족해야 합니다.

  1. P95/P99 지연을 낮춘다 (사용자 체감)
  2. 리콜을 유지한다 (정답 문서가 후보에 들어와야 함)
  3. 안정성을 확보한다 (데이터 증가, 업데이트, VACUUM 이후에도 성능이 크게 흔들리지 않음)

여기서 리콜은 보통 오프라인 평가로 잡습니다.

  • 골든 쿼리 세트(질문, 정답 문서 id)
  • 지표: Recall@k, MRR, nDCG
  • 온라인 지표: 클릭/정답률/후속 질문률

그리고 “지연”은 DB만 재는 게 아니라 아래를 분리해서 봐야 합니다.

  • 임베딩 생성 시간
  • DB 쿼리 시간(네트워크 포함)
  • 리랭킹 시간
  • LLM 생성 시간

이 글은 DB 검색 구간을 중심으로 다룹니다.

pgvector 인덱스 선택: HNSW vs IVFFlat

pgvector는 크게 두 계열의 근사 최근접 탐색(ANN) 인덱스를 제공합니다.

HNSW가 유리한 경우

  • 높은 리콜이 필요하고, k가 작지 않더라도 안정적으로 가져오고 싶다
  • 쿼리당 지연을 낮추되, 인덱스 메모리 사용량을 감수할 수 있다
  • 삽입/업데이트가 잦지 않거나(또는 배치), 인덱스 빌드 시간이 길어도 된다

HNSW는 그래프 기반 탐색이라 ef_search를 올리면 리콜이 좋아지지만 CPU와 지연이 증가합니다.

IVFFlat이 유리한 경우

  • 데이터가 매우 크고, 메모리 예산이 타이트하다
  • 쿼리 패턴이 일정하고, lists/probes로 성능을 예측 가능하게 만들고 싶다
  • 배치 인덱싱(리빌드)이 가능한 환경이다

IVFFlat은 클러스터(lists)를 나누고 일부(probes)만 탐색합니다. probes를 낮추면 빨라지지만 리콜이 떨어질 수 있습니다.

스키마/쿼리 기본기: 튜닝 전에 확인할 것

1) 벡터 차원과 연산자 정합성

임베딩 모델 차원이 768인데 컬럼이 1536이면 삽입이 안 되거나, 캐스팅 비용이 발생할 수 있습니다.

또한 거리 연산자 선택이 인덱스와 맞아야 합니다.

  • 코사인 유사도: 보통 vector_cosine_ops
  • L2 거리: vector_l2_ops
  • 내적: vector_ip_ops

2) 필터가 있으면 “벡터만” 빠르게 해도 느릴 수 있음

RAG는 보통 멀티테넌트, 문서 타입, 권한, 시간 범위 같은 필터가 붙습니다. 이때는 다음을 고려해야 합니다.

  • 필터 컬럼에 B-Tree 인덱스
  • 파티셔닝(테넌트 단위)
  • “필터 후 벡터” vs “벡터 후 필터”의 실행계획 확인

3) 테이블 bloat/통계 문제

삭제/업데이트가 많으면 테이블/인덱스 bloat로 인해 랜덤 IO가 늘고 지연이 튈 수 있습니다. 특히 RAG에서 문서 재수집/재임베딩이 잦으면 자주 발생합니다.

VACUUM이 기대만큼 효과가 없거나 autovacuum이 밀린다면 아래 글이 도움이 됩니다: PostgreSQL VACUUM 안먹을 때 - bloat 원인·해결

HNSW 튜닝: ef_search가 지연을 결정한다

HNSW는 크게 다음 파라미터를 봅니다.

  • m: 그래프 연결 수(대략). 높을수록 리콜이 좋아지지만 인덱스가 커지고 빌드/삽입 비용이 증가
  • ef_construction: 인덱스 빌드 품질. 높을수록 리콜이 좋아질 수 있으나 빌드 시간이 증가
  • ef_search: 쿼리 시 탐색 폭. 지연과 리콜을 가장 직접적으로 좌우

HNSW 인덱스 생성 예시

아래 예시는 코사인 기준입니다.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE rag_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(768) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- 필터용 인덱스(멀티테넌트/문서 단위)
CREATE INDEX rag_chunks_tenant_doc_idx ON rag_chunks (tenant_id, doc_id);

-- HNSW 인덱스
CREATE INDEX rag_chunks_embedding_hnsw
ON rag_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);

쿼리 시 ef_search 조절

pgvector는 SET LOCAL로 세션 단위로 조절할 수 있어, API 요청 단위로 튜닝하기 좋습니다.

BEGIN;
SET LOCAL hnsw.ef_search = 40;

SELECT id, doc_id, chunk_no, content
FROM rag_chunks
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;

COMMIT;

여기서 $2는 쿼리 임베딩 벡터입니다. 본문에 부등호가 들어가면 MDX에서 오인될 수 있어, 위 쿼리는 코드 블록 안에서만 사용해야 합니다.

실전 가이드: ef_search는 “k의 2~10배”에서 시작

경험적으로 다음처럼 시작하면 튜닝 시간이 줄어듭니다.

  • k = 10이면 ef_search = 40부터 시작
  • 리콜이 부족하면 80, 120으로 올려본다
  • 지연이 목표를 넘으면 k를 줄이거나(후단 리랭킹으로 보완), ef_search를 낮춘다

중요한 점은 RAG에서 최종적으로 필요한 건 “진짜 top-1”이 아니라 리랭커가 정답을 고를 수 있을 만큼의 후보라는 것입니다. 그래서 HNSW에서 ef_search를 무작정 올리기보다, 아래처럼 파이프라인을 나누는 게 더 효율적입니다.

  • 1차 검색: 빠르게 후보 50개
  • 2차 리랭킹: 50개 중 상위 5~10개 선택

이 구조에서는 HNSW의 ef_search를 “리랭커가 먹을 후보 수”에 맞춰 최소화할 수 있습니다.

mef_construction은 자주 안 바꾸되, 너무 낮게 시작하지 말 것

  • m = 16은 무난한 시작점
  • 리콜이 계속 낮고 ef_search를 올려도 개선이 미미하면 m = 24 또는 32를 고려
  • ef_construction은 64~256 범위에서 시작

단, m을 올리면 인덱스 메모리 사용량이 증가하고, 빌드 시간이 늘어납니다. 운영 환경에서는 인덱스 빌드/리빌드 시간도 SLO에 포함시키는 게 좋습니다.

IVFFlat 튜닝: lists/probes로 “탐색 범위”를 제어한다

IVFFlat은 다음 파라미터가 핵심입니다.

  • lists: 클러스터 개수(인덱스 생성 시 고정)
  • probes: 쿼리 시 탐색할 클러스터 수(동적으로 조절 가능)

IVFFlat 인덱스 생성 예시

CREATE INDEX rag_chunks_embedding_ivfflat
ON rag_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 2000);

-- 통계/플래너를 위해 권장
ANALYZE rag_chunks;

lists는 데이터 크기에 따라 달라집니다. 흔히 다음을 старт 포인트로 둡니다.

  • 수십만 행: lists 100~1000
  • 수백만 행: lists 1000~5000
  • 수천만 행: lists 5000 이상

정답은 없고, 결국 probes와 세트로 측정해야 합니다.

쿼리 시 probes 조절

BEGIN;
SET LOCAL ivfflat.probes = 10;

SELECT id, doc_id, chunk_no, content
FROM rag_chunks
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;

COMMIT;
  • probes를 낮추면 빨라지지만 리콜이 떨어질 수 있음
  • probes를 올리면 리콜이 좋아지지만 지연이 증가

실전에서는 probes = 5 또는 10부터 시작해서, 리콜이 부족하면 20, 50으로 올려봅니다.

IVFFlat의 함정: 데이터 분포가 바뀌면 성능이 흔들린다

IVFFlat은 클러스터링 기반이라, 데이터가 크게 누적되거나 분포가 변하면 초기 클러스터가 최적이 아닐 수 있습니다. 이때는 다음을 고려합니다.

  • 주기적 리인덱싱(배치 윈도우 확보)
  • 테넌트 단위 파티셔닝으로 분포 변화 완화
  • 핫 테넌트는 HNSW로 분리(하이브리드 전략)

“지연↓”의 핵심: 후보 수 설계와 리랭킹

DB 검색만 빠르게 해서는 RAG 전체가 빨라지지 않습니다. 하지만 DB에서 필요 이상으로 높은 리콜을 직접 달성하려고 ef_search 또는 probes를 과하게 올리면, 비용이 급격히 증가합니다.

권장 접근은 아래입니다.

  1. 1차 ANN에서 후보를 넉넉히 가져온다(예: 30~100)
  2. 2차 리랭킹으로 상위 5~10을 고른다
  3. LLM에는 상위 N개만 컨텍스트로 넣는다

이 구조에서 “1차 후보 수”는 다음과 같이 튜닝합니다.

  • 리랭커가 없으면: k를 늘려야 하므로 ANN 파라미터를 더 공격적으로 올려야 함
  • 리랭커가 있으면: ANN은 적당한 리콜만 확보하고, 리랭커로 정밀도를 올림

측정 방법: EXPLAIN (ANALYZE, BUFFERS)로 병목을 분해

튜닝은 감이 아니라 숫자로 해야 합니다.

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

여기서 확인할 포인트:

  • 인덱스를 타는지(Seq Scan이면 거의 실패)
  • Buffers에서 shared hit/read 비율(디스크 read가 많으면 콜드 캐시 영향)
  • 필터가 인덱스 탐색을 방해하는지(조건 푸시다운이 되는지)

추가로 운영에서는 다음도 함께 수집하는 게 좋습니다.

  • P50/P95/P99 쿼리 시간(테넌트별)
  • 쿼리당 반환 후보 수, 리랭킹 소요
  • autovacuum 지연, dead tuple 비율(테이블/인덱스 bloat 징후)

추천 튜닝 플로우(체크리스트)

1) 목표를 먼저 정한다

  • 예: 검색 쿼리 P95 80ms 이하, Recall@10 0.9 이상

2) HNSW 또는 IVFFlat을 선택한다

  • 리콜 우선/안정성 우선이면 HNSW
  • 메모리/대용량 우선이면 IVFFlat

3) 초기값으로 벤치마크한다

  • HNSW: m = 16, ef_construction = 128, ef_search = 40
  • IVFFlat: lists = 2000, probes = 10

4) 리콜이 부족하면 “쿼리 파라미터”부터 올린다

  • HNSW: ef_search 상향
  • IVFFlat: probes 상향

인덱스 재생성이 필요한 파라미터(m, ef_construction, lists)는 마지막에 만지는 게 비용이 적습니다.

5) 지연이 높으면 다음 순서로 줄인다

  1. 후보 k를 줄이고 리랭킹 도입/강화
  2. 필터 인덱스/파티셔닝으로 검색 범위 축소
  3. HNSW는 ef_search 하향, IVFFlat은 probes 하향
  4. 캐시/커넥션 풀/워크메모리 등 DB 레벨 튜닝

운영 팁: 멀티테넌트 RAG에서 흔한 패턴

테넌트별 데이터 크기 편차가 큰 경우

  • 작은 테넌트는 어떤 설정이든 빠르지만, 큰 테넌트가 P99를 망칩니다.
  • 해결: 큰 테넌트를 별도 파티션/테이블로 분리하고 인덱스 파라미터를 다르게 운영

업데이트가 잦은 경우

  • HNSW는 삽입이 가능하지만, 대량 업데이트/삭제가 반복되면 bloat/성능 변동이 생길 수 있습니다.
  • 해결: 배치로 재임베딩 후 교체(새 테이블에 적재 후 스왑), VACUUM/REINDEX 전략 수립

결론: “ef/probes를 올리면 된다”가 아니라, 파이프라인을 설계해야 한다

pgvector RAG 튜닝에서 지연을 낮추는 가장 흔한 실수는, ANN 인덱스 파라미터를 올려서 “DB에서만 정답 top-k를 완벽히 맞추려는 것”입니다. RAG는 애초에 2단계 구조(후보 생성 + 정밀 선택)가 잘 맞습니다.

  • HNSW는 ef_search가 지연을 좌우하므로, 리랭커 기준 후보 수에 맞춰 최소화
  • IVFFlat은 lists/probes를 세트로 보고, 분포 변화에 대비해 리인덱싱 전략 포함
  • 필터/테넌트/업데이트 패턴까지 포함해 측정하고, EXPLAIN (ANALYZE, BUFFERS)로 병목을 분해

이 과정을 거치면 “검색 지연↓”과 “검색 품질 유지”를 동시에 달성할 확률이 크게 올라갑니다.