Published on

pgvector RAG 지연↓ - IVF/HNSW 튜닝 실전

Authors

RAG를 pgvector로 운영하다 보면 초반에는 간단합니다. vector 컬럼 하나 만들고, 코사인 거리로 ORDER BY ... LIMIT k만 해도 잘 돌아갑니다. 문제는 트래픽이 붙거나(동시성), 문서가 쌓이거나(백만 단위), 필터 조건이 늘어날 때(테넌트, 권한, 시간 범위)입니다. 이때 지연의 대부분은 벡터 유사도 검색 단계에서 발생하고, 해법의 대부분은 인덱스 선택과 파라미터 튜닝에 있습니다.

이 글은 pgvector의 대표적인 근사 최근접(ANN) 인덱스인 IVFFlatHNSW를 RAG 관점에서 어떻게 선택하고, 어떤 순서로 튜닝하면 지연을 체계적으로 낮출 수 있는지 다룹니다.

먼저: 지연을 “측정 가능한 형태”로 만들기

튜닝은 감으로 하면 끝이 없습니다. 최소한 아래 3가지는 숫자로 확보하세요.

  1. p50/p95/p99 지연: RAG는 꼬리 지연이 품질에 직결됩니다.
  2. 리콜(Recall) 목표: 예를 들어 “정확 검색 대비 top-10에서 0.95 이상” 같은 기준.
  3. 검색 조건의 현실: tenant_id 같은 필터가 항상 붙는지, QPS는 얼마인지, k는 보통 몇인지.

Postgres에서는 EXPLAIN (ANALYZE, BUFFERS)로 계획과 실제 시간을 봅니다. 인덱스가 제대로 타는지, 필터가 인덱스 전/후에 적용되는지, 버퍼 히트율이 어떤지 확인합니다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content
FROM documents
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
  • <=>는 코사인 거리(설정에 따라) 연산자입니다. 본문에 부등호가 포함되므로 위처럼 반드시 코드 블록 안에서만 사용하세요.

IVFFlat vs HNSW: 어떤 인덱스를 선택할까

두 인덱스는 “지연을 줄이기 위한 근사 검색”이라는 목표는 같지만, 운영 특성이 다릅니다.

IVFFlat(IVF) 특징

  • 빌드가 비교적 단순하고, 메모리 사용이 예측 가능합니다.
  • 검색 시 probes로 “얼마나 많은 클러스터를 훑을지”를 조절합니다.
  • 데이터가 커질수록 lists(클러스터 수) 설계가 중요해집니다.
  • 대개 대량 데이터에서 안정적인 성능을 내기 쉽습니다.

HNSW 특징

  • 보통 낮은 지연과 높은 리콜을 동시에 달성하기 쉽습니다.
  • ef_search로 검색 품질/지연을 조절하고, m, ef_construction으로 인덱스 품질과 빌드 비용을 조절합니다.
  • 인덱스가 커지고 업데이트가 잦으면 메모리/유지 비용이 부담될 수 있습니다.

실전 선택 가이드(빠른 결론)

  • 읽기 비중이 높고(p95가 중요), 리콜을 높게 유지해야 한다: HNSW부터 고려
  • 데이터가 매우 크고, 메모리/저장 비용을 예측 가능하게 가져가고 싶다: IVF부터 고려
  • 필터가 강하게 걸려 후보군이 작아진다: IVF가 생각보다 불리할 수 있고, HNSW가 유리한 경우가 많습니다(단, 필터 적용 방식에 따라 다름)

스키마와 기본 쿼리: RAG용 테이블 구성

RAG에서는 최소한 id, content, metadata, embedding, 그리고 필터용 컬럼(예: tenant_id)이 필요합니다.

