Published on

PostgreSQL+pgvector RAG 인덱스 튜닝 - HNSW vs IVF

Authors

RAG(Retrieval-Augmented Generation)를 PostgreSQL 위에서 운영하면, 애플리케이션/데이터 파이프라인이 단순해지고 트랜잭션 일관성도 얻을 수 있습니다. 하지만 임베딩 벡터 검색은 전통적인 B-Tree 튜닝과 결이 다릅니다. 특히 pgvector의 근사 최근접(ANN) 인덱스인 HNSWIVF(IVFFlat) 중 무엇을 선택하고, 어떤 파라미터를 어떻게 튜닝하느냐에 따라 지연시간, 정확도(recall), 비용(메모리/디스크), 운영 난이도가 크게 갈립니다.

이 글은 다음을 목표로 합니다.

  • RAG에서 자주 쓰는 쿼리 패턴(Top-K + 필터) 기준으로 HNSWIVF를 비교
  • 실무에서 흔히 겪는 “느린데 왜 느린지 모름”을 줄이기 위한 측정/추적 루틴 제시
  • 인덱스 생성/튜닝 SQL과 운영 팁을 코드로 제공

운영 중 쿼리 병목을 먼저 잡고 싶다면, 벡터 검색도 결국 SQL이므로 통합 관측이 중요합니다. 병목 추적 루틴은 PostgreSQL 쿼리 폭주? pg_stat_statements로 병목 추적 글의 방식이 그대로 적용됩니다.

RAG 벡터 검색의 전형적인 쿼리 형태

RAG 검색은 보통 아래 형태를 가집니다.

  • 입력 쿼리를 임베딩 q로 변환
  • q와 문서 청크 임베딩 사이의 거리(코사인/내적/L2)를 기준으로 Top-K를 찾음
  • 동시에 테넌트, 문서 타입, 권한, 시간 범위 같은 메타데이터 필터가 붙음

예시 스키마와 쿼리입니다.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE rag_chunk (
  id bigserial PRIMARY KEY,
  tenant_id text 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()
);

-- 코사인 거리 기준 Top-K (필터 포함)
SELECT id, doc_id, chunk_no, content
FROM rag_chunk
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;

여기서 embedding <=> $2는 코사인 거리 연산자(설정/버전에 따라 다를 수 있음)로, 인덱스 타입에 따라 실행 계획이 크게 달라집니다.

HNSW vs IVF: 한 장으로 보는 선택 기준

HNSW의 특징

  • 장점
    • 높은 recall을 비교적 낮은 튜닝 비용으로 얻기 쉬움
    • 검색 지연시간이 안정적인 편(특히 Top-K가 작을 때)
    • “일단 빠르게 만들고 운영하면서 조금씩 조정”이 가능
  • 단점
    • 메모리 사용량이 커질 수 있음(그래프 구조)
    • 대량 삽입/갱신이 많은 워크로드에서는 인덱스 빌드/유지 비용이 부담
    • 필터가 강하게 걸리면(테넌트별 데이터가 작아지는 경우) 효율이 떨어질 수 있음

IVF(IVFFlat)의 특징

  • 장점
    • 메모리 압박이 상대적으로 덜하고 디스크 친화적
    • 데이터가 매우 크고(수천만 이상), 필터로 후보군이 줄어드는 패턴에서 설계가 잘 맞으면 비용 효율적
    • listsprobes로 성능/정확도 곡선을 명확히 컨트롤 가능
  • 단점
    • 인덱스 품질이 ANALYZE/학습(클러스터링)과 파라미터에 민감
    • 잘못 튜닝하면 recall이 급격히 떨어지거나, 반대로 probes를 올리면 지연이 급격히 증가
    • 운영 중 데이터 분포가 바뀌면 재빌드가 필요해질 수 있음

정리하면,

  • “높은 recall이 중요하고, 운영 단순성이 최우선”이면 HNSW가 출발점으로 좋습니다.
  • “데이터가 매우 크고 비용을 정교하게 통제해야 하며, 재빌드/튜닝을 감수할 수 있다”면 IVF가 강력합니다.

인덱스 생성 SQL: HNSW와 IVF

