项目简介
基于 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常见问题 (5)
故障排除如何修复升级到 TurboVec 1.1.0 后,在包含 32K+ 向量的索引上掩码或允许列表搜索变慢 3–4 倍的问题?
升级到 TurboVec 1.1.1 或更高版本。此版本包含针对 #556 和 #558 的修复,这两个问题影响在包含 32,768 个及以上向量的索引上使用的分阶段 AVX-512/ARM 路径中的带掩码单查询搜索。
作为 1.1.0 上的临时解决方法,可通过以下方式强制使用经典内核:
export TURBOVEC_4BIT_PLANES=0
./your_search_binary
或者仅针对单条命令:
TURBOVEC_4BIT_PLANES=0 ./your_search_binary
出现该回归问题的原因是:1.1.0 的分阶段路径在符号扫描和重新评分期间通过缓慢的逐通道标量路径应用掩码;选择性掩码可能导致带掩码搜索比无掩码扫描更慢。
故障排除如何修复 TurboQuantIndex.swap_remove 或 IdMapIndex.remove 在并发搜索时挂起/耗时数十秒的问题?
这是 lock_write_gil_aware 中的活锁(livelock),而不是普通的锁竞争。它调用 try_write(),当返回 WouldBlock 时,会在 py.detach() 内获取一个阻塞式写锁,但在重试之前又立即将其释放。并发的搜索者可能会先重新获取读锁,因此删除操作可能反复在竞争中落败。
通过使用普通的阻塞式 lock_write,在一个 detached 闭包内执行删除操作,来修复 swap_remove 和 IdMapIndex.remove 的调用点:
py.detach(|| {
let _guard = lock_write(&lock);
// 在此处执行 swap_remove/remove;guard 是 !Send
});
不要获取后立即释放锁。如果保留无竞争快速路径很重要,可以使用一次 attached 的 try_write();如果它返回 WouldBlock,则回退到恰好一次 detached 的阻塞式获取,并在该闭包内执行操作。这与已经设有边界的 add 路径一致。
打过补丁的构建将 8 个并发 swap_remove 调用的耗时从总计 86.1 秒 / 最差 25.8 秒降低到了总计 1.13 秒 / p50 为 144 毫秒。请升级到包含 issue #484 修复的版本,或者应用该 PR。作为临时变通方案,可以将删除操作与搜索串行化,或者在调用 swap_remove()/remove() 之前暂停搜索线程。
故障排除如何修复 TurboVec 分阶段搜索遗漏最近邻并导致 k=10 时召回率下降的问题?
将 TurboVec 升级到 1.1.2,该版本修复了在 MedCPT 和 bge-small 等紧密聚集的嵌入向量上进行分阶段搜索时的召回率回退问题:
python -m pip install -U "turbovec>=1.1.2"
受影响的版本为 1.1.0 和 1.1.1。在这些版本中,第一个符号位阶段仅保留 max(256, 20 × k) 个候选,因此在 k=10 时,对于具有强共享向量方向的语料库,它可能会在重打分之前丢弃真正的最近邻。版本 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%,召回率几乎与全索引扫描持平。
故障排除为什么 turbovec 在目标 basename 为 235 字节或更长时会泄漏 .tmp.* 文件且从不清理它们?
之所以出现这种情况,是因为临时文件的创建与过期临时文件的清理使用了不同的名称前缀。当 base + suffix 超过 NAME_MAX(255 字节)时,tmp_sibling 会截断目标文件的基本名(basename),而 sweep_stale_tmps 却是与完整的、未截断的基本名进行匹配。由于临时后缀为 21 字节,因此当基本名长度达到 235 字节时就会开始截断。修复方法是在 sweep_stale_tmps 中计算相同的截断点,并匹配 <base[..cut]>.tmp.,同时保留现有的 is_our_tmp_suffix 与 1 小时 mtime 保护条件。该问题已在 PR #524 中修复,后续跟进还覆盖了 sync()/write_full 路径。请升级到包含这些更改的构建版本。作为临时规避措施,请将目标文件的基本名保持在 234 字节或更短。若要仅手动清理旧的临时兄弟文件,请先进行检查:
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 即可删除这些文件。
故障排除为什么 TurboVec 的 allowlist 搜索在使用多个 Rayon 线程时比使用单个线程时更慢?
这是 TurboVec 1.0.0 中针对选择性掩码的一个已知回归问题。单查询的块并行阈值统计的是索引中的全部块,而不是掩码所允许的块。当向量数超过 32,768(即 1,024 个块)时,即使允许列表中只有一个 ID,也会进入并行路径并承担 Rayon 线程池交接的开销,导致 16 线程搜索比单线程搜索慢约 2×。
修复方法:将 TurboVec 升级到 1.1.1 或更高版本,该版本使带掩码的单查询搜索保持在快速路径上,并包含了相关的带掩码分段路由修复:
cargo add turbovec@1.1.1
# 或者,在已有 lockfile 的情况下
cargo update -p turbovec --precise 1.1.1
cargo build --release
使用以下命令验证解析到的版本:
cargo tree -p turbovec
如果暂时无法升级,可采用临时变通方案:以单个 Rayon 工作线程运行对延迟敏感的选择性单查询搜索:
RAYON_NUM_THREADS=1 ./target/release/your-binary
这样可以避免选择性掩码场景下的并行交接,但同时也会禁用整个进程范围内的 Rayon 并行,可能会降低稠密搜索或未加掩码搜索的速度。