Database · ARTICLE · 2026
turbovec: TurboQuant 알고리즘을 Rust로 구현한 학습이 필요 없는 벡터 인덱스
요약
PyTorch Korea User Group 운영자 9bow(박정환)가 2026년 5월 21일 PyTorchKR Discuss에 올린 한국어 소개글이다. Rust로 구현된 벡터 인덱스 라이브러리 turbovec을 다루며, 이 라이브러리가 구현한 TurboQuant 양자화 알고리즘의 요점과 벤치마크 수치, 그리고 Python과 Rust 코드 예제를 한국어로 정리한다.
글이 앞세우는 문장은 다음과 같다. "1,000만 건 규모의 임베딩을 float32로 저장하면 31GB가 필요하지만, turbovec은 같은 데이터를 4GB에 담으면서 FAISS보다 빠르게 검색한다." 압축과 속도를 동시에 얻는다는 주장이 글 전체를 관통하고, 나머지 절이 그 주장을 압축률, recall, 속도 수치로 뒷받침한다.
이 페이지는 알고리즘과 구현의 상세를 다루지 않는다. 원 알고리즘은 TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate가, 구현체의 API와 벤치마크 전문은 turbovec: Google TurboQuant for vector search (Rust + Python)가 담당한다. 여기서 다루는 것은 한국어 커뮤니티가 이 도구를 어떤 순서와 강조로 소개했는가, 그리고 그 소개가 원 자료를 얼마나 정확히 옮겼는가다. 뒤의 질문에 대한 대조 결과는 한계 절에 표로 실었다.
배경
글이 출발점으로 삼는 문제는 임베딩 저장 비용이다. 임베딩은 텍스트나 이미지를 고정 차원 벡터로 바꾼 표현이며, 검색용 인덱스는 이 벡터를 전부 메모리에 올려두어야 빠르게 동작한다. 1,000만 건 규모에서 float32 원본을 그대로 두면 약 31GB가 필요하다는 것이 글이 제시하는 구체적 수치다.
양자화는 이 비용을 줄이는 표준적인 대응이다. 양자화는 값을 낮은 비트 수로 줄여 데이터를 압축하는 기법이다. 문제는 기존 방식이 압축 전에 학습 단계를 요구한다는 점이다. 글은 FAISS의 PQ(Product Quantization)를 그 대표 사례로 들고, PQ가 데이터를 보고 codebook을 학습한다고 설명한다.
TurboQuant이 내세우는 차별점이 여기서 나온다. 데이터에 무관한(data-oblivious) 방식이라 codebook 학습이 없고, 따라서 학습, 튜닝, 재빌드 단계 자체가 사라진다. 글의 제목에 "학습이 필요 없는"이 들어간 이유다.
핵심 개념
data-oblivious 양자화는 입력 데이터로 codebook을 학습하지 않는 양자화를 말한다. 어떤 데이터가 들어오든 버킷 경계가 같으므로, 코퍼스가 커져도 인덱스를 다시 만들 필요가 없다.
무작위 직교 회전은 모든 벡터에 같은 무작위 직교 행렬을 곱하는 전처리다. 직교 변환은 벡터의 길이와 벡터 사이 각도를 바꾸지 않으므로 검색 결과의 기하가 보존된다. 글의 설명에 따르면 회전을 거친 좌표는 입력 데이터와 무관하게 사전에 알려진 Beta 분포를 따르게 되고, 분포를 미리 알기 때문에 최적 버킷을 데이터 없이 계산할 수 있다.
Lloyd-Max 스칼라 양자화는 분포가 주어졌을 때 버킷 경계와 중심값을 정하는 방법이다. 앞 단계가 분포를 고정해 주었으므로 이 계산은 데이터를 한 번도 보지 않고 끝난다.
비트 폭은 좌표 하나를 몇 비트로 표현할지를 정하는 설정값이며, 압축률과 recall을 맞바꾸는 설정이다. 글이 다루는 값은 2비트와 4비트 두 가지다.
| 비트 폭 | 벡터당 크기 (d=1536) | 압축 배수 | 특징 |
|---|---|---|---|
| float32 원본 | 6,144바이트 | 1배 | 압축 없음 |
| 4비트 | 768바이트 | 약 8배 | 저차원에서도 FAISS보다 recall이 높다 |
| 2비트 | 384바이트 | 약 16배 | 메모리를 가장 크게 줄이지만 저차원에서 recall이 떨어진다 |
길이 재정규화 스칼라는 벡터마다 하나씩 저장해 두었다가 검색 커널에서 곱하는 값이다. 글은 이 값 덕분에 내적 추정기가 편향 없이 동작한다고 설명한다. 이 단계의 출처에는 주의할 점이 있으며 한계 절에서 다룬다.
방법
알고리즘 다섯 단계
글은 "핵심 알고리즘 흐름"이라는 절에서 다섯 단계를 제시한다. 각 단계는 한 문장씩만 서술되고 수식이나 구현 근거는 실리지 않는다.
| 단계 | 이름 | 글의 설명 | 얻는 것 |
|---|---|---|---|
| 1 | 정규화 | 벡터 길이를 float으로 저장하고 본체는 단위 방향으로 처리한다 | 길이와 방향을 분리해 방향만 압축 대상으로 남긴다 |
| 2 | 무작위 직교 회전 | 모든 벡터에 동일한 무작위 직교 행렬을 적용한다 | 좌표 분포를 데이터와 무관하게 만든다 |
| 3 | Lloyd-Max 스칼라 양자화 | 데이터를 보지 않고 버킷 경계와 중심값을 계산한다 | 학습 단계를 없앤다 |
| 4 | 비트 패킹 | 각 좌표를 비트 단위로 패킹한다 | 실제 메모리 절감이 발생한다 |
| 5 | 길이 재정규화 점수 | 인코딩 시 계산한 스칼라를 검색 커널에서 곱한다 | 내적 추정의 편향을 없앤다 |
앞의 네 단계가 인코딩 시점에 일어나고 마지막 단계만 검색 시점에 개입한다.
Python API
글은 세 가지 사용 형태를 코드로 보여준다. 클래스는 두 개뿐이고, 외부 ID가 필요한지 여부로 나뉜다.
| 형태 | 클래스 | 핵심 호출 | 쓰임 |
|---|---|---|---|
| 기본 인덱스 | TurboQuantIndex(dim=1536, bit_width=4) |
add(vectors), search(query, k=10) |
벡터를 넣은 순서가 곧 인덱스인 단순한 경우 |
| 외부 ID 보존 | IdMapIndex(dim=1536, bit_width=4) |
add_with_ids(vectors, ids), remove(1002) |
삭제가 있어도 외부 ID가 유지되어야 하는 경우 |
| 필터링된 검색 | IdMapIndex |
search(query, k=10, allowlist=allowed) |
허용된 ID 집합 안에서만 검색해야 하는 경우 |
import numpy as np
from turbovec import TurboQuantIndex, IdMapIndex
index = TurboQuantIndex(dim=1536, bit_width=4)
index.add(vectors)
scores, indices = index.search(query, k=10)
idmap = IdMapIndex(dim=1536, bit_width=4)
idmap.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))
idmap.remove(1002)
scores, ids = idmap.search(query, k=10, allowlist=allowed)
외부 ID는 np.uint64로 넘긴다. allowlist 인자가 있는 세 번째 형태는 다른 시스템이 좁혀준 후보 집합 안에서만 검색하는 용도다.
Rust API
Rust 예시는 Python과 같은 두 클래스를 쓰되 파일 저장 호출이 추가된다.
| 호출 | 역할 |
|---|---|
TurboQuantIndex::new(1536, 4) |
차원과 비트 폭을 지정해 인덱스를 만든다 |
index.add(&vectors) |
벡터를 넣는다 |
index.search(&queries, 10) |
상위 10개를 검색한다 |
index.write("index.tv").unwrap() |
인덱스를 파일로 저장한다 |
IdMapIndex::new(1536, 4) |
외부 ID를 보존하는 인덱스를 만든다 |
id_index.add_with_ids(&vectors, &[1001, 1002, 1003]) |
외부 ID와 함께 넣는다 |
id_index.remove(1002) |
ID로 삭제한다 |
use turbovec::{TurboQuantIndex, IdMapIndex};
let mut index = TurboQuantIndex::new(1536, 4);
index.add(&vectors);
let results = index.search(&queries, 10);
index.write("index.tv").unwrap();
let mut id_index = IdMapIndex::new(1536, 4);
id_index.add_with_ids(&vectors, &[1001, 1002, 1003]);
id_index.remove(1002);
배포와 통합
글은 도입 경로를 짧게 정리한다. 네 개 RAG 프레임워크에 드롭인 어댑터가 있고, 순수 로컬로 동작하므로 데이터가 기기 밖으로 나가지 않는다.
| 항목 | 내용 |
|---|---|
| 라이선스 | MIT License. 상업적 활용, 수정, 재배포가 자유롭고 라이선스 파일과 저작권 고지를 유지해야 한다 |
| 배포 채널 | PyPI와 crates.io |
| RAG 프레임워크 어댑터 | LangChain, LlamaIndex, Haystack, Agno |
| 동작 방식 | 순수 로컬 |
결과
글이 밝힌 벤치마크 조건은 10만 벡터, 1,000 질의(query), k=64다. 데이터셋과 하드웨어 이름 외에 반복 횟수나 스레드 설정 같은 측정 조건은 적지 않는다.
압축률과 메모리
| 항목 | float32 | 2비트 양자화 | 4비트 양자화 |
|---|---|---|---|
| 벡터당 크기 (d=1536) | 6,144바이트 | 384바이트 | 768바이트 |
| 압축 배수 | 1배 | 약 16배 | 약 8배 |
| 1,000만 건 코퍼스 | 약 31GB | 4GB 수준 | 글에 수치 없음 |
메모리 절감폭이 가장 크게 드러나는 지점은 코퍼스 규모다. 31GB는 일반적인 서버 한 대의 메모리를 크게 차지하는 크기이지만, 4GB는 노트북에서도 상주시킬 수 있는 수준이다. 글이 로컬 동작을 강조하는 이유가 이 수치에 있다.
표의 마지막 행은 글의 표기를 그대로 옮긴 것이며, 글이 세운 d=1536 전제와 맞지 않는다. 계산 근거는 한계 절에서 다룬다.
recall
recall은 정답 이웃이 top-k 안에 들어오는 비율이며, R@1은 최근접 1개만 따진 값이다. 글은 FAISS와의 차이를 점수 단위로 표기한다.
| 데이터셋 | 차원 | 4비트 | 2비트 |
|---|---|---|---|
| OpenAI 임베딩 | d=1536, d=3072 | TurboQuant이 0.4~3.4점 우위 (비트 폭 구분 없이 제시) | 같은 범위로 제시 |
| GloVe | d=200 | TurboQuant이 0.3점 우위 | TurboQuant이 1.2점 열세 |
고차원 임베딩에서는 TurboQuant이 앞서고 저차원에서는 2비트에서 뒤진다는 것이 글이 제시하는 구도다. 다만 글은 그 원인을 설명하지 않고 수치만 적는다.
검색 속도
| 플랫폼 | 프로세서 | FAISS FastScan 대비 |
|---|---|---|
| ARM | Apple M3 Max | 12~20% 빠름 |
| x86 | Intel Xeon | 4비트 설정에서 1~6% 우위 |
x86의 2비트 설정에 대한 수치는 글에 없다. 4비트만 적혀 있다는 점이 뒤의 대조에서 의미를 갖는다.
한계
저자가 밝힌 것과 밝히지 않은 것
저자는 이 글의 한계나 향후 과제를 따로 밝히지 않는다. 글에 실린 유일한 열세 수치는 GloVe d=200 2비트의 recall 1.2점 열세이며, 저자는 그 원인도 대응 방법도 적지 않는다.
자료의 성격에서 오는 제약은 세 가지다.
- 3차 자료: 원 저장소와 원 논문을 요약한 소개글이라 알고리즘의 근거, 벤치마크 재현 방법, API 전체 목록이 없다.
- 측정 조건 미기재: 반복 횟수, 스레드 설정, FAISS 비교 대상 인덱스 종류를 밝히지 않아 같은 조건의 재현이 어렵다.
- 시점 고정: 2026년 5월 21일 시점의 저장소 상태를 옮긴 글이다.
글 안에서 어긋나는 수치
압축률 절의 네 수치는 모두 "OpenAI 임베딩 기준(d=1536)"이라는 한 줄 아래에 놓여 있는데, 마지막 수치만 그 전제와 맞지 않는다.
| 수치 | 글의 표기 | d=1536 전제에서의 계산값 |
|---|---|---|
| 벡터당 float32 크기 | 6,144바이트 | 1,536 곱하기 4바이트로 6,144바이트, 일치한다 |
| 2비트 압축 배수 | 약 16배 (6,144바이트에서 384바이트) | 일치한다 |
| 1,000만 건 2비트 | 4GB 수준 | 384바이트 곱하기 1,000만으로 약 3.8GB, 일치한다 |
| 1,000만 건 float32 | 약 31GB | 6,144바이트 곱하기 1,000만으로 약 61GB, 어긋난다 |
float32 항목만 절반 수준으로 적혀 있다. 31GB를 벡터당 크기로 환산하면 약 3,100바이트이고 차원으로는 d 약 775에 해당하므로, d=1536 전제와 맞지 않는다. 같은 문제가 글 첫머리의 인용문에도 이어진다. 31GB에서 4GB로 줄어든다는 비율은 약 7.75배인데, 글 자신이 2비트에 붙인 16배와 어긋나고 오히려 4비트의 약 8배에 가깝다.
31GB와 4GB 조합은 저장소 README 첫 문장에 차원 표기 없이 등장하는 값이다. 글이 이를 d=1536 절 안으로 옮겨 적으면서 전제가 어긋난 것으로 보인다. 압축 효과를 인용할 때는 배수(2비트 약 16배, 4비트 약 8배)를 기준으로 삼고, 절대 용량은 차원을 확인한 뒤 다시 계산하는 편이 안전하다.
원 저장소와 대조한 결과
세 번째 제약이 실제 문제로 드러난다. 저장소 README 스냅샷(raw/repos/ryancodrai-turbovec.md, 2026년 6월 17일 시점)과 대조하면 수치와 단계 구성이 여러 곳에서 어긋난다.
| 항목 | 글 (2026-05-21) | 저장소 README 스냅샷 (2026-06-17) |
|---|---|---|
| 알고리즘 단계 수 | 5단계 | 6단계. 회전과 Lloyd-Max 사이에 좌표별 보정(TQ+)이 들어간다 |
| ARM 속도 우위 | 12~20% | 10~19% |
| x86 속도 | 4비트에서 1~6% 우위 (2비트 언급 없음) | 4비트에서 최대 약 5% 우위, 2비트에서는 FAISS가 앞선다 (d=1536 단일 스레드 약 8%) |
| OpenAI 임베딩 R@1 | 0.4~3.4점 우위 | 0.2~1.9점 우위 |
| GloVe d=200 R@1 | 4비트 0.3점 우위, 2비트 1.2점 열세 | 4비트 0.9점 우위, 2비트는 0.1점 이내로 사실상 동률 |
| FAISS 비교 대상 | 밝히지 않음 | recall은 IndexPQ(LUT256, nbits=8), 속도는 FastScan |
| Rust 인덱스 생성 | TurboQuantIndex::new(1536, 4) |
TurboQuantIndex::new(1536, 4).unwrap() |
| 인덱스 로드 | 예시 없음 | Python과 Rust 양쪽에 load()가 있다 |
이 차이는 저자의 오기로 보기 어렵다. README는 새로 추가된 TQ+ 단계가 가장 크게 어긋나는 구간에서 R@1을 최대 1.4%p 끌어올리며 GloVe 2비트가 그 사례라고 밝힌다. 글이 적은 2비트 1.2점 열세에 1.4%p를 더하면 README가 보고하는 사실상 동률에 도달한다. 즉 글은 TQ+가 들어가기 전 저장소 상태를 옮긴 것이고, 그 뒤 약 한 달 사이에 저장소가 갱신된 것으로 보인다.
실무 적용 시에는 이 글의 수치를 2026년 5월 시점의 스냅샷으로 읽어야 한다. 도입을 검토한다면 저장소의 현재 벤치마크를 다시 확인하는 절차가 필요하다.
원 논문과 대조한 결과
원 논문 PDF(raw/papers/zandieh-2025-turboquant-online-vector-quantization-with.pdf)와 대조하면 소개 문구와 알고리즘 귀속에서 부정확한 대목이 나온다.
| 항목 | 글의 서술 | 논문 PDF |
|---|---|---|
| 저자 소속 | "Google Research가 공개한" | 저자 4명 중 Amir Zandieh와 Vahab Mirrokni만 Google Research다. Majid Hadian은 Google DeepMind, Majid Daliri는 New York University 소속이다 |
| 공개 시점 | "ICLR 2026에서 공개한" | arXiv 게재일은 2025년 4월 28일이다. PDF 전문 25페이지에 ICLR이라는 문자열이 없다. ICLR 2026 표기는 저장소 README에만 있다 |
| 내적 편향 보정 | 길이 재정규화를 TurboQuant 알고리즘의 5단계로 소개 | 논문은 MSE 양자화 뒤 잔차에 1비트 QJL 변환을 적용하는 2단계 방식으로 무편향 내적 추정기를 만든다. 논문 전문에 길이 재정규화라는 표현이 없다 |
| 왜곡률 보장 | 언급 없음 | 정보이론적 하한 대비 약 2.7배 이내라는 것이 논문의 핵심 주장 중 하나다 |
세 번째 항목이 가장 실질적인 차이다. 저장소 README는 길이 재정규화를 RaBitQ(SIGMOD 2024)에서 가져와 적용했다고 References에서 밝히는데, 글은 이 단계를 TurboQuant 알고리즘 자체의 구성으로 소개한다. 논문의 알고리즘과 turbovec의 구현이 이 지점에서 다르며, 글을 읽고 논문을 찾아본 독자는 대응되는 단계를 찾지 못하게 된다.
핵심 용어
| 용어 | 뜻 |
|---|---|
| turbovec | TurboQuant 알고리즘을 Rust로 구현한 벡터 인덱스 라이브러리. Python 바인딩과 Rust 크레이트로 배포된다 |
| TurboQuant | 무작위 직교 회전으로 좌표 분포를 Beta 분포로 만든 뒤 스칼라 양자화를 적용하는 양자화 알고리즘 |
| data-oblivious 양자화 | 입력 데이터로 codebook을 학습하지 않는 양자화. 회전으로 분포를 미리 정해두므로 학습 단계가 없다 |
| PQ (Product Quantization) | 글이 비교 대상으로 드는 FAISS의 대표 양자화 방식. 데이터를 보고 codebook을 학습한다 |
| Lloyd-Max 스칼라 양자화 | 분포가 알려져 있을 때 버킷 경계와 중심값을 데이터 없이 계산하는 스칼라 양자화 방식 |
| 길이 재정규화 스칼라 | 벡터마다 저장해 두었다가 검색 커널에서 곱하는 값. 내적 추정기의 편향을 없앤다 |
관련 페이지
- TurboQuant: Online Vector Quantization with Near-optimal Distortion Rate: 글이 소개하는 원 알고리즘 논문. 무편향 내적 추정 방식과 왜곡률 보장을 확인하려면 이 페이지를 본다. 위 대조표의 논문 쪽 근거가 여기에 있다.
- turbovec: Google TurboQuant for vector search (Rust + Python): 글이 소개하는 OSS 구현체의 저장소 페이지. 알고리즘 단계 전문, 벤치마크 조건, API 전체, 프레임워크 어댑터 상세를 담당한다. 이 글의 수치가 어긋나는 부분의 기준이 되는 페이지다.
- RAG-Anything 한국어 소개글 (PyTorchKR, 2026): 같은 작성자가 같은 게시판에 올린 다른 한국어 소개글. 원 자료를 한국어로 압축해 전달하는 같은 형식이라 서술 방식을 비교하기 좋다.