Published on

RAG 벡터DB 비용 70% 절감 - PQ·HNSW 튜닝

Authors

RAG 시스템에서 비용이 가장 빨리 불어나는 지점은 의외로 LLM 토큰이 아니라 벡터DB 인프라인 경우가 많습니다. 문서가 늘수록 인덱스는 커지고, 메모리 상주형 ANN 인덱스(HNSW 등)는 노드 수를 그대로 끌어올립니다. 이 글은 PQ(Product Quantization)로 벡터를 압축하고, HNSW 파라미터를 튜닝해 검색 지연과 리콜을 관리하면서 총비용(TCO)을 70% 수준까지 절감하는 접근을 실전 관점에서 정리합니다.

핵심 메시지는 단순합니다.

  • 비용을 좌우하는 것은 dimN(문서 수)보다도 인덱스 메모리 상주량쿼리당 CPU입니다.
  • PQ는 메모리를 크게 줄이고, HNSW 튜닝은 CPU를 줄입니다.
  • “정확도”는 한 숫자로 끝나지 않습니다. RAG에서는 Recall@k 뿐 아니라 **다운스트림(정답률, 근거 적합성)**까지 같이 봐야 합니다.

왜 벡터DB가 비싸지는가: 비용 구성부터 쪼개기

벡터DB 비용은 대략 아래로 분해됩니다.

  1. 벡터 저장 비용: N * dim * bytes (FP32면 4바이트)
  2. 인덱스 오버헤드: HNSW 그래프의 링크, 레벨 구조, 메타데이터
  3. 메모리 상주 비용: HNSW는 성능상 메모리 적중률이 중요해 RAM이 커짐
  4. 쿼리 CPU 비용: 탐색 폭(efSearch)과 그래프 복잡도(M)에 비례
  5. 리빌드/업서트 비용: 인덱스 업데이트, 컴팩션, 리밸런싱

여기서 70% 절감이 나오는 지점은 보통 다음 조합입니다.

  • PQ로 벡터 메모리 자체를 4배~16배 줄임
  • HNSW M, efConstruction, efSearch를 재조정해 CPU와 p99 지연을 낮춤
  • 그 결과 노드 수를 줄이거나, 동일 노드로 더 많은 컬렉션을 수용

목표 지표 정의: “리콜”만 보면 실패한다

튜닝을 시작하기 전에 아래 지표를 고정하세요.

  • 검색 품질
    • Recall@k (예: k=10, k=20)
    • nDCG@k 또는 MRR (순위 품질)
  • RAG 품질(다운스트림)
    • 정답률(EM/F1), 혹은 LLM 평가 점수
    • 근거 적합성(grounding), 인용 문서 정확도
  • 성능/비용
    • p50/p95/p99 latency
    • QPS당 CPU 사용량
    • 인덱스 메모리 사용량(GB)

운영 환경에서 429 재시도/백오프가 필요한 경우, 벡터DB가 느려져 상위 레이어에서 재시도가 폭증하며 비용이 2차로 튈 수 있습니다. API 재시도 설계는 별도로 정리한 글도 함께 참고하면 좋습니다: OpenAI 429 RateLimitError 재시도·백오프 구현

PQ(Product Quantization)로 메모리 4~16배 줄이기

PQ가 뭘 줄이는가

PQ는 벡터를 여러 서브벡터로 쪼갠 뒤 각 서브벡터를 코드북 인덱스(바이트)로 저장합니다.

  • 원본: FP32 dim=1536이면 벡터 1개당 1536*4 = 6144바이트
  • PQ 예시: m=96 서브벡터, 각 서브벡터를 8비트 코드로 저장하면 96바이트

물론 실제 시스템은 원본 벡터를 일부 보관하거나(리랭킹/재검증), OPQ/IVF를 섞기도 해서 단순 계산과 다를 수 있습니다. 하지만 “벡터 본체”가 차지하는 메모리 비중이 크다면 PQ는 가장 강력한 비용 절감 레버입니다.

PQ 파라미터 감 잡기: m 과 코드 크기

일반적으로 다음이 트레이드오프입니다.

  • m(서브벡터 개수) 증가: 정확도 상승 경향, 저장 크기 증가
  • 코드 크기(보통 8bit) 증가: 정확도 상승, 저장 크기 증가

실무에서 자주 쓰는 출발점:

  • 임베딩 dim=768: m=48 또는 m=64
  • 임베딩 dim=1536: m=96 또는 m=128
  • 코드 크기: 8bit(256 centroid)

