Published on

pgvector HNSW 튜닝으로 RAG 지연 50% 줄이기

Authors

RAG 파이프라인에서 체감 지연의 대부분은 LLM 생성이 아니라 retrieval(벡터 검색) 에서 터지는 경우가 많습니다. 특히 pgvector를 쓰는 팀은 “PostgreSQL 하나로 끝내자”는 장점 때문에 빠르게 도입했다가, 데이터가 커지면서 ORDER BY embedding <-> $query LIMIT k 가 병목이 되는 순간을 맞습니다.

이 글은 pgvector의 HNSW 인덱스를 제대로 튜닝해서, 동일한 품질(또는 허용 가능한 recall 하락) 범위에서 RAG 지연을 50%까지 낮추는 방법을 정리합니다. 단순히 파라미터 값을 나열하는 것이 아니라, 어떤 증상에서 어떤 레버를 만져야 하는지와 운영 관측 포인트까지 포함합니다.

또한 HNSW 튜닝은 DB 레이어 튜닝과 함께 가야 효과가 큽니다. RDS에서 IOPS가 튀거나 슬로우 쿼리가 겹치면 체감 지연이 더 악화되므로, 필요하면 AWS RDS PostgreSQL IOPS 폭증과 Slow Query 해결도 같이 참고하세요.

전제: RAG에서 pgvector 쿼리가 느려지는 대표 패턴

다음 중 하나라도 해당하면 HNSW가 후보입니다.

  • 문서(벡터) 수가 수십만을 넘어가면서 p95 검색 지연이 급상승
  • LIMIT 5LIMIT 10 으로 작게 가져오는데도 지연이 길다
  • IVFFlat을 쓰고 있는데 lists 를 올려도 recall과 지연이 같이 흔들린다
  • 필터(테넌트, 문서 타입, 권한 등) 조건이 붙으면서 플랜이 불안정해졌다

HNSW는 그래프 기반 근사 최근접 탐색(ANN) 이라서, 인덱스만 잘 만들어두면 작은 k 에서 특히 강합니다. 대신 빌드 비용이 크고, 메모리/캐시 영향이 큽니다.

pgvector HNSW에서 실질적으로 만지는 값은 3개입니다.

m: 그래프의 연결도(메모리와 속도의 스위치)

  • 의미: 각 노드(벡터)가 유지하는 이웃 개수(대략)
  • 효과: m 이 높을수록 recall과 탐색 안정성이 좋아지지만, 인덱스 크기와 빌드/업데이트 비용이 증가
  • 경험칙
    • 768 차원 임베딩(예: text-embedding 계열)에서 보통 m=16 또는 m=24 로 시작
    • 데이터가 수백만 단위거나 recall이 특히 중요하면 m=32 도 고려

ef_construction: 인덱스 빌드 품질(빌드 시간 vs recall)

  • 의미: 인덱스 구축 시 후보를 얼마나 넓게 탐색하며 그래프를 만들지
  • 효과: 높을수록 인덱스 품질(=검색 recall)이 좋아지지만 빌드 시간이 늘어남
  • 경험칙
    • ef_construction=64 로 시작해서, recall이 부족하면 128 또는 200 으로 상향
    • 온라인으로 자주 재구축해야 한다면 너무 높게 잡지 말 것

ef_search: 쿼리 시 탐색 폭(지연 vs recall)

  • 의미: 검색 시 후보를 얼마나 넓게 탐색할지
  • 효과: 높을수록 recall이 좋아지지만 지연이 증가
  • 운영 팁
    • 가장 자주 바꾸는 값. 인덱스 재생성 없이 세션/트랜잭션 단위로 조절 가능
    • RAG에서는 보통 k=5~20 이므로, ef_search40~200 범위에서 스윕하며 p95와 정답률을 같이 본다

정리하면:

  • m, ef_construction 은 “인덱스 품질/크기”
  • ef_search 는 “실시간 지연-품질 트레이드오프”

스키마와 인덱스 생성: 최소 예제

아래는 코사인 거리 기준 예시입니다. (내적, L2도 동일 패턴)

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
  id bigserial PRIMARY KEY,
  tenant_id bigint NOT NULL,
  content text NOT NULL,
  embedding vector(768) NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- HNSW 인덱스 (cosine)
CREATE INDEX CONCURRENTLY documents_embedding_hnsw
  ON documents
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128);

-- 필터를 자주 탄다면 보조 인덱스도 중요
CREATE INDEX CONCURRENTLY documents_tenant_id_idx
  ON documents (tenant_id);

검색 쿼리 예시는 다음과 같습니다.

-- 세션 단위로 ef_search 조절
SET LOCAL hnsw.ef_search = 80;

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