HNSW 인덱스 생성

-- 코사인 거리 기준 예시
CREATE INDEX rag_chunk_embedding_hnsw
ON rag_chunk
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);

ANALYZE rag_chunk;
  • m: 그래프에서 각 노드가 가지는 연결 수(대략 메모리/정확도/빌드 비용에 영향)
  • ef_construction: 빌드 시 탐색 폭(클수록 빌드 느리지만 품질↑)

IVF(IVFFlat) 인덱스 생성

CREATE INDEX rag_chunk_embedding_ivf
ON rag_chunk
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 2000);

ANALYZE rag_chunk;
  • lists: 클러스터(버킷) 수. 일반적으로 데이터가 클수록 늘립니다.
  • IVF는 ANALYZE가 특히 중요합니다. 통계가 부정확하면 계획/품질이 흔들릴 수 있습니다.

런타임 튜닝: ef_search vs probes

HNSW는 쿼리 시 탐색 폭을 ef_search로 조절합니다.

SET LOCAL hnsw.ef_search = 80;

SELECT id, doc_id
FROM rag_chunk
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;
  • ef_search를 올리면 recall이 올라가지만 지연시간도 증가합니다.
  • 운영에서는 “기본값 + 특정 요청만 상향” 전략이 유용합니다. 예를 들어, 재랭킹 전 후보를 넉넉히 뽑는 요청만 ef_search를 올립니다.

IVF 런타임: probes

IVF는 쿼리 시 몇 개의 리스트를 탐색할지 probes로 제어합니다.

SET LOCAL ivfflat.probes = 10;

SELECT id, doc_id
FROM rag_chunk
WHERE tenant_id = $1
ORDER BY embedding <=> $2
LIMIT 10;
  • probes가 낮으면 빠르지만 recall이 떨어질 수 있습니다.
  • lists가 큰데 probes가 너무 낮으면 “거의 랜덤”처럼 동작할 때가 있습니다.

필터(tenant_id 등)와 ANN의 충돌: 실무에서 제일 많이 터지는 지점

RAG 운영에서 흔한 패턴은 tenant_id 같은 강한 필터입니다. 문제는 ANN 인덱스가 “전체 공간에서 가까운 벡터”를 찾는 구조라서, 필터로 후보가 크게 줄면 인덱스가 기대만큼 효율적이지 않을 수 있다는 점입니다.

대응 전략 1: 파티셔닝(테넌트/기간)

테넌트별 데이터가 충분히 크고 테넌트 수가 제한적이면, 테이블 파티셔닝이 강력합니다.

CREATE TABLE rag_chunk (
  id bigserial,
  tenant_id text 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()
) PARTITION BY LIST (tenant_id);

CREATE TABLE rag_chunk_tenant_a PARTITION OF rag_chunk FOR VALUES IN ('tenant-a');

CREATE INDEX rag_chunk_tenant_a_hnsw
ON rag_chunk_tenant_a
USING hnsw (embedding vector_cosine_ops);
  • 파티션 프루닝으로 검색 범위를 줄이면서 ANN 인덱스 효율을 지킬 수 있습니다.
  • 단, 파티션 수가 과도하면 DDL/오토베큠/통계 관리가 복잡해집니다.

대응 전략 2: 2단계 검색(후보 확대 + 재랭킹)

  • 1단계: ANN으로 Top-N(예: 50~200) 후보를 빠르게 확보
  • 2단계: 필터/정확한 거리 계산/추가 스코어링으로 Top-K 결정

이때 1단계에서 ef_search 또는 probes를 상황에 따라 올리는 식으로 제어합니다.

성능 측정: EXPLAIN (ANALYZE, BUFFERS)로 확인할 것

튜닝은 반드시 측정 루프가 있어야 합니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM rag_chunk
WHERE tenant_id = 'tenant-a'
ORDER BY embedding <=> $1
LIMIT 10;

체크 포인트:

  • 인덱스를 타는지(플랜에서 Index Scan/Bitmap/Seq Scan 여부)
  • BUFFERS에서 shared hit vs read 비율(캐시 의존도)
  • 실행 시간이 “가끔만 튀는지” 또는 “항상 느린지”(p95/p99를 봐야 함)