PQ 도입 시 흔한 함정 3가지

  1. 거리 왜곡: 코사인 유사도를 쓰는데 PQ가 L2 기반으로 학습되면 품질이 흔들릴 수 있습니다. 구현/엔진별로 “내부적으로 정규화 후 L2”로 바꿔치기하는지 확인하세요.
  2. 롱테일 쿼리 붕괴: 헤드 쿼리는 괜찮은데 희귀 표현에서 Recall이 급락하는 경우가 있습니다. 평가셋을 헤드/롱테일로 분리하세요.
  3. 리랭킹 부재: PQ는 근사이므로 topK를 넉넉히 뽑고(예: 50~200) 원본 벡터 또는 크로스 인코더로 리랭킹하면 품질 손실을 크게 줄일 수 있습니다.

HNSW 튜닝으로 CPU와 지연을 줄이기

HNSW는 “그래프 기반 근사 최근접 탐색”입니다. 비용 관점에서 중요한 파라미터는 아래 3개입니다.

  • M: 노드당 링크 수(그래프 밀도)
  • efConstruction: 인덱스 구축 품질(빌드 시간/메모리/정확도)
  • efSearch: 검색 시 후보 탐색 폭(정확도/지연)

M 튜닝: 메모리와 정확도의 스위트 스팟

  • M이 커지면 그래프가 촘촘해져 Recall은 오르지만
  • 링크 저장과 탐색 비용이 늘어 메모리/CPU가 증가합니다.

실전 가이드:

  • 텍스트 임베딩(768~1536)에서 출발점으로 M=16 또는 M=24
  • 비용이 너무 비싸면 M을 24에서 16으로 내리고, 손실된 Recall은 efSearch로 보정
  • 반대로 PQ로 거리 정보가 거칠어졌다면 M을 약간 올려 그래프 품질을 보완하는 경우도 있습니다

efSearch 튜닝: 쿼리당 CPU를 직접 제어

efSearch는 “얼마나 넓게 찾아볼지”입니다. 비용 절감에서 가장 즉각적인 손잡이입니다.

  • efSearch 낮춤: 지연/CPU 절감, Recall 하락
  • efSearch 높임: Recall 상승, 지연/CPU 증가

운영 팁:

  • 쿼리 유형별로 efSearch를 다르게 주는 “적응형”이 효과적입니다.
    • 예: 짧은 키워드성 쿼리는 efSearch를 높이고
    • 명확한 긴 쿼리는 efSearch를 낮춰도 품질이 유지되는 경우가 많습니다

efConstruction: 빌드 비용과 운영 비용의 균형

efConstruction은 인덱스 품질에 영향을 주지만, 운영 중 비용(쿼리 CPU)에도 간접적으로 영향을 줍니다. 빌드 품질이 낮으면 같은 Recall을 얻기 위해 efSearch를 더 올려야 해서 운영비가 증가합니다.

  • 대규모 컬렉션이면 efConstruction을 너무 낮추지 마세요.
  • 배치 빌드가 가능하면 빌드 비용을 조금 더 쓰고 운영 비용을 줄이는 편이 TCO에 유리한 경우가 많습니다.

“70% 절감”이 나오는 전형적인 설계 패턴

아래는 자주 재현되는 패턴입니다.

  1. 원본 FP32 벡터 + HNSW 기본값으로 운영
  2. PQ 도입으로 벡터 메모리 대폭 감소
  3. 메모리가 줄어든 만큼 노드 수 축소(또는 동일 노드에 더 많은 샤드 수용)
  4. PQ로 인한 Recall 손실을 efSearch 증가로 보정하되
  5. M을 조정해 efSearch를 과도하게 올리지 않게 균형

결과적으로:

  • RAM 기반 노드 수가 줄어 월 비용이 크게 감소
  • CPU도 efSearch 최적화로 줄어들어 autoscaling 규모가 감소

측정 방법: 오프라인+온라인을 같이 돌려라

오프라인 벤치마크(필수)

  • 쿼리셋: 프로덕션 로그에서 샘플링(헤드/롱테일 포함)
  • 정답셋: 클릭/다운스트림 정답, 또는 고품질 라벨
  • 비교 대상: “정확 검색(브루트포스)” 또는 “가장 품질 좋은 설정”

지표:

  • Recall@10, Recall@20
  • p95/p99 latency
  • 인덱스 크기(디스크/메모리)

온라인 A/B(권장)

RAG는 검색이 좋아져도 LLM이 답을 못하면 의미가 없습니다.

  • A/B에서 봐야 할 것
    • 사용자 만족 지표(클릭, 재질문율)
    • 근거 인용 정확도
    • 토큰 비용 변화(검색 품질이 나빠지면 컨텍스트를 더 넣게 되어 토큰이 늘 수 있음)

실전 코드 예제: FAISS로 PQ+HNSW 구성하기

