벡터 검색 인덱스 메모리 최적화: 양자화와 halfvec로 RAM 줄이기
벡터 검색 인덱스 메모리는 대부분 원본 임베딩이 차지한다. Qdrant·pgvector 문서 기준으로 스칼라 양자화 약 4배, 바이너리 32배, halfvec 2배 절감과 오버샘플링·디스크 분리로 정확도를 지키는 법을 개발자 눈높이로 정리했다.
목차
벡터 검색 인덱스에서 메모리를 잡아먹는 주범은 원본 임베딩 벡터다. 임베딩(텍스트나 이미지를 숫자 배열로 바꾼 것) 100만 개가 1536차원 float32라면 그것만으로 약 6GB를 쓴다. 여기서 RAM을 줄이는 길은 크게 세 갈래다. 숫자 타입을 작게 바꾸는 양자화, 32비트를 16비트로 낮추는 반정밀도, 그리고 원본은 디스크로 내리고 압축본과 그래프만 메모리에 두는 계층 분리다. 2026년 7월 기준 Qdrant 공식 문서와 pgvector 프로젝트가 안내하는 수치를 근거로, 각 방법이 얼마나 줄여 주고 무엇을 감수해야 하는지 짚는다. 이 글은 HNSW 기반 근사 최근접 검색(정확한 이웃 대신 충분히 가까운 이웃을 빠르게 찾는 방식)을 pgvector나 Qdrant 같은 벡터 데이터베이스로 운영하는 개발자를 대상으로 하며, 임베딩 모델 자체를 교체하는 이야기는 다루지 않는다.
메모리는 어디에 쌓이는가
인덱스 메모리는 두 덩어리로 나뉜다. 원본 벡터와 그래프 구조다. 원본 벡터가 차지하는 양은 계산이 간단하다. 벡터 개수 × 차원 × 4바이트(float32)다. 100만 개 × 1536차원이면 약 6.1GB가 나온다.
그래프 쪽은 생각보다 작다. HNSW(계층적 그래프로 이웃 노드를 따라가며 탐색하는 인덱스)의 참조 구현인 hnswlib 문서는 그래프가 원소 하나당 대략 M×8~10바이트를 쓰며, 연결 수를 정하는 M 값은 대부분의 경우 12~48 사이가 적당하다고 밝힌다. 100만 원소에 M=16이면 그래프는 대략 130~160MB 수준이다. 6GB짜리 원본 벡터에 비하면 곁가지다. 그래서 메모리 최적화의 승부처는 그래프 튜닝이 아니라 원본 벡터를 어떻게 줄이느냐에 있다.