운영에서는 개별 쿼리의 EXPLAIN만으로 부족합니다. 특정 기간 동안 어떤 쿼리가 가장 시간을 먹는지, 호출 수가 폭증했는지를 pg_stat_statements로 같이 봐야 합니다. 이 루틴은 앞서 언급한 내부 글(PostgreSQL 쿼리 폭주? pg_stat_statements로 병목 추적)을 참고해 그대로 적용하면 됩니다.

튜닝 가이드: 현실적인 시작점

HNSW 추천 시작점

  • m = 16 또는 m = 24
  • ef_construction = 100~400
  • 런타임 hnsw.ef_search = 40~120 범위에서 p95 지연과 recall을 같이 보고 결정

운영 팁:

  • 데이터가 커질수록 인덱스 빌드 시간이 길어집니다. 배치 적재 후 인덱스 생성(또는 CONCURRENTLY) 전략을 검토하세요.
  • 대량 업데이트가 잦다면, “새 테이블에 적재 후 스왑” 같은 운영 패턴이 더 단순할 수 있습니다.

IVF 추천 시작점

  • lists는 데이터 건수 N에 대해 대략 sqrt(N) 근처를 출발점으로 두는 경험칙이 자주 쓰입니다(정답은 아니며 분포에 좌우됨).
  • 런타임 ivfflat.probes1~30 사이에서 단계적으로 올리며 측정

운영 팁:

  • IVF는 ANALYZE가 중요합니다. 적재 직후 통계가 없으면 성능이 흔들릴 수 있으니, 적재 파이프라인에 ANALYZE를 포함하세요.
  • 데이터 분포가 바뀌면 IVF 품질이 악화될 수 있습니다. 분기별/월별로 재빌드가 필요한지 지표(지연, recall)를 잡아두는 편이 안전합니다.

메모리/캐시 관점: “DB가 느린 게 아니라 디스크를 읽고 있다”

벡터 검색은 랜덤 액세스 성격이 강해서 캐시 적중률이 성능을 좌우하는 경우가 많습니다.

  • HNSW는 구조적으로 메모리를 더 쓰는 편이라, 워킹셋이 RAM에 올라오면 매우 빠르지만 RAM이 부족하면 급격히 흔들릴 수 있습니다.
  • IVF는 디스크 친화적일 수 있지만, probes를 올려 많은 리스트를 읽기 시작하면 결국 I/O가 늘어납니다.

따라서 튜닝은 파라미터만이 아니라 “인스턴스 메모리”와 “shared_buffers/OS 캐시”까지 포함한 총체적 문제입니다.

운영 체크리스트: 장애를 줄이는 습관

  • 인덱스 변경 전후로 같은 쿼리를 고정된 조건에서 비교(동일 LIMIT, 동일 필터, 동일 임베딩 샘플)
  • p50이 아니라 p95/p99를 기준으로 판단
  • recall을 측정할 샘플셋을 준비(정답 Top-K를 brute force로 계산해 비교)
  • 쿼리 폭주/병목은 pg_stat_statements로 상시 관측
  • 배포 중 커넥션 고갈이 동반되면 애플리케이션 스레딩 모델도 점검(예: Spring Boot 3 가상스레드로 DB 커넥션 고갈 막기)

결론: 어떤 선택이 “정답”인가

  • 빠르게 안정적인 RAG 검색을 만들고, 높은 recall을 비교적 쉽게 얻고 싶다면 HNSW가 좋은 기본값입니다.
  • 데이터가 매우 크고 비용/성능을 정교하게 통제해야 하며, 재빌드와 튜닝을 운영 프로세스에 포함할 수 있다면 IVF가 강력한 선택지입니다.

실무에서는 “HNSW로 먼저 안정화 → 비용 압박/규모 확장 시 IVF를 검토” 흐름이 가장 실패 확률이 낮습니다. 무엇을 선택하든, EXPLAIN (ANALYZE, BUFFERS) + pg_stat_statements 기반의 측정 루프를 먼저 만들면 튜닝이 감이 아니라 엔지니어링이 됩니다.