아래 예시는 로컬 벤치마크용으로 많이 쓰는 FAISS 구성입니다. 엔진/서비스형 벡터DB마다 옵션 이름은 다르지만 개념은 동일합니다.

import faiss
import numpy as np

# 예시 데이터
N = 200_000
dim = 1536
np.random.seed(7)
xb = np.random.randn(N, dim).astype('float32')
faiss.normalize_L2(xb)  # 코사인 유사도 대체: 정규화 후 inner product

xq = np.random.randn(2000, dim).astype('float32')
faiss.normalize_L2(xq)

# 1) HNSW (inner product)
M = 16
index_hnsw = faiss.IndexHNSWFlat(dim, M, faiss.METRIC_INNER_PRODUCT)
index_hnsw.hnsw.efConstruction = 200
index_hnsw.hnsw.efSearch = 64
index_hnsw.add(xb)

# 2) PQ (IndexPQ는 L2가 기본이라, 실제로는 IVF+PQ 또는 OPQ를 더 자주 씀)
# 여기서는 개념 예시로만 참고
m = 96  # subquantizers
nbits = 8
index_pq = faiss.IndexPQ(dim, m, nbits)
index_pq.train(xb)
index_pq.add(xb)

# 검색
k = 10
D1, I1 = index_hnsw.search(xq, k)
D2, I2 = index_pq.search(xq, k)
print(I1.shape, I2.shape)

현업에서는 보통 아래 형태를 더 많이 씁니다.

  • IVF+PQ: 대규모에서 속도/메모리 균형
  • OPQ+IVF+PQ: PQ 왜곡을 줄여 Recall 방어
  • “근사 검색 후 리랭킹”: topK를 넉넉히 뽑고 재정렬

운영 체크리스트: 튜닝 이후에 터지는 문제들

1) OOM과 재시작 루프

인덱스가 커지거나 efSearch를 올리면 순간 메모리 피크가 증가할 수 있습니다. 쿠버네티스에서 OOM으로 재시작 루프가 나면 성능 튜닝보다 먼저 안정화가 필요합니다.

2) p99 지연이 튄다

대부분 아래 원인입니다.

  • efSearch가 너무 큼
  • GC/메모리 압박으로 페이지 폴트 증가
  • 샤드 불균형(핫 샤드)

해결은 “리콜을 약간 양보하고 p99를 지키는” 방향이 되는 경우가 많습니다. RAG는 p99가 나빠지면 상위 레이어 타임아웃과 재시도가 겹쳐 시스템이 더 불안정해집니다.

3) 인덱스 업데이트 전략

HNSW는 실시간 업서트가 가능해도, 삭제/업데이트가 누적되면 성능이 흔들릴 수 있습니다.

  • tombstone 누적 정책
  • 주기적 리빌드/컴팩션
  • 배치 머지 전략

이때 배치 작업이 클러스터 리소스를 잡아먹어 서비스가 흔들리면, 오토스케일/우선순위/중단 허용 정책까지 같이 봐야 합니다.

추천 튜닝 플로우(재현 가능한 순서)

  1. 기준선 측정: 현재 설정에서 Recall@10, p95/p99, 인덱스 메모리
  2. PQ 도입: 목표 압축률(예: 8배) 설정 후 오프라인 평가
  3. HNSW efSearch 스윕: 리콜-지연 곡선 작성
  4. HNSW M 조정: 같은 리콜을 더 낮은 efSearch로 달성 가능한지 확인
  5. 리랭킹 추가(선택): PQ/HNSW 근사로 인한 품질 손실을 회복
  6. 온라인 A/B: RAG 다운스트림 지표로 최종 승인

결론: 비용 절감은 “압축+탐색”을 같이 잡을 때 나온다

PQ만 도입하면 Recall이 흔들리고, HNSW만 조이면 CPU는 줄지만 메모리 노드 수가 그대로일 수 있습니다. PQ로 메모리를 줄여 노드 수를 내리고, HNSW 튜닝으로 쿼리 CPU와 p99를 관리하면, 품질을 크게 해치지 않으면서도 월 비용을 70% 가까이 절감하는 케이스가 현실적으로 가능합니다.

마지막으로, 튜닝은 한 번에 끝내는 작업이 아니라 데이터 분포(문서/쿼리)가 변할 때마다 재평가해야 합니다. 특히 새로운 도메인 문서가 유입되거나 임베딩 모델이 바뀌면 PQ 코드북과 HNSW 최적점도 함께 이동합니다. 이를 전제로 오프라인 벤치+온라인 A/B 파이프라인을 만들어두면, 비용과 품질을 동시에 지키는 RAG 운영이 훨씬 쉬워집니다.