Annoy

Annoy

Spotify 开源的 C++/Python 近邻检索库,索引以静态文件 mmap 共享

核心功能

Annoy(Approximate Nearest Neighbors Oh Yeah)由 Spotify 编写,用随机投影建成二叉树森林做近似最近邻查询。它最大的特点是把索引写成只读静态文件并 mmap 进内存,多个进程可以共享同一份索引。Spotify 用它做音乐推荐。与 FAISS、hnswlib 相比,它放弃了在线增删,换来极小的内存占用和几乎瞬时的索引加载。

功能亮点

索引就是文件

建索引与加载完全解耦,索引文件可以分发到生产环境或 Hadoop 任务里被直接映射使用

多进程共享内存

load 走 mmap,同机多个进程映射同一份索引,不必每个进程各自复制一遍向量数据

两个参数就够调

官方称 n_trees 与 search_k 大致相互独立,分别对应索引体积精度和单次查询耗时

内存占用优先

README 说明设计上刻意压小索引体积,Spotify 在数百万音轨的高维空间中以此为首要约束

适用场景

• 推荐系统中对用户/物品 embedding 做相似召回,索引离线构建后作为文件分发到线上
• 多进程或多 worker 的服务共用一份只读索引,避免每个进程各占一份向量内存
• 内存受限的机器上检索大规模向量,用 on_disk_build 把索引直接建在磁盘上

安装配置

bash
PyPI(README 给出的方式):
pip install --user annoy

C++(header-only):
克隆仓库后在代码中 #include "annoylib.h" 即可

README 另外指出 Annoy 在 conda-forge 上有 python-annoy 包,覆盖 Linux、macOS 与 Windows。

使用方法

python
README 中的 Python 示例:

from annoy import AnnoyIndex
import random

f = 40  # Length of item vector that will be indexed

t = AnnoyIndex(f, 'angular')
for i in range(1000):
    v = [random.gauss(0, 1) for z in range(f)]
    t.add_item(i, v)

t.build(10) # 10 trees
t.save('test.ann')

# ...

u = AnnoyIndex(f, 'angular')
u.load('test.ann') # super fast, will just mmap the file
print(u.get_nns_by_item(0, 1000)) # will find the 1000 nearest neighbors

要点(来自官方 API 说明):
- AnnoyIndex(f, metric) 中 metric 可取 "angular"、"euclidean"、"manhattan"、"hamming"、"dot"
- build(n_trees, n_jobs=-1) 之后不能再 add_item
- get_nns_by_item(i, n, search_k=-1, include_distances=False),search_k 不传时默认为 n_trees * n
- 也可用 get_nns_by_vector(v, n, ...) 按向量查询
- C++ API 与之基本一致,#include "annoylib.h" 即可

关键指标

尚未核验对标产品,此处只列本工具自身指标,不做对比结论。

指标Annoy
价格免费
开源
上手难度入门

相关工具

优点

  • 索引以静态文件形式存在、load 走 mmap,加载几乎瞬时且可跨进程共享,README 明确把这点列为区别于其他库的核心特性
  • 官方说明设计上尽量压小索引体积,Spotify 在数百万音轨的高维向量场景中把内存占用作为首要约束
  • 调参面很窄,只有 n_trees 和 search_k 两个参数,且官方说明两者大致相互独立
  • 支持 angular / euclidean / manhattan / hamming / dot 五种度量,Hamming 距离底层打包成 64 位整数并使用位计数原语

缺点

  • 索引一旦 build 完成就不能再 add_item,也没有删除接口——README 明确写明 index creation is separate from lookup
  • 只接受非负整数作为 item id,并且会按 max(id)+1 预分配内存,其他 id 体系需要自己维护映射表
  • README 说明维度不太高(小于 100)时效果更好,虽然到 1000 维仍能用
  • README 直言不做数值边界检查(no bounds checking performed on the values),传错数据不会报错