주의할 점은, MDX나 JSX와 무관하게 SQL 자체는 괜찮지만, 본문에서 부등호가 노출되면 빌드가 깨질 수 있으므로 이 글에서는 연산자를 전부 코드 블록에 넣었습니다.

“지연 50%↓”가 나오는 튜닝 순서(실전 체크리스트)

HNSW 튜닝은 무작정 파라미터를 올리는 게 아니라, 측정 가능한 루프로 들어가야 합니다.

1) 기준선 측정: p50/p95, recall, DB 리소스

먼저 아래를 같은 시간대/부하에서 측정합니다.

  • 쿼리 p50/p95/p99 (애플리케이션에서 측정)
  • EXPLAIN (ANALYZE, BUFFERS) 로 실제 실행 시간과 버퍼 히트율
  • CPU 사용률, 디스크 read, IOPS, shared buffers hit ratio

IOPS가 튄다면 벡터 인덱스 자체보다 캐시 미스가 원인일 수 있습니다. 이 경우 DB 튜닝/스토리지 튜닝이 먼저입니다. 운영 이슈 관점은 AWS RDS PostgreSQL IOPS 폭증과 Slow Query 해결에서 다룬 방식과 동일하게 접근하면 됩니다.

2) ef_search 스윕: 가장 싸고 빠른 레버

인덱스를 다시 만들지 않고도 즉시 효과를 보는 방법입니다.

  • 목표: p95를 줄이되, RAG 품질이 허용 가능한지 확인
  • 방법: ef_search 를 40, 60, 80, 120, 160… 이런 식으로 올려가며 측정

예시(테스트 스크립트용):

DO $$
DECLARE
  v integer;
BEGIN
  FOREACH v IN ARRAY ARRAY[40, 60, 80, 120, 160] LOOP
    EXECUTE format('SET LOCAL hnsw.ef_search = %s', v);
    -- 여기서 대표 쿼리를 여러 번 실행해 평균/분산을 측정
  END LOOP;
END $$;

운영에서 자주 보는 패턴은 다음입니다.

  • ef_search 가 너무 낮으면: 지연은 짧지만 recall이 떨어져서 LLM이 헛소리(근거 부족)를 하거나, 재질문/재시도 로직이 늘어 전체 지연이 증가
  • ef_search 가 너무 높으면: 검색 지연이 직접 증가

“지연 50%↓”는 보통 너무 보수적으로 높게 잡아둔 ef_search 를 낮추는 것만으로도 나옵니다. 특히 초기 PoC에서 recall을 과하게 챙기려고 ef_search 를 크게 잡아두고 그대로 운영에 들어가는 경우가 많습니다.

3) m 조정: 같은 recall에서 ef_search 를 낮추기

ef_search 를 낮추면 recall이 흔들릴 때, m 을 올려서 그래프 품질을 개선하면 같은 recall을 더 낮은 ef_search 로 달성할 수 있습니다.

  • 예: m=16, ef_search=120 에서 만족 recall
  • 목표: m=24 로 올리고 ef_search=80 으로 내려서 p95를 절반 가까이 줄이기

다만 m 변경은 인덱스 재생성이 필요합니다.

DROP INDEX CONCURRENTLY IF EXISTS documents_embedding_hnsw;

CREATE INDEX CONCURRENTLY documents_embedding_hnsw
  ON documents
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 24, ef_construction = 128);

운영 팁:

  • 인덱스 크기가 커지면 캐시 적중률이 떨어질 수 있어 “오히려 느려지는” 역효과가 날 수 있습니다.
  • 따라서 m 을 올릴 때는 메모리(shared_buffers, OS page cache)와 인덱스 크기를 같이 봐야 합니다.

4) ef_construction 은 “검색 지연”보다 “품질 안정성”을 위해

ef_construction 을 올리면 검색 품질이 안정화되어, 결과적으로 ef_search 를 낮춰도 recall이 유지되는 경우가 있습니다. 하지만 효과 대비 비용(빌드 시간)이 크기 때문에 마지막에 만지는 편이 좋습니다.

권장 흐름:

  1. ef_search 로 먼저 최적점 찾기
  2. m 으로 그래프 품질 개선
  3. 그래도 부족하면 ef_construction 상향

필터가 있는 RAG에서 HNSW가 느려지는 이유와 해결

실전 RAG는 보통 다음 조건이 붙습니다.

  • tenant_id
  • 문서 타입, 최신 버전, 권한, 언어
  • time window

문제는 “벡터 검색 + 필터”에서 플래너가 애매한 선택을 할 수 있다는 점입니다.

전략 A: 필터 카디널리티를 낮추기(테넌트 분리/파티셔닝)

테넌트별 데이터가 크고 쿼리가 항상 테넌트 조건을 탄다면, 다음이 효과적입니다.

  • 테이블 파티셔닝(예: tenant_id range/list)
  • 테넌트별 별도 테이블 또는 별도 DB(운영 복잡도 증가)