CREATE TABLE documents (
  id BIGSERIAL PRIMARY KEY,
  tenant_id BIGINT NOT NULL,
  content TEXT NOT NULL,
  metadata JSONB,
  embedding vector(1536) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX documents_tenant_id_idx ON documents (tenant_id);

여기서 중요한 포인트는 “벡터 인덱스만 만들면 끝”이 아니라, 필터 컬럼 인덱스도 같이 설계해야 한다는 점입니다.

IVFFlat 튜닝: listsprobes의 균형

IVF는 데이터를 여러 리스트(클러스터)로 나눠서, 검색 시 일부 리스트만 탐색합니다.

인덱스 생성

-- 코사인 거리 기준 예시
CREATE INDEX documents_embedding_ivf_idx
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 200);

ANALYZE documents;
  • lists는 클러스터 수입니다. 너무 작으면 후보군이 커져 느려지고, 너무 크면 각 리스트가 너무 잘게 쪼개져 probes를 올려야 리콜이 나옵니다.

검색 시 probes 조절

probes는 “몇 개 리스트를 볼지”입니다. 일반적으로 probes를 올리면 리콜은 올라가고 지연도 올라갑니다.

SET ivfflat.probes = 10;

SELECT id, content
FROM documents
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;

경험칙(정답은 아니지만 출발점)

  • lists: 대략 sqrt(N) 근처에서 시작하는 팀이 많습니다(예: N = 1,000,000이면 lists를 1000 근처로 시작).
  • probes: 1부터 시작해 리콜 목표에 도달할 때까지 올립니다.

튜닝 순서(추천)

  1. lists를 합리적인 값으로 고정(너무 작지 않게)
  2. probes로 리콜을 맞춤
  3. 그래도 느리면 lists를 늘리고 probes를 낮춰 재탐색

IVF는 “리콜을 맞추기 위해 probes를 과하게 올리면” 결국 거의 전체 탐색에 가까워져 이점이 사라집니다. 이 구간에 들어갔다면 HNSW로 전환을 고려할 타이밍입니다.

HNSW는 그래프 기반 ANN입니다. 튜닝은 크게 두 축입니다.

  • 인덱스 품질/크기/빌드 비용: m, ef_construction
  • 검색 품질/지연: ef_search

인덱스 생성

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

ANALYZE documents;
  • m: 그래프에서 각 노드가 유지하는 연결 수에 가깝습니다. 올리면 리콜이 좋아지는 경향이 있지만 인덱스가 커지고 빌드/업데이트 비용이 증가합니다.
  • ef_construction: 인덱스 빌드 품질에 영향. 올리면 빌드는 느려지지만 검색 품질이 좋아집니다.

검색 시 ef_search 조절

SET hnsw.ef_search = 40;

SELECT id, content
FROM documents
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;
  • ef_search를 올리면 리콜이 올라가고 지연도 증가합니다.
  • 실무에서는 ef_search를 “리콜 SLA를 맞추는 다이얼”로 두고, 트래픽 피크 때는 낮추는 식의 운영도 합니다(품질과 비용의 트레이드오프를 명시적으로 관리).

RAG에서 특히 중요한 함정: 필터와 ANN의 결합

RAG 검색은 보통 tenant_id, document_type, created_at 같은 조건이 붙습니다. 이때 흔한 문제는 다음입니다.

  • ANN 인덱스는 벡터 기준으로 후보를 찾는데, 필터가 강하면 후보가 필터에서 대거 탈락합니다.
  • 그 결과 원하는 k개를 채우기 위해 훨씬 더 많은 후보를 봐야 하며, 지연이 튀거나 리콜이 떨어집니다.

대응 전략 1: 필터 선택도를 먼저 파악

예를 들어 테넌트별 데이터가 매우 작다면, 아예 테넌트별 파티셔닝(또는 테넌트별 테이블/인덱스)도 검토할 수 있습니다.

-- 월 단위 파티셔닝 예시(개념)
-- 실제 운영에서는 테넌트/시간 축 중 무엇이 더 중요한지에 따라 설계

