向量数据库

Annoy 与树形索引

核心索引算法约 20 分钟kp-010

前置知识

一句话定义

Annoy 用随机投影递归二分把向量空间切成一棵树(默认建多棵树),查询时沿每棵树走到叶子再合并候选:为"静态、只读、多进程内存共享"的推荐场景而生,是理解树形 ANN 路线的最小范本。

为什么重要

Annoy 是 ANN 工程化的早期代表作(Erik Bernhardsson 为 Spotify 音乐推荐所写),其两个贡献超越了算法本身:把"随机化 + 多棵树投票"的稳健思想带入工程,以及 ann-benchmarks 评测框架确立了行业比较标准。读懂 Annoy 的取舍,能理解为什么嵌入时代图索引最终胜出——这是选型判断力的历史坐标。

前置知识

kp-006(Flat 基线);二叉树概念。

核心概念

原理与机制

建树:每棵树独立随机采样超平面序列,把空间切成细长单元格;同一向量出现在每棵树的一个叶子里。查询:对每棵树从根下行到叶子(约 log₂N 步),收集叶子内向量,跨树合并、按真实距离排序取 top-k;search_k 决定"每棵树多认真"。

结构取舍(与 HNSW 对照):

维度AnnoyHNSW
结构静态多树动态图
增量插入不支持,需重建支持(图可扩)
多进程内存共享mmap 原生各进程独立或依赖系统实现
高维召回曲线明显弱于 HNSW/PQ 系强
典型场景旧式推荐、维度不高、只读通用在线检索

树形路线在高维的根本困难:随机超平面切分在高维近似"均分球面",叶子内近邻命中率随维度升高而下降,必须靠"多棵树 + 大 search_k"硬补——这正是 ann-benchmarks 上 Annoy 曲线整体位于 HNSW 右下方(同等召回需要更多延迟)的原因。

公式或模型

本节不适用:结构对比篇,无新公式;随机投影的理论基础(Johnson–Lindenstrauss 引理)见延伸阅读。

图示

         根(随机超平面 h1)
        ╱              ╲
   左子(h2)          右子(h3)
   ╱     ╲           ╱     ╲
 叶A(点集) 叶B     叶C      叶D
查询: 每棵树选一侧下行(可回溯) → 收集叶子点 → 合并排序 top-k

实例或案例

Spotify 场景画像:千万曲目、128 维(旧式音频特征)、模型每天全量重训一次、几十个 worker 进程只读查询。Annoy 用 2 倍内存换来"一次构建、多进程零拷贝共享",完美贴合;同样的库若要求实时增删曲目,Annoy 立即出局——静态共享与动态更新是它和图索引的分界线。

直观类比

Annoy 像一群考官(n_trees 棵树)各自按不同目录分类法把图书馆分层分架;找书时每位考官指一个书架,把几个书架的书都翻出来比对(search_k)。分类法固定后不能临时改(只读),想找得更准只能多翻几架。

常见误区

  1. "Annoy 适合新项目做嵌入检索"——高维嵌入下它的召回-延迟曲线已被图索引全面超越,新项目选它只剩"多进程 mmap 共享"这一个理由。
  2. "search_k 默认值总是够用"——默认约 n_trees × k,对高召回目标(97%+)通常不足,需显式调大并重测。
  3. "树形索引都能增量更新"——Annoy 是纯静态构建,任何写变化都要全量重建,写多场景直接排除。

自测题

  1. n_trees 与 search_k 分别在哪一端发力?
  2. 答:n_trees 决定候选覆盖广度(建库侧,动它要重建);search_k 决定查询探查深度(在线侧,可随时调大换召回)。

  3. Annoy 在今天还有哪些不可替代的场景?
  4. 答:超大规模只读索引需要多进程 mmap 共享内存、或作为轻量无依赖方案嵌入单机应用。

  5. 为什么高维下树形切分的召回吃亏?
  6. 答:高维空间中随机超平面近乎均分各方向,"近邻对"被切开的概率升高,叶子内近邻密度下降,只能靠更多树与更大 search_k 补偿。

与其他知识点的关系

kp-008 的 HNSW 是它今天的替代者;kp-004 的"维度灾难"解释了树形路线的衰落;kp-005 的流派谱系里它是"树派"代表。

延伸阅读

Bernhardsson, "Annoy: Approximate Nearest Neighbors in C++/Python"(开源项目与作者博客,2013 起)——树形 ANN 的工程实录与 mmap 设计细节。

相关知识点