OpenSource-Hub

turbovec

라이브러리

RyanCodrai/turbovec

TurboQuant 기반의 고효율 벡터 인덱스, Rust 코어 및 Python 바인딩.

개요

Google TurboQuant 알고리즘 기반으로 높은 압축률과 빠른 검색을 구현하며, 필터링 쿼리와 안정적인 ID를 지원하고 대부분의 벤치마크에서 FAISS를 능가합니다. Rust로 작성되었으며 Python 바인딩을 제공하여 손쉽게 통합할 수 있습니다.

README 미리보기

\n  \n\n\n\n  \n  \n  \n  \n\n\n---\n\n**A 10 million document corpus takes 31 GB of RAM as float32. turbovec fits it in 4 GB - and searches it faster than FAISS.**\n\nturbovec is a Rust vector index with Python bindings, built on Google Research's [**TurboQuant**](https://arxiv.org/abs/2504.19874) algorithm — a data-oblivious quantizer that matches the Shannon lower bound on distortion, with no codebook training and no separate train phase.\n\n- **Online ingest.** Add vectors, they're indexed — no train step, no parameter tuning, no rebuilds as the corpus grows.\n- **Faster than FAISS.** Hand-written NEON (ARM) and AVX-512BW (x86) kernels beat FAISS IndexPQFastScan by 12–20% on ARM and match-or-beat it on x86.\n- **Filter at search time.** Pass an id allowlist (or a slot bitmask) to `search()` and the kernel honours it directly. You always get up to `k` results from the allowed set — no over-fetching, no recall hit on selective filters.\n- **Pure local.** No managed service, no data leaving your machine or VPC. Pair with any open-source embedding model for a fully air-gapped RAG stack.\n\nBuilding RAG where privacy, memory, or latency matters? **You're in the right place.**\n\n## Python\n\n```bash\npip install turbovec\n```\n\n```python\nfrom turbovec import TurboQuantIndex\n\nindex = TurboQuantIndex(dim=1536, bit_width=4)\nindex.add(vectors)\nindex.add(more_vectors)\n\nscores, indices = index.search(query, k=10)\n\nindex.write("my_index.tq")\nloaded = TurboQuantIndex.load("my_index.tq")\n```\n\nNeed stable ids that survive deletes? Use `IdMapIndex`:\n\n```python\nimport numpy as np\nfrom turbovec import IdMapIndex\n\nindex = IdMapIndex(dim=1536, bit_width=4)\nindex.add_with_ids(vectors, np.array([1001, 1002, 1003], dtype=np.uint64))\n\nscores, ids = index.search(query, k=10)   # ids are your uint64 external ids\nindex.remove(1002)                         # O(1) by id\n\nindex.write("my_index.tvim")\nloaded = IdMapIndex.load("my_index.tvim")\n```\n\n### Hybrid retr

FAQ (5)

문제 해결
TurboVec 1.1.0으로 업그레이드한 후 벡터 수가 32K 이상인 인덱스에서 masked 또는 allowlist 검색이 3–4배 느려지는 문제를 어떻게 해결하나요?

TurboVec 1.1.1 이상으로 업그레이드하세요. 이 릴리스에는 32,768개 이상의 벡터로 구성된 인덱스에 사용되는 staged AVX-512/ARM 경로에서 마스크된 단일 쿼리 검색에 대한 수정 사항 #556 및 #558이 포함되어 있습니다. 1.1.0에서 임시 해결 방법으로 다음과 같이 classic 커널을 강제로 사용하세요:

export TURBOVEC_4BIT_PLANES=0
./your_search_binary

또는 하나의 명령으로 실행하려면:

TURBOVEC_4BIT_PLANES=0 ./your_search_binary

이 회귀는 1.1.0 staged 경로가 sign scan 및 rescoring 과정에서 느린 레인별 스칼라 경로를 통해 마스크를 적용했기 때문에 발생했으며, 선택적 마스크로 인해 마스크된 검색이 마스크되지 않은 스캔보다 느려질 수 있었습니다.

원본 Issue #557
문제 해결
동시 검색 환경에서 TurboQuantIndex.swap_remove 또는 IdMapIndex.remove가 hang 상태가 되거나 수십 초씩 걸리는 문제를 해결하는 방법은?

이것은 일반적인 락 경합이 아니라 lock_write_gil_aware에서 발생하는 라이브락(livelock)입니다. 이 코드는 try_write()를 호출하고, WouldBlock이 반환되면 py.detach() 내부에서 블로킹 쓰기 락을 획득하지만 재시도하기 전에 즉시 해제합니다. 동시에 실행되는 검색 스레드가 읽기 락을 먼저 다시 획득할 수 있으므로, 삭제 작업이 경쟁에서 반복적으로 밀릴 수 있습니다.

일반적인 블로킹 lock_write를 사용하여 분리된(detached) 클로저 하나 안에서 삭제를 수행하도록 swap_remove 및 IdMapIndex.remove 호출 지점을 수정하세요:

py.detach(|| {
    let _guard = lock_write(&lock);
    // perform swap_remove/remove here; the guard is !Send
});

락을 획득했다가 바로 해제하지 마세요. 경합이 없는 경우의 빠른 경로를 유지하는 것이 중요하다면, 연결된(attached) try_write()를 한 번 사용하고, WouldBlock이 반환되면 정확히 한 번의 분리된 블로킹 획득으로 폴백하여 해당 클로저 안에서 작업을 실행하세요. 이는 이미 상한이 정해진 add 경로와 일치합니다.