압축으로 메모리를 얼마나 줄일 수 있나
스칼라 양자화면 약 4배, 바이너리 양자화면 약 32배까지 줄인다. 다만 압축이 셀수록 정확도 손실도 커지므로 그냥 켜기만 하면 안 되고 뒤에서 다룰 재점수와 함께 써야 한다.
스칼라 양자화는 각 숫자를 int8 정수로 바꾼다. Qdrant 공식 문서에 따르면 float32를 int8로 변환하면 숫자 하나가 쓰는 메모리를 75% 줄이고, int8 전용 SIMD 명령으로 검색 속도까지 붙으며, 이때 생기는 오차는 대체로 1% 미만이다. 바이너리 양자화는 각 성분을 1비트로 뭉갠다. Qdrant는 이 방식이 메모리를 32배 줄이고 검색을 최대 40배 빠르게 하되 고차원 벡터에서 잘 맞는다고 설명한다. 제품 양자화(PQ, 벡터를 여러 조각으로 쪼개 미리 학습한 코드북 번호로 치환하는 방식)는 더 공격적이어서 스칼라 양자화보다 훨씬 높은 압축률을 낼 수 있다. FAISS의 IVFPQ 인덱스가 이 계열의 대표다.
절반만 줄여도 충분하다면 반정밀도가 가장 안전하다. pgvector 프로젝트는 0.7 버전에서 도입한 halfvec 타입이 16비트(2바이트) 부동소수점으로 저장해 용량을 절반으로 줄인다고 안내한다. 참고로 양자화는 벡터 검색만의 기법이 아니다. KV 캐시와 양자화로 LLM 메모리 병목을 줄이는 접근과 뿌리가 같다.
| 방식 | 원본 대비 메모리 | 특징 |
|---|---|---|
| 반정밀도(halfvec) | 약 1/2 | 16비트 저장, 손실이 가장 작음 |
| 스칼라 양자화(int8) | 약 1/4 | 오차 대체로 1% 미만 |
| 바이너리 양자화 | 약 1/32 | 고차원에 적합, 재점수 필요 |
| 제품 양자화(PQ) | 최대 1/64 | 코드북 학습 필요 |
정확도 손실은 어떻게 되돌리나
오버샘플링과 재점수(rescoring)로 대부분 회복한다. 압축본은 어디까지나 근사치라 순위가 조금 흔들리는데, 후보를 넉넉히 뽑아 원본으로 다시 줄 세우면 그 흔들림을 걷어낼 수 있다.
Qdrant 문서의 예를 들면, 상위 100개가 필요할 때 오버샘플링 배수를 2.4로 두면 압축본으로 240개를 먼저 뽑고 원본 벡터로 다시 채점해 최종 100개를 낸다. 여기에 계층 분리를 얹으면 메모리와 정확도를 함께 챙긴다. 원본 벡터는 디스크에 두고(on_disk) 압축본과 그래프는 메모리에 상주시키면(always_ram), 대부분의 탐색은 빠른 RAM 압축본으로 처리하고 마지막 재점수 단계에서만 디스크 원본을 읽는다. 단 디스크 재점수는 느려질 수 있으니 오버샘플링 배수는 1.5~3배 범위에서 시작해 정확도와 지연을 보며 조절하는 편이 무난하다.
도구별 적용 방법
pgvector에서 halfvec는 표현식 인덱스로 붙인다. 원본 컬럼은 float32 vector로 두고 인덱스만 halfvec로 캐스팅하는 방식이다. pgvector는 기본 vector 타입의 HNSW 인덱싱 한도가 2,000차원이며, halfvec를 쓰면 이 한도가 4,000차원으로 늘어난다고 안내한다.
-- 원본은 vector, 인덱스는 16비트 halfvec로 생성
CREATE INDEX ON items
USING hnsw ((embedding::halfvec(1536)) halfvec_cosine_ops);Qdrant는 컬렉션 설정의 quantization_config에서 스칼라·바이너리·제품 양자화를 고르고, 앞서 말한 on_disk와 always_ram으로 저장 위치를 지정한다. FAISS를 직접 다룬다면 IndexIVFPQ처럼 역파일 인덱스와 제품 양자화를 결합한 타입을 골라 코드북 크기로 압축률을 조율한다. 셋 다 원리는 같다. 원본을 줄이거나 내리고, 그래프와 압축본을 메모리에 남긴다.
학습 없는 양자화를 로컬로 구현한 turbovec
앞의 도구들은 벡터 데이터베이스나 라이브러리 설정에 기대지만, 같은 압축 개념을 로컬 인덱스로 직접 구현한 오픈소스도 있다. turbovec(MIT 라이선스, Rust 코어 + Python 바인딩)이 그런 도구다. 이 인덱스는 Google Research의 TurboQuant 양자화 알고리즘(arXiv 2504.19874) 위에 올라간다. TurboQuant 논문은 이 방식을 데이터 무관(data-oblivious) 양자화로, 별도 학습 단계 없이 근최적 왜곡률을 낸다고 설명한다. 앞서 본 제품 양자화(PQ)가 코드북을 미리 학습해야 하는 것과 갈리는 지점이다. 코드북 훈련이 없으니 캘리브레이션 데이터도, 사전 처리도 필요 없다.
메모리 절감 폭은 프로젝트가 README에서 밝힌 수치로 가늠할 수 있다. turbovec은 1000만 문서 코퍼스를 float32로 두면 약 31GB인데 자신은 약 4GB에 담고, FAISS보다 빠르게 검색한다고 주장한다. 속도 근거로는 손수 작성한 SIMD 커널(ARM NEON, x86 AVX-512BW)을 든다. README에 따르면 이 커널이 FAISS의 IndexPQFastScan 대비 ARM에서 10~19% 빠르고, x86에서는 4비트 구성이 우세하되 2비트 구성은 수 % 뒤진다.
운영 관점에서 눈에 띄는 특징은 온라인 인제스트다. 프로젝트는 벡터를 추가하면 곧바로 색인되고, 코퍼스가 커져도 train 단계·파라미터 튜닝·리빌드가 없다고 설명한다. 검색 시 필터링도 커널 수준에서 처리한다. id 허용목록(allowlist)이나 비트마스크를 SIMD 커널이 32벡터 블록 단위로 직접 반영해, 허용된 슬롯이 하나도 없는 블록은 스코어링 전에 통째로 건너뛴다. turbovec은 이 방식이 선택적 필터에서도 오버페치나 리콜 손실 없이 동작하고 결과 개수가 min(k, 허용 수)가 된다고 밝힌다. 삭제가 잦은 환경에서 외부 id를 안정적으로 유지하는 IdMapIndex도 함께 제공한다.
포지셔닝은 순수 로컬이다. 관리형 서비스도, 장비 밖으로 나가는 데이터도 없어 프라이버시·메모리·지연이 동시에 중요한 에어갭 RAG에 맞는 선택지다. LangChain·LlamaIndex·Haystack·Agno에 드롭인으로 붙는 통합을 제공하고, 설치는 pip install turbovec 또는 cargo add turbovec로 한다. 코드와 벤치마크는 turbovec GitHub 저장소에서 확인할 수 있다.
적용 순서와 흔한 함정
순서를 지키면 헛수고를 줄인다. 먼저 실제 RAM 사용량을 측정하고, 손실이 가장 적은 halfvec나 스칼라 양자화부터 적용한다. 그래도 부족할 때 바이너리나 제품 양자화로 넘어가되 반드시 재점수를 함께 켠다. 처음부터 바이너리로 직행하면 정확도 회복 설계가 빠져 검색 품질이 무너지기 쉽다.
빌드 단계의 메모리도 함정이다. HNSW 그래프가 빌드 메모리에 다 들어가야 인덱스 생성이 빠른데, PostgreSQL의 maintenance_work_mem 기본값은 64MB에 불과하다. pgvector는 그래프가 이 한도를 넘으면 알림을 띄우며 값을 키우라고 권하고, 넘어서면 훨씬 느린 디스크 기반 빌드로 떨어진다고 안내한다. 큰 인덱스를 만들 때는 세션에서 SET maintenance_work_mem = '2GB';처럼 넉넉히 올린 뒤 생성하는 편이 안전하다.
마지막 함정은 가장 흔하면서 허무하다. 양자화만 켜고 원본을 그대로 RAM에 남겨 두는 경우다. Qdrant는 양자화를 켜면 원본과 압축본을 둘 다 기본적으로 RAM에 두며, 원본을 디스크로 내리려면 on_disk를 명시적으로 켜야 한다고 안내한다. 이 설정을 빠뜨리면 메모리가 줄기는커녕 압축본만큼 더 늘어난다. 양자화를 적용한 뒤에는 반드시 실제 메모리를 다시 재 보고, 줄었는지 눈으로 확인하고 넘어가자.





