이 레포가 무엇을 하는 물건인가.
벡터 검색의 오랜 상식은 "압축하려면 먼저 데이터로 코드북을 학습해야 한다"였습니다. turbovec이 구현한 TurboQuant는 그 학습 단계를 수학으로 통째로 없앴습니다 — 벡터를 넣는 순간 바로 인덱싱되고, 데이터가 늘어나도 재학습이 없습니다.
turbovec은 벡터 인덱스 (vector index, 임베딩 벡터를 저장하고 유사한 것을 빠르게 찾아주는 자료구조)입니다. RAG (Retrieval-Augmented Generation, 검색 증강 생성) 시스템에서 "질문과 가장 비슷한 문서 조각 10개를 찾아와"라는 요청을 처리하는 핵심 부품이죠. 코어는 Rust로 작성됐고, pip install turbovec 한 줄이면 Python에서 바로 씁니다.
특징은 세 가지입니다. ① 학습 제로 — 코드북 훈련, 데이터 보정, 재빌드가 전혀 없습니다. ② FAISS보다 빠름 — 손으로 짠 SIMD 커널이 ARM에서 FAISS FastScan을 10~19% 앞섭니다. ③ 완전 로컬 — 관리형 서비스 없이 내 머신/VPC 안에서만 돕니다. 오픈소스 임베딩 모델과 짝지으면 완전 폐쇄망 RAG 스택이 됩니다.
트렌딩 이유 · 경쟁 대비 장점.
벡터 DB 시장은 Pinecone 같은 관리형 서비스와 FAISS 같은 로컬 라이브러리로 나뉩니다. 로컬 진영의 표준인 FAISS의 PQ (Product Quantization, 곱 양자화 — 벡터를 조각내 각 조각을 코드북으로 압축)는 강력하지만 한 가지 전제가 있습니다: 먼저 데이터로 코드북을 학습해야 한다는 것. turbovec이 트렌딩 1위에 오른 이유는 이 전제를 깬 구글의 TurboQuant 논문(2025년 4월 공개, ICLR 2026 채택)을 가장 빠르게, 실전 품질로 구현한 레포이기 때문입니다.
| 비교 항목 | FAISS PQ 계열 | turbovec (TurboQuant) |
|---|---|---|
| 코드북 학습 | k-means 훈련 필요 (데이터 한 번 훑어야 함) | 불필요 — 수학으로 미리 계산 |
| 데이터 추가 | 분포가 변하면 재학습/재빌드 | 그냥 add() — 분포 무관 |
| 검색 속도 | FastScan SIMD (기준선) | ARM +10~19%, x86 4-bit +1~6% |
| 이론 보장 | 경험적 성능 | 샤논 한계의 2.7배 이내 왜곡 (증명됨) |
PQ는 "이 데이터 집합에 맞는 압축 사전(코드북)"을 k-means로 학습합니다. 문서가 계속 늘어나는 실서비스에선 분포가 바뀔 때마다 재학습을 고민해야 하고, 새 도메인 데이터엔 기존 코드북이 안 맞을 수 있습니다. 학습엔 시간도, 대표 샘플도 필요합니다.
TurboQuant의 통찰: 벡터를 정규화하고 무작위 회전을 걸면, 입력 데이터가 뭐든 각 좌표가 수학적으로 알려진 분포(Beta 분포)를 따르게 됩니다. 분포를 미리 알면 최적 양자화 구간도 미리 계산할 수 있죠. 그래서 데이터를 한 번도 보지 않고 — "data-oblivious" — 코드북이 완성됩니다.
또 하나의 매력은 정직한 벤치마크입니다. README가 "우리가 GloVe 저차원에선 FAISS에 3~6점 진다"는 약점까지 그래프와 JSON 원본으로 공개하고, 비교 기준을 논문보다 더 강한 FAISS 기본값으로 잡았다고 명시합니다. 벤치마크 부풀리기가 흔한 분야에서 신뢰를 산 부분입니다.
Rust 코어 · Python 바인딩 · 벤치마크 인프라.
레포는 Cargo 워크스페이스 (workspace, 여러 Rust 크레이트를 한 저장소에서 함께 빌드하는 구조)로 구성됩니다. 핵심 계산은 전부 turbovec 크레이트(Rust)에 있고, turbovec-python이 그걸 Python으로 노출합니다.
| 레이어 | 기술 / 버전 | 역할 |
|---|---|---|
| 코어 엔진 | Rust 1.70+ (turbovec 0.9.0) | 인코딩·검색·직렬화 전부. MIT 라이선스. |
| 수학 라이브러리 | faer · statrs · ndarray(+BLAS) | faer는 QR 분해(회전행렬 생성), statrs는 Beta 분포 계산, ndarray는 행렬 연산(macOS는 Accelerate, Linux는 OpenBLAS). |
| 병렬화 | rayon 1.10 | 인코딩·검색을 코어 수만큼 자동 분산. |
| SIMD 커널 | NEON (ARM) · AVX-512BW / AVX2 (x86) | 손으로 짠 점수 계산 커널. 런타임에 CPU 기능을 감지해 선택. |
| Python 바인딩 | PyO3 0.27 + numpy + maturin | Rust 함수를 Python 클래스로 노출. abi3-py39라 Python 3.9~3.14 한 바이너리로 호환. |
| 프레임워크 연동 | LangChain · LlamaIndex · Haystack · Agno | pip install turbovec[langchain]처럼 옵션 설치로 VectorStore 어댑터 제공. |
| 재현성 | rand_chacha (시드 42 고정) | 회전행렬이 결정적이라 어디서 빌드해도 같은 인덱스. |
벡터 하나가 들어가서 검색 결과로 나오기까지.
먼저 전체 그림입니다. 파이프라인은 크게 인코딩(add)과 검색(search) 두 갈래이고, 둘 다 같은 "회전된 공간"에서 만납니다.
이제 벡터 하나의 여정을 따라가 봅시다. 1536차원 OpenAI 임베딩 하나(6,144바이트)가 index.add()로 들어오면:
① 정규화. 벡터의 길이(norm)를 떼어내 float 하나로 따로 저장합니다. 남은 것은 구면 위의 "방향"뿐입니다. ② 회전. 모든 벡터에 같은 무작위 직교 행렬을 곱합니다(rotation.rs — ChaCha8 난수를 시드 42로 고정하고 faer로 QR 분해). 이 회전이 마법의 핵심인데, 회전 후엔 어떤 입력이든 각 좌표가 Beta((d-1)/2, (d-1)/2) 분포를 따릅니다. ③ TQ+ 보정. 실제 데이터는 이론 분포에서 살짝 어긋나므로, 좌표마다 shift/scale 두 파라미터를 5%/95% 분위수로 맞춥니다(encode.rs — 1,000개 미만이면 통계가 불안정해 보정을 건너뜀). ④ 양자화. 분포를 아니까 최적 구간도 미리 계산됩니다 — codebook.rs의 Lloyd-Max 알고리즘이 4비트면 16개, 2비트면 4개의 구간 경계와 대푯값을 "수학에서" 뽑아냅니다. ⑤ 패킹. 좌표 하나가 0~15의 작은 정수가 됐으니 pack.rs가 바이트에 꽉꽉 눌러 담습니다. 결과: 6,144바이트 → 4비트 기준 768바이트.
검색은 반대 방향의 잔머리입니다. 데이터베이스 쪽을 복원(decompress)하는 게 아니라 쿼리를 한 번만 같은 공간으로 회전시키고, 코드북 대푯값과의 곱을 미리 표로 만들어(LUT (Look-Up Table, 룩업테이블 — 계산 결과를 미리 적어둔 표)) 압축된 코드에서 바로 점수를 읽습니다. 이 안쪽 루프가 search.rs(1,821줄, 레포에서 가장 큰 파일)의 SIMD 커널입니다 — 32개 벡터를 한 블록으로 묶어 NEON/AVX-512 레지스터에서 니블(4비트) 단위로 테이블을 찾습니다.
도서관 장서 100만 권의 위치를 외우는 두 가지 방법이 있습니다. FAISS PQ는 "우리 도서관 책들을 쭉 훑어보고 분류 체계를 만드는" 사서입니다 — 정확하지만 신간이 쏟아지면 분류 체계를 다시 짜야 하죠. TurboQuant는 "책을 회전 선반에 올리면 어느 도서관이든 통하는 표준 분류표"를 수학으로 미리 만들어 둔 쪽입니다. 책(데이터)을 볼 필요 자체가 없습니다.
이 레포의 "정상적인 모양새" 하나를 짚자면: 모든 주석이 '왜'를 설명합니다. 예컨대 입력 검증 코드엔 "NaN이 들어오면 어떤 경로로 인덱스가 조용히 오염되는지"가, 상수 하나엔 "왜 하필 1000개인지" 실험 근거가 달려 있습니다. 주석 읽는 것만으로 수치 컴퓨팅의 함정 목록을 배울 수 있는 코드베이스입니다.
어떤 폴더가 무슨 일을 하나.
| 위치 | 주목할 점 |
|---|---|
turbovec/src/search.rs | 레포의 심장. 아키텍처별 unsafe SIMD 커널이 모여 있고, 마스크 필터링의 블록 건너뛰기 통계까지 노출. |
turbovec/tests/ | distortion(이론 왜곡률 검증), concurrent_search(스레드 안전성), io_versioning(파일 포맷 호환) — 수치 라이브러리 테스트의 모범. |
benchmarks/suite/ | "벤치마크 하나 = 자기완결 스크립트 하나" 패턴. 결과는 JSON으로 커밋되어 그래프가 재현 가능. |
turbovec-python/python/ | 코어를 건드리지 않고 프레임워크 어댑터만 순수 Python으로 — 바인딩과 생태계 연동의 분리. |
이 레포에서 배울 만한 것 + 어디를 보면 되는지.
고차원 단위 벡터를 무작위 회전하면 좌표가 알려진 분포를 따른다는 사실 하나가 학습 단계를 통째로 지웁니다. rotation.rs(51줄)와 codebook.rs(126줄)는 짧아서 논문 없이도 읽힙니다. "Beta 분포 → Lloyd-Max로 최적 구간"의 흐름만 따라가면 핵심을 잡은 겁니다.
search.rs는 NEON과 AVX-512BW 커널을 따로 들고, x86에선 is_x86_feature_detected!로 실행 시점에 CPU를 확인해 AVX2 폴백으로 안전하게 내려갑니다. "최신 명령어를 쓰되 구형 CPU에서도 돌게 하는" 실전 패턴입니다.
turbovec-python/src/lib.rs 377줄이 전부입니다. numpy 배열을 복사 없이 받고, abi3로 Python 버전 호환을 한 방에 풀고, 나머지 생태계 연동(LangChain 등)은 순수 Python 레이어에 맡기는 — 깔끔한 3층 구조.
TurboQuantIndex는 위치 기반(빠르고 작음), IdMapIndex는 삭제에도 안 변하는 외부 id 제공. swap_remove가 "순서를 안 지킨다"는 것까지 문서에 명시하고, 안정성이 필요하면 다른 타입을 쓰라고 안내합니다 — 숨기는 대신 이름으로 드러내는 API 설계.
인-커널 필터링도 눈여겨볼 만합니다. 검색 후 결과를 걸러내는(post-filter) 게 아니라 커널 안에서 허용 목록 밖 벡터를 힙에 아예 안 넣기 때문에, 필터를 걸어도 항상 k개가 꽉 채워져 나옵니다:
# SQL/BM25 1차 검색이 추린 후보 id 안에서만 벡터 검색 (하이브리드 검색)
allowed = np.array([1003, 1010, 1042], dtype=np.uint64)
scores, ids = idx.search(queries, k=10, allowlist=allowed)
돌리려면 무엇이 필요한가.
| 항목 | 요구사항 |
|---|---|
| Python 사용 | Python 3.9~3.14 + numpy. pip install turbovec 끝 (Linux·macOS wheel 제공). |
| Rust 사용 | Rust 1.70+. cargo add turbovec. |
| CPU (x86) | AVX2 지원(2013년 Haswell 이후)이 기본선. AVX-512BW가 있으면 자동으로 더 빠른 커널 사용. |
| CPU (ARM) | NEON — Apple Silicon·최신 ARM 서버는 모두 해당. |
| 메모리 | 핵심 셀링포인트. 1536차원 1,000만 벡터 기준 float32 ≈ 61GB → 4비트 ≈ 7.7GB, 2비트 ≈ 3.8GB (+norm/id 오버헤드 약간). |
| GPU | 불필요 — 순수 CPU 라이브러리. |
난이도별로 손에 익히는 단계.
pip install turbovec 후 무작위 벡터 1만 개로 인덱스를 만들고 검색해 봅니다. 복사만 하면 OK:
import numpy as np
from turbovec import TurboQuantIndex
vecs = np.random.randn(10000, 384).astype(np.float32)
index = TurboQuantIndex(dim=384, bit_width=4)
index.add(vecs)
scores, idx = index.search(vecs[:5], k=10)
print(idx[0]) # 첫 쿼리는 자기 자신(0번)이 1등으로 나와야 정상
같은 데이터로 bit_width=2와 4 인덱스를 만들고, numpy 브루트포스 정답과 비교해 recall@10 (상위 10개 중 진짜 정답이 몇 개나 들어있나)을 직접 계산해 봅니다. README의 "2비트는 16배 압축이지만 저차원에서 recall이 떨어진다"를 내 데이터로 확인하는 과제.
pip install turbovec[langchain] sentence-transformers로 임베딩 모델까지 로컬에 두고, 문서 100개를 넣어 질문-답변 검색기를 만듭니다. 외부 API 호출이 0인지(완전 폐쇄망인지) 확인해 보세요. write()/load()로 인덱스를 파일로 저장·복원하는 것까지.
IdMapIndex에 날짜를 id에 인코딩해 넣고, "최근 7일 문서만" allowlist로 제한한 검색과 전체 검색의 결과·속도를 비교합니다. post-filter 방식(전체 검색 후 거르기)을 직접 짜서 결과 개수가 어떻게 달라지는지도 관찰.
레포를 클론해 benchmarks/suite/의 스크립트 하나를 내 머신에서 돌려보고 README 수치와 비교합니다. Rust가 있다면 cargo build --release 후 turbovec/tests/kernel_correctness.rs를 읽으며 SIMD 커널과 스칼라 폴백이 같은 답을 내는지 검증하는 방식을 배워 보세요.
한 주씩 따라가는 계획.
| 주차 | 주제 | 학습 자료 |
|---|---|---|
| 1주차 | 벡터 검색 기초 — 임베딩·코사인 유사도·ANN 개념, 실습 1 | README "How it works" · docs/api.md |
| 2주차 | 양자화의 수학 — PQ vs 스칼라 양자화, Lloyd-Max, Beta 분포 | codebook.rs · TurboQuant 논문 1~3장 |
| 3주차 | 인코딩 파이프라인 정독 — 회전·TQ+ 보정·패킹 | rotation.rs → encode.rs → pack.rs 순서로 |
| 4주차 | SIMD 입문 — LUT 스코어링이 왜 빠른가 | search.rs 주석 · FAISS FastScan 위키 |
| 5주차 | Rust↔Python 바인딩 — PyO3·maturin·abi3 | turbovec-python/ · PyO3 공식 가이드 |
| 6주차 | 실전 응용 — RAG 연동·하이브리드 필터링, 실습 3~4 | docs/integrations/ · 본문 실습 |
본문에 나온 용어 빠른 참조.
| 용어 | 의미 |
|---|---|
| 벡터 인덱스 | 임베딩 벡터를 저장하고 "가장 비슷한 k개"를 빠르게 찾는 자료구조. |
| ANN | Approximate Nearest Neighbor — 정확도를 약간 양보하고 속도를 얻는 근사 최근접 탐색. |
| 양자화 | 연속값을 소수의 등급으로 뭉뚱그려 저장 공간을 줄이는 압축 기법. |
| TurboQuant | 구글 리서치의 데이터 무관(data-oblivious) 양자화 알고리즘. 학습 없이 샤논 한계 2.7배 이내 왜곡 달성 (ICLR 2026). |
| 코드북 | 압축에 쓰는 "대푯값 사전". PQ는 데이터로 학습하고, TurboQuant는 수학으로 미리 계산. |
| Lloyd-Max | 알려진 확률분포에 대해 평균제곱오차를 최소화하는 양자화 구간을 찾는 고전 알고리즘. |
| Beta 분포 | 고차원 구면 위 단위 벡터를 회전했을 때 각 좌표가 따르는 분포. 차원이 크면 정규분포에 수렴. |
| FAISS | 메타의 벡터 검색 표준 라이브러리. turbovec이 속도·압축 비교 기준으로 삼는 상대. |
| SIMD / NEON / AVX-512 | 한 CPU 명령으로 여러 데이터를 동시 처리하는 기술과 그 ARM/x86 구현. |
| LUT | Look-Up Table — 곱셈 결과를 미리 표로 만들어 검색 때는 "찾아 읽기"만 하는 가속 기법. |
| recall@k | 상위 k개 결과 중 진짜 정답(브루트포스 기준)이 포함된 비율. ANN 정확도의 표준 지표. |
| RAG | Retrieval-Augmented Generation — 검색으로 찾은 문서를 LLM에 넣어 답을 만드는 패턴. |
| PyO3 / maturin | Rust 코드를 Python 패키지로 만드는 바인딩 라이브러리와 빌드 도구. |
| rayon | Rust의 데이터 병렬화 라이브러리 — 반복문을 코어 수만큼 자동 분산. |
| swap_remove | 마지막 원소를 삭제 자리에 옮겨 넣는 O(1) 삭제. 빠르지만 순서가 바뀜 — Rust Vec의 관례를 따른 이름. |