패치가 적용된 빌드에서는 8개의 동시 swap_remove 호출이 총 86.1초 / 최악 25.8초에서 총 1.13초 / p50 144ms로 감소했습니다. issue #484에 대한 수정이 포함된 릴리스로 업그레이드하거나 해당 PR을 적용하세요. 임시 해결 방법으로는 삭제 작업을 검색과 직렬화하거나 swap_remove()/remove()를 호출하기 전에 검색 스레드를 일시 중지하세요.

원본 Issue #484
문제 해결
TurboVec 단계적 검색에서 최근접 이웃이 누락되고 k=10에서 recall이 저하되는 문제를 해결하는 방법은?

TurboVec를 1.1.2로 업그레이드하세요. 이 버전은 MedCPT 및 bge-small처럼 촘촘하게 군집된 임베딩에서 발생하는 staged-search 재현율 회귀를 수정합니다:

python -m pip install -U "turbovec>=1.1.2"

영향을 받는 버전은 1.1.0과 1.1.1입니다. 해당 릴리스에서는 첫 번째 sign-bit 단계가 max(256, 20 × k)개의 후보만 유지하므로, 강한 공유 벡터 방향을 가진 코퍼스에서는 k=10일 때 rescoring 전에 실제 최근접 이웃이 버려질 수 있습니다. 버전 1.0.0은 영향을 받지 않았습니다.

즉시 업그레이드할 수 없는 경우에는 해결 방법으로 더 큰 k로 검색한 뒤 결과를 잘라내세요. 예를 들어 search(query, k=64)를 호출하고 처음 10개의 결과만 사용합니다. TurboVec 1.1.1에서 MedCPT 4-bit로 테스트한 결과, 이 방법은 테스트된 모든 쿼리에서 실제 최근접 이웃을 상위 10개 안에 포함시켰지만, search(query, k=10)은 약 0.37–0.40%의 쿼리에서 이를 놓쳤습니다.

업그레이드 후에는 전체 스캔과 비교하여 재현율을 확인하세요. 보고된 101K PubMed 슬라이스에서 4-bit k=10의 동일 결과 일치율은 MedCPT의 경우 82.6%에서 99.1%로, bge-small의 경우 75.6%에서 96.6%로 향상되었으며, 재현율은 전체 인덱스 스캔과 거의 일치했습니다.

원본 Issue #562
문제 해결
대상 basename이 235바이트 이상일 때 turbovec이 `.tmp.*` 파일을 누수시키고 정리하지 않는 이유는 무엇인가요?

이 문제는 임시 파일 생성과 오래된 임시 파일 정리(stale-temp sweeping)가 서로 다른 이름 접두사를 사용하기 때문에 발생합니다. tmp_sibling은 base + suffix가 NAME_MAX(255바이트)를 초과할 때 대상 basename을 잘라내지만, sweep_stale_tmps는 잘리지 않은 전체 basename과 비교합니다. 임시 접미사가 21바이트이므로, basename 길이가 235바이트부터 잘라내기가 시작됩니다. 수정 사항은 sweep_stale_tmps에서 동일한 잘라내기 지점을 계산하여 <base[..cut]>.tmp.와 비교하도록 하는 것이며, 기존의 is_our_tmp_suffix 검사와 1시간 mtime 가드는 그대로 유지합니다. 이 문제는 PR #524에서 수정되었으며, 후속 작업에서 sync()/write_full 경로도 다루었습니다. 해당 변경 사항이 포함된 빌드로 업그레이드하세요. 임시 해결 방법으로는 대상 basename을 234바이트 이하로 유지하세요. 오래된 임시 sibling 파일만 수동으로 회수하려면, 먼저 다음을 확인하세요:

find /path/to/dest -maxdepth 1 -type f -regextype posix-extended \
  -regex '.+\.tmp\.[0-9]+\.[0-9]+\.[0-9a-fA-F]{8}$' -mmin +60 -print

그런 다음 -print를 -delete로 바꾸어 확인된 파일을 삭제하세요.

원본 Issue #488
문제 해결
TurboVec allowlist 검색이 하나의 스레드를 사용할 때보다 여러 개의 Rayon 스레드를 사용할 때 더 느린 이유는 무엇인가요?

이것은 선택적 마스크(selective mask)에서 발생하는 TurboVec 1.0.0의 알려진 회귀 문제입니다. 단일 쿼리 블록 병렬 임계값이 마스크가 허용하는 블록 대신 인덱스의 모든 블록을 계산합니다. 32,768개 벡터(1,024개 블록)를 초과하면 ID가 하나뿐인 allowlist라도 병렬 경로로 진입하여 Rayon 풀 핸드오프 비용을 치르게 되므로, 16-스레드 검색이 1-스레드 검색보다 약 2배 느려집니다.

해결 방법: TurboVec를 1.1.1 이상으로 업그레이드하세요. 해당 버전은 마스크된 단일 쿼리 검색을 빠른 경로로 유지하며 관련된 마스크된 staged-route 수정 사항도 포함합니다:

cargo add turbovec@1.1.1
# 또는 기존 lockfile이 있는 경우
cargo update -p turbovec --precise 1.1.1
cargo build --release

다음 명령으로 확인된(resolved) 버전을 확인하세요:

cargo tree -p turbovec

업그레이드할 수 없는 경우의 임시 해결 방법: 지연 시간에 민감한 선택적 단일 쿼리 검색을 하나의 Rayon 워커로 실행하세요:

RAYON_NUM_THREADS=1 ./target/release/your-binary

이렇게 하면 선택적 마스크에 대한 병렬 핸드오프를 피할 수 있지만, 프로세스 전체의 Rayon 병렬 처리도 비활성화되므로 dense하거나 마스크되지 않은 검색이 느려질 수 있습니다.

원본 Issue #554