파티션마다 HNSW 인덱스를 가지면 탐색 공간이 줄어 p95가 크게 줄 수 있습니다.

전략 B: “후보를 넉넉히 뽑고 필터링”의 역전 현상 주의

일부 시스템은 먼저 ANN으로 후보를 뽑고 애플리케이션에서 필터링합니다. 하지만 필터링으로 대부분이 탈락하면, 원하는 k 를 채우기 위해 더 큰 ef_search 가 필요해져 지연이 늘어납니다.

가능하면 SQL 레벨에서 필터를 함께 적용하고, 카디널리티가 큰 필터는 데이터 모델로 해결하는 편이 낫습니다.

RAG 품질 지표: recall만 보지 말고 “정답률”로 보정

HNSW 튜닝에서 흔한 실수는 “벡터 recall”만 보고 결정을 내리는 것입니다. RAG는 최종적으로 LLM이 답을 잘 하느냐가 중요합니다.

권장 지표:

  • retrieval recall@k (정답 문서가 top-k에 포함되는 비율)
  • MRR, nDCG 같은 랭킹 지표
  • RAG 정답률(골든 QA 세트로 평가)
  • hallucination rate(근거 없는 답변 비율)

검색 지연을 줄였는데 정답률이 떨어져서 재시도/재질문이 늘면, 전체 지연은 오히려 증가합니다.

운영 관측: EXPLAIN (ANALYZE, BUFFERS)로 확인할 것

대표 쿼리 하나를 잡고 아래를 확인하세요.

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

체크 포인트:

  • 실행 시간이 안정적인가(p95가 튀지 않는가)
  • BUFFERS 에서 shared hit vs read 비율이 어떤가
  • read가 많다면: 인덱스/테이블이 메모리에 못 올라오는 상태일 수 있음

DB 레벨에서 락/경합이 겹치면 지연이 더 악화됩니다. RAG 인퍼런스 트래픽이 올라가면서 배치 작업까지 같이 돌면 데드락이나 lock wait이 생길 수 있으니, 필요하면 PostgreSQL 데드락(40P01) 원인·해결 9단계도 같이 점검하는 게 안전합니다.

“50% 지연 감소”를 만드는 전형적인 조합(예시)

아래는 현장에서 자주 나오는 개선 시나리오입니다.

  • 초기 상태

    • m=16, ef_construction=64
    • 운영에서 ef_search=200 고정
    • p95 검색 지연이 높고 변동성 큼
  • 개선

    1. 골든셋으로 RAG 정답률을 측정해 “필요한 품질”의 하한을 정의
    2. ef_search 를 200에서 80까지 단계적으로 낮추며 정답률이 유지되는 지점 찾기
    3. 정답률이 흔들리면 m 을 24로 올리고 인덱스 재생성
    4. 그 후 ef_search 를 60~100 범위에서 재조정

이 조합은 CPU 사용률을 낮추고 tail latency를 줄이며, 특히 동시 요청이 늘 때 효과가 큽니다.

자주 묻는 질문

IVFFlat 대신 HNSW를 언제 선택하나

  • 작은 k 로 빠르게 top-k가 필요하고, 지연이 중요하면 HNSW가 유리한 경우가 많습니다.
  • 반면 데이터가 매우 크고(수천만 이상), 빌드/메모리 비용이 부담이면 IVFFlat이 운영상 더 단순할 수 있습니다.

(다른 벡터 DB의 튜닝 감각이 궁금하면 Milvus IVF_PQ 튜닝으로 recall 올리고 지연 줄이기도 비교 관점에서 도움이 됩니다.)

HNSW 인덱스는 업데이트/삭제가 많아도 괜찮나

PostgreSQL 특성상 업데이트/삭제가 누적되면 bloat와 vacuum 이슈가 생길 수 있습니다. 벡터 테이블이 append-only에 가깝다면 유리하고, 빈번한 수정이 있다면 파티셔닝/주기적 재구축 전략을 같이 가져가야 합니다.

마무리: 튜닝은 ef_search부터, 그리고 캐시까지

pgvector HNSW로 RAG 지연을 크게 줄이는 핵심은 “복잡한 설정”이 아니라 다음 순서입니다.

  1. ef_search 를 스윕하며 지연-품질 최적점을 찾기
  2. 품질이 부족하면 m 을 올려 같은 품질을 더 낮은 ef_search 로 달성하기
  3. ef_construction 은 마지막에 품질 안정화를 위해 조정하기
  4. 인덱스 크기와 캐시 적중률을 같이 보며 IOPS 폭증을 막기

이 루프를 제대로 돌리면, retrieval p95를 절반 수준으로 낮추는 것은 충분히 현실적인 목표입니다.