문제: 벡터 인덱스는 메모리를 빠르게 소진한다

임베딩 차원이 768~1536으로 커지면 벡터 하나당 float32 기준 3~6KB다. 1억 개면 원본 벡터만 300~600GB로, 대부분의 인덱스는 검색 속도를 위해 이를 RAM에 올려야 한다. HNSW처럼 그래프 구조를 쓰면 링크 오버헤드까지 더해져 실제 사용량은 더 늘어난다. 결국 노드를 수평 확장하거나, 인스턴스 등급을 올리는 비용 문제로 직결된다.

PQ(Product Quantization)는 정확도를 약간 희생하고 메모리를 크게 줄이는 압축 기법이다. 원본을 디스크에 두고 재랭킹으로 정확도를 보정할 수 있어 실무에서 자주 쓴다.

PQ의 동작 원리

D차원 벡터를 M개의 서브벡터로 쪼갠다. 각 서브공간마다 k-means로 2^nbits개(보통 nbits=8, 256개)의 대표 벡터(centroid)를 학습해 코드북을 만든다. 이후 각 서브벡터는 가장 가까운 centroid의 인덱스(1바이트)로 치환된다.

즉 D차원 float32 벡터(4D 바이트)가 M바이트로 줄어든다. 1536차원(6144바이트)을 M=96, nbits=8로 압축하면 벡터당 96바이트, 약 64배 압축이다. 검색 시에는 쿼리의 각 서브벡터와 코드북 간 거리를 미리 계산한 테이블(ADC, Asymmetric Distance Computation)을 만들어 룩업 합산으로 거리를 근사한다.

Faiss로 IVF-PQ 인덱스 만들기

PQ는 단독으로도 쓰지만, IVF(역파일)로 후보군을 좁힌 뒤 PQ로 압축 저장하는 IVF-PQ 조합이 대용량에서 표준적이다.

import faiss
import numpy as np

d = 1536          # 임베딩 차원
nlist = 4096      # IVF 클러스터 수 (√N 근처에서 시작)
m = 96            # 서브벡터 개수 (d % m == 0 이어야 함)
nbits = 8         # centroid = 2^8 = 256개

quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits)

xb = np.random.random((1_000_000, d)).astype('float32')
index.train(xb[:200_000])   # 코드북 학습에는 표본이면 충분
index.add(xb)

index.nprobe = 32           # 탐색할 클러스터 수 (정확도/속도 트레이드오프)
D, I = index.search(xb[:5], 10)
print(I)

m은 반드시 d의 약수여야 하고, 클수록 정확도는 오르지만 압축률은 떨어진다. nprobe는 런타임에 조정 가능하니 배포 후 지표를 보며 튜닝하면 된다.

재랭킹으로 정확도 보정

PQ는 근사 거리라 상위 결과 순위가 흔들린다. 후보를 넉넉히 뽑은 뒤 원본 벡터로 정밀 거리를 다시 계산하면 recall을 상당히 회복할 수 있다. Faiss는 IndexRefineFlat으로 이를 감싼다.

base = faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits)
index = faiss.IndexRefineFlat(base)   # 원본 벡터를 별도 보관해 재계산

index.train(xb[:200_000])
index.add(xb)

index.k_factor = 4        # 최종 k의 4배를 PQ로 뽑아 원본으로 재정렬
faiss.downcast_index(index.base_index).nprobe = 32
D, I = index.search(query, 10)

단, RefineFlat은 원본을 메모리에 유지하므로 압축 이점이 사라진다. 원본은 오브젝트 스토리지나 로컬 디스크(mmap)에 두고, 상위 후보에 대해서만 읽어 재랭킹하는 구조가 실무에서는 더 현실적이다.

기법별 메모리·정확도 비교

방식벡터당 크기(d=1536)상대 recall비고
Flat (원본)6144B기준(1.0)정확하지만 무겁다
HNSW6144B + 링크≈1.0빠르나 메모리 최다
IVF-PQ (m=96)≈96B0.7~0.85약 60배 압축
IVF-PQ + 재랭킹96B + 원본(디스크)0.9~0.97디스크 I/O 추가

운영 시 주의점

  • 학습 데이터 분포: 코드북은 학습 표본 분포에 종속된다. 도메인이나 임베딩 모델이 바뀌면 코드북을 재학습해야 하며, 그렇지 않으면 recall이 조용히 떨어진다.
  • 정규화: 코사인 유사도를 쓴다면 add/search 전에 벡터를 L2 정규화하고 내적(IP) 메트릭을 사용해야 거리 근사가 일관된다.
  • 학습 비용: IVF의 nlist와 PQ 코드북 학습은 표본만으로 충분하다. 전체 데이터로 train()하면 시간만 낭비된다.
  • recall 측정: 배포 전 Flat 인덱스로 만든 정답셋과 비교해 recall@k를 정량 측정하고, nprobe·k_factor를 지표 기반으로 정하라. 감으로 정하면 서비스 품질이 흔들린다.
  • 업데이트: IVF-PQ는 대량 삭제·갱신에 약하다. 변경이 잦으면 주기적 재빌드 파이프라인을 함께 설계해야 한다.

정리하면 PQ는 "원본을 완벽히 보존하는" 기법이 아니라 "허용 가능한 정확도 손실로 메모리를 수십 배 줄이는" 트레이드오프 도구다. 재랭킹과 recall 측정을 함께 갖추면 대용량 벡터 검색의 인프라 비용을 실질적으로 낮출 수 있다.