대응 전략 2: 후보 수를 늘리는 대신 비용을 통제

  • IVF: probes를 올리는 대신 lists를 재조정
  • HNSW: ef_search를 올리되, p95가 무너지지 않도록 상한을 둠

대응 전략 3: 2단계 검색(거친 후보군 + 정밀 재정렬)

ANN으로 top-k가 아니라 top-k'(예: 100)를 뽑고, 그 안에서 추가 스코어링(BM25, 최신성 가중치, 권한)을 적용해 최종 top-k를 만듭니다.

-- 1) ANN으로 후보 100개
WITH candidates AS (
  SELECT id, content, metadata, (embedding <=> $1) AS dist
  FROM documents
  WHERE tenant_id = 42
  ORDER BY embedding <=> $1
  LIMIT 100
)
-- 2) 후보 내에서 재정렬(예: 최신성 가중)
SELECT id, content
FROM candidates
ORDER BY dist ASC
LIMIT 10;

운영에서 지연을 더 줄이는 실전 팁

1) 인덱스 빌드/리빌드 전략을 계획

임베딩 드리프트나 모델 교체로 재색인이 필요할 수 있습니다. 벡터 DB에서도 동일한 문제가 있고, pgvector도 예외가 아닙니다. 운영 관점의 재색인/드리프트 이슈는 아래 글의 접근이 그대로 도움이 됩니다.

2) 백오프/큐로 피크 트래픽을 완화

RAG는 보통 “임베딩 생성 + 검색 + LLM 호출”의 합성 지연입니다. 검색이 빨라져도 상류/하류가 병목이면 p95는 그대로입니다. 특히 외부 API 호출이 섞이면 레이트리밋이 지연을 폭발시킵니다.

3) 캐시 무효화로 “느린 재계산”을 줄이기

RAG 결과를 캐시하는 경우(질문 정규화, 쿼리 임베딩 캐시, top-k 결과 캐시) 캐시 무효화가 꼬이면 오히려 불필요한 재검색이 늘어납니다. Next.js 기반이라면 태그 기반 무효화 전략을 함께 보세요.

튜닝 체크리스트: “지연↓”을 재현 가능하게

아래 순서대로 하면 시행착오를 크게 줄일 수 있습니다.

  1. 기준선 확보: 정확 검색(브루트포스) 대비 리콜, p95 지연, QPS
  2. 인덱스 선택: IVF 또는 HNSW를 워크로드에 맞게 결정
  3. 파라미터 1차 세팅
    • IVF: lists 고정 후 probes로 리콜 맞추기
    • HNSW: m, ef_construction은 보수적으로 시작 후 ef_search로 리콜 맞추기
  4. 필터 영향 측정: 필터가 강할수록 후보 탈락이 늘어나는지 확인
  5. 2단계 검색 도입 여부 결정: ANN 후보 확대 + 재정렬
  6. 운영 상한 설정: probes/ef_search 최대치, 타임아웃, 리트라이 정책
  7. 재색인 플랜: 모델 교체/드리프트/스키마 변경 시 롤링 재색인

결론: IVF/HNSW는 “다이얼”이고, 목표는 p95 안정화

pgvector RAG에서 지연을 줄이는 핵심은 ANN 인덱스를 “켜는 것”이 아니라, 리콜 목표와 필터 현실을 반영해 다이얼을 조정하는 것입니다.

  • IVF는 listsprobes의 균형이 전부이고,
  • HNSW는 ef_search를 중심으로, 필요하면 mef_construction까지 조정합니다.

마지막으로, RAG의 사용자 체감은 p50이 아니라 p95/p99에서 결정됩니다. 튜닝 결과를 항상 꼬리 지연 기준으로 검증하고, 피크 트래픽에서 파라미터 상한이 어떤 영향을 주는지까지 포함해 운영 설계를 마무리하면 “지연↓”을 안정적으로 달성할 수 있습니다.