TANNS 完整端到端流程梳理

Codex版

这篇论文的方法可以概括为一条完整主线:

复制代码
带有效期的向量事件流
        ↓
按时间顺序处理插入 / 失效事件
        ↓
维护"当前有效"的 HNSW 式邻近图
        ↓
为每个节点保存主邻居 + 备份邻居
        ↓
把每次邻居变化记录为历史版本
        ↓
用历史邻居树 HNT 压缩这些版本
        ↓
收到带查询时间 ts 的向量查询
        ↓
动态还原 ts 时刻的邻居关系
        ↓
在该历史图快照上执行 HNSW 搜索
        ↓
返回当时有效的 k 个近似最近邻

下面按照"数据进入系统 → 索引更新 → 数据失效 → 历史压缩 → 查询执行"的顺序展开。

1. 数据与查询进入系统

每个向量 \(u_i\) 除了高维特征,还带有两个时间属性:

\ u_i=\[\\text{vector},u_i.s,u_i.e) \\

其中:

  • \(u_i.s\):向量开始有效的时间;
  • \(u_i.e\):向量失效的时间;
  • 写入时通常只知道 \(u_i.s\),并不知道未来何时失效;
  • 查询由 \((q,t_s,k)\) 构成,即在时间 \(t_s\) 的有效数据中,寻找与查询向量 \(q\) 最相似的 k 个向量。

论文将其定义为:

The timestamp approximate nearest neighbor search TANNS(D, ts, q, k) returns a subset of k valid vectors that are approximately closest to q.

例如,查询 2024 年 6 月的知识时,只允许检索在该时刻有效的文档向量,2025 年新增的信息不能参与搜索。


2. 系统按时间顺序接收更新事件

系统把数据变化组织为时间事件流:

复制代码
t1:插入向量 u1
t2:插入向量 u2
t3:向量 u1 失效
t4:插入向量 u3
......

论文为简化分析,假定每个离散时间戳只发生一次插入或失效。实际系统可以把同一批更新拆成多个逻辑时间戳。

整个索引不为每个时间戳重新构建一份 HNSW,而是维护一张持续演化的统一图,即时间戳图(Timestamp Graph)。


3. 新向量插入时间戳图

假设新向量 \(o\) 在时间 t 开始有效。

3.1 搜索候选邻居

首先在当前有效图上执行一次类似 HNSW 的搜索,找出 \(M'\) 个候选邻居:

\ cand=\\text{Search}(TG,o,M') \\

论文默认:

  • \(M=16\):正式邻居数量;
  • \(M'=200\):构图时的候选邻居数量。
3.2 从候选中选择 2M 个邻居

传统 HNSW 只选择 M 个邻居,而论文选择最多 2M 个:

\ TG\[o,Bo=\text{Select-Nbrs}(o,cand,2M) \]

其中:

  • 最近、结构质量较好的 M 个进入主邻居列表 \(TGo\);
  • 剩余 M 个进入备份邻居列表 \(Bo\)。

通俗来说,主邻居是当前图中的正式连接,备份邻居是"替补席"。以后主邻居失效时,可以直接从替补席补位,不必立即重新搜索整个图。

3.3 建立反向连接

对于 \(o\) 的每个主邻居 u,系统检查是否应当把 o 加入 u 的邻居列表:

  • 如果 o 比 u 当前最差的邻居更合适,而且没有被其他邻居支配:
    • 把 u 原来最差的主邻居移入 \(Bu\);
    • 把 o 加入 \(TGu\);
    • 记录 u 在时间 t 的新邻居版本。
  • 如果 o 不足以成为主邻居:
    • 把 o 放入 \(Bu\);
    • 若备份数量超过 M,删除最远的备份。

因此,一次插入只局部影响 o 及其附近大约 M 个节点,而不需要重建整张图。


4. 向量失效时维护图连通性

假设向量 o 在时间 t 失效。

如果直接删除 o,周围节点的邻居数量会逐渐减少,严重时会使图断开,导致 HNSW 搜索无法到达正确区域。因此系统对所有与 o 相连的节点 u 逐一处理。

情况一:o 只是 u 的备份邻居

直接从 \(Bu\) 删除 o,不改变图结构。

情况二:o 是 u 的主邻居

首先从 \(TGu\) 删除 o。

如果 \(Bu\) 不为空:

\ cand=TG\[u\cup Bu \]

然后重新执行邻居选择,从这些局部候选中选出新的 M 个主邻居,其余继续作为备份。

如果 \(Bu\) 已经为空,才执行一次代价更高的全图搜索:

\ cand=\\text{Search}(TG,u,M') \\

再重新选择 M 个主邻居和 M 个备份邻居。

最后,把 u 在时间 t 的新邻居列表记录下来。

o 自身如何处理

当 o 失效时:

  • 清空当前使用的 \(Bo\);
  • o 不再参与当前时间之后的搜索;
  • 但不能删除 o 的历史邻居记录,因为过去时间戳的查询仍可能访问它。

这就是系统能够执行"历史时间旅行查询"的基础。


5. 形成历史邻居版本

随着插入和失效不断发生,一个节点 o 的邻居可能这样变化:

时间 主邻居
\(t_1\) \(u_3,u_4\)
\(t_2\) \(u_1,u_3\)
\(t_3\) \(u_1,u_3\)
\(t_4\) \(u_1,u_2\)
\(t_5\) \(u_2,u_6\)
\(t_6\) \(u_1,u_6\)
\(t_7\) 无,o 已失效

未压缩的时间戳图会保存这些历史版本。查询 \(t_4\) 时,通过二分查找找到 \(t_4\) 对应的邻居表 \(\{u_1,u_2\}\)。

问题在于:相邻版本高度重复。例如 \(u_1\) 连续出现在多个版本中,如果每个版本都保存完整邻居列表,仍会消耗较多空间。


6. 用历史邻居树 HNT 压缩版本

论文进一步为每个节点维护一棵历史邻居树(Historic Neighbor Tree)。

它由两部分组成:

复制代码
当前邻居列表 Lnow
+
历史邻居树 HNT
  • \(L_{now}\):保存当前邻居;
  • HNT:保存已经离开当前邻居表的历史邻居及其有效区间。
6.1 邻居发生变化时

系统收到节点 o 在时间 t 的最新邻居表 \(L_t\),比较:

\ L_{now}\\setminus L_t \\

这些点刚刚从当前邻居表中被移除,需要进入 HNT。

6.2 新历史邻居如何进入 HNT

对于第一次进入历史树的点 p:

  1. 找到从根节点到最新叶节点的 active path;
  2. 沿路径寻找 p 能够覆盖的最高节点;
  3. 将 p 存入该节点;
  4. 如果最新叶节点中的点超过阈值 \(\mu\),就进行节点分裂;
  5. 论文默认 \(\mu=8\)。

HNT 采用自底向上的扩展方式,以保证树高最多为:

\ O(\\log n) \\

6.3 邻居退出后又重新出现

如果 p 以前已经进入 HNT,后来又重新成为当前邻居,再次退出时,不重复保存新副本,而是将原记录沿祖先路径向上移动到能够覆盖更多时间范围的位置。

论文证明,移动后的节点一定是原节点的祖先。

6.4 一个点只保存一次

HNT 最关键的不变式是:

a point valid in multiple nodes is stored only in the highest node where it is valid.

也就是说,即使某个邻居出现在十几个历史版本里,它也尽量只保存一份,通过有效时间区间判断它属于哪些版本。

因此,空间从时间戳图的:

\ O(M\^2N) \\

进一步压缩为:

\ O(MN) \\

这与只保存单个时间快照的 HNSW 空间复杂度相同。


7. 收到 TANNS 查询

现在系统收到查询:

\ TANNS(D,t_s,q,k) \\

例如:

复制代码
查询向量:q
目标时间:t5
返回数量:k=10

系统不需要生成完整的 \(t_5\) 数据快照,而是在图搜索过程中按需还原局部邻居。


8. 从入口点开始执行 HNSW 式搜索

搜索维护两个集合:

  • pool:下一步可能访问的候选节点;
  • ann:当前找到的较优近邻集合。

搜索广度为 \(k'\),通常 \(k'>k\)。论文实验通过改变 \(k'\) 调节召回率和 QPS。

基本循环是:

  1. 从 pool 中取出与 q 最近的节点 u;
  2. 获取 u 在查询时间 \(t_s\) 的邻居;
  3. 把尚未访问且在 \(t_s\) 有效的邻居加入候选;
  4. 维护最多 \(k'\) 个当前最优候选;
  5. 当剩余候选不可能优于现有结果时停止;
  6. 返回距离最近的 k 个结果。

与普通 HNSW 唯一但关键的区别是:

复制代码
普通 HNSW:
    直接读取当前邻居 G[u]

本文方法:
    根据查询时间 ts 动态重建 TG[u, ts]

9. 如何重建某节点在 ts 时刻的邻居

对于访问到的节点 u,系统执行 Algorithm 5。

9.1 扫描当前邻居列表

\(L_{now}\) 中的点按开始时间排序。

系统从前向后扫描:

  • 如果点在 \(t_s\) 时有效,加入邻居集合;
  • 当遇到开始时间晚于 \(t_s\) 的点时停止。
9.2 遍历 HNT

从 HNT 根节点开始:

  • 在当前树节点中加入 \(t_s\) 时有效的点;
  • 若 \(t_s=n.t\),直接结束;
  • 若 \(t_s<n.t\),走左子树;
  • 若 \(t_s>n.t\),走右子树;
  • 到达叶节点后完成重建。

每个 HNT 节点中的点同时按开始时间和结束时间排序:

  • 查询时间早于节点时间时,按开始时间扫描;
  • 查询时间晚于节点时间时,按结束时间扫描。

这样,每层最多多访问一个无效点。

重建复杂度为:

\ O(\\log n+M_r) \\

其中 \(M_r\) 是重建出的邻居数量。论文实验中最大 \(M_r\) 约为 \(1.31M\),即 M=16 时通常不超过约 21 个邻居。


10. 返回最终结果

搜索结束后,系统从 ann 中选出距离 q 最近的 k 个向量,并保证这些向量在查询时间 \(t_s\) 有效。

准确率使用 Recall@k:

\ Recall=\\frac{\|r\\cap r\^\*\|}{k} \\

其中:

  • r:TANNS 返回结果;
  • \(r^*\):在 \(t_s\) 时刻所有有效向量上执行精确 kNN 得到的真值。

论文实验使用 \(k=10\)。


11. 完整生命周期示例

假设节点 o 最初有:

复制代码
主邻居:u3、u4
备份邻居:u1、u2

完整演化过程如下:

  1. \(t_1\):o 建立主邻居 {u3,u4},备份 {u1,u2}。
  2. \(t_2\):u4 失效,从备份中选择 u1 补位,主邻居变成 {u1,u3}。
  3. \(t_3\):u5 插入,但不够好,进入 o 的备份列表。
  4. \(t_4\):u3 失效,从备份中选择新邻居,主邻居变为 {u1,u2}。
  5. \(t_5\):u6 插入并取代 u1,u1 被移入备份列表。
  6. \(t_6\):u2 失效,u1 再次从备份中恢复,主邻居变成 {u1,u6}。
  7. \(t_7\):o 失效,清空当前备份,但保留 o 的全部历史关系。
  8. 查询 \(t_4\):系统通过 HNT 恢复 o 当时的邻居 {u1,u2}。
  9. 查询 \(t_8\):o 已失效,因此不会进入有效搜索结果。

12. 最终复杂度与效果

阶段 压缩时间戳图
向量插入 \(O(\log^2N)\)
向量失效 摊销 \(O(\log^2N)\)
TANNS 查询 \(O(\log^2N)\)
历史邻居重建 \(O(\log n+M_r)\)
总索引空间 \(O(MN)\)

最终实验结果:

  • 四个数据集召回率均可超过 99%;
  • 95% 召回率下,相比现有方案加速 4.4×--138.1×;
  • M=16 时支持超过 1000 次更新/秒;
  • 1000 万 DEEP 向量上达到 857 QPS、99% 召回率;
  • 压缩结构占用 46.7GB ,内存最多降低 66.8%。

最核心的理解是:它并没有为历史时间生成完整图快照,而是把图的变化记录下来;查询某一历史时刻时,只对搜索途中访问到的节点按需恢复当时的邻居关系。这样同时避开了"为每个时间点复制整张图"和"先搜索大量无效向量再过滤"这两类开销。

doubao版

整个流程围绕时间戳图(Timestamp Graph, TG) 和历史邻居树(Historic Neighbor Tree, HNT) 两大核心结构展开,覆盖索引构建与压缩 → 查询执行 → 动态更新三大阶段,全程按时间维度增量推进,无需全量重建索引。


一、索引构建与压缩全流程

该阶段的目标是:把带有效期的高维向量,构建成一张支持所有历史时间戳检索、且经过极致压缩的统一图索引。 论文采用离散时间戳假设:每个时间戳恰好有 1 个向量生效或过期,索引按时间顺序增量构建,而非批量生成。

1. 初始状态与数据准备

  • 输入:N 个高维向量,每个向量附带生效时间 s 和过期时间 e(写入时 e 未知,后续动态触发)。
  • 初始化:
    • 空的多层时间戳图 TG(继承 HNSW 的多层小世界图结构,每层都是一个子图)。
    • 每个向量节点预留两个核心存储:
      • L_now:当前最新的主邻居列表(M 个)+ 备份邻居列表(M 个)。
      • 历史邻居树 HNT:用于归档所有曾经出现过、但已不在当前列表中的邻居,按有效时间段压缩存储。

2. 增量构建时间戳图(按时间顺序插入向量)

每到一个新时间戳,若事件是「新向量生效」,则执行点插入算法(论文 Algorithm 3),步骤如下:

步骤 1:搜索候选邻居

在当前已有的时间戳图中,对新向量o执行一次标准 HNSW 搜索,检索出M'个距离最近的候选邻居(M'远大于 M,默认 200),保证邻居质量。

步骤 2:分配主备双邻居

从M'个候选中,选出距离最近的2M个点:

  • 前 M 个作为主邻居 ,存入TG[o](对应图中的实线连接);
  • 后 M 个作为备份邻居 ,存入B[o](对应图中的虚线连接)。

设计目的:后续主邻居过期时,可直接从备份里补充,避免图连通性下降。

步骤 3:反向更新邻居节点

新向量o需要被加入到它所有邻居的对应列表中,保证图的双向性:

  • 对每个主邻居u:如果o比u当前最远的主邻居更近,就把u最远的主邻居挤到备份列表,把o加入u的主邻居;否则直接加入u的备份列表。
  • 对备份列表超过 M 个的节点,删掉最远的备份点,维持长度固定。
步骤 4:记录历史邻居版本

所有被修改了主邻居列表的节点,都在自己的历史邻居列表末尾,追加一条带当前时间戳的新版本记录。

例:节点 u 在 t1 时刻邻居是 {A,B},t2 时刻新增 C 变成 {A,B,C},则 u 的历史列表会记录「t1: {A,B}」「t2: {A,B,C}」两个版本。

3. 历史邻居树(HNT)增量压缩

原始时间戳图的历史列表存在大量重复(同一个邻居可能在多个版本中反复出现),因此每次邻居列表变动时,都会同步通过追加邻居列表算法(论文 Algorithm 6) 压缩到 HNT 中。

核心逻辑

每个节点的邻居分为两部分:

  • L_now:存放当前最新的有效邻居,按生效时间排序。
  • HNT:存放所有已从 L_now 中移除的历史邻居,按有效时间段树状归档,同一个邻居只存一次。
压缩执行步骤

当节点的L_now发生变化、有邻居p被移除时:

  1. 新邻居归档 :如果p从未在 HNT 中出现过,从 HNT 的最新叶子节点向上遍历,找到能容纳p的最高层节点(即p的有效期覆盖该节点的时间戳),将p存入该节点。
  2. 节点分裂平衡:如果叶子节点的点数超过阈值 μ(默认 μ=8),则该叶子节点标记为 "已满",新建一个内部节点作为它的父节点,同时新建一个空的叶子节点作为兄弟节点,承接后续新归档的邻居。
  3. 已有邻居调整 :如果p之前已经在 HNT 中(即之前被移除过、后来又加回来了),则找到它所在的节点,尝试向上移动到更高层的祖先节点,保证存储位置最高、冗余最少。

通俗理解:HNT 就像给每个节点的 "通讯录历史" 做了压缩归档 ------ 重复出现的联系人只存一次,标注好它的有效时间段;需要查某个时间点的通讯录时,再临时拼出来。


二、TANNS 查询执行全流程

当收到查询请求(查询向量q、查询时间戳ts、返回数量k)时,执行完整的时态近邻搜索,核心是先重建对应时间的邻居,再执行贪心搜索。

1. 邻居列表重建(压缩版特有步骤)

对搜索过程中访问到的每个节点u,都需要先通过邻居列表重建算法(论文 Algorithm 5) ,还原出ts时刻的完整有效邻居列表L_t。

重建步骤
  1. 扫描当前列表 :遍历u的L_now,把所有在ts时刻有效的邻居加入结果集,直到遇到第一个生效时间晚于ts的点,停止扫描。
  2. 遍历历史邻居树 :从 HNT 根节点开始向下遍历:
    • 若当前节点的时间戳等于ts,直接加入该节点所有有效点,结束遍历;
    • 若ts < 当前节点时间戳,向左子树走,同时按生效时间顺序扫描节点内的点,加入有效点;
    • 若ts > 当前节点时间戳,向右子树走,同时按过期时间顺序扫描节点内的点,加入有效点;
  3. 到达叶子节点后,加入叶子里所有ts时刻有效的点,最终得到完整的L_t。

论文验证:重建出的邻居列表大小平均仅为 1.31M,和原始 M 非常接近,不会带来显著的计算开销。

2. 多层贪心搜索(和 HNSW 逻辑兼容)

邻居重建完成后,搜索逻辑和标准 HNSW 基本一致,全程在ts对应的有效子图中导航:

  1. 初始化 :从图的最顶层入口点开始,维护两个集合:
    • pool:候选导航点,用于继续向更近的方向探索;
    • ann:潜在最近邻,用于保存当前找到的最近点。
  2. 逐层导航:从最顶层开始,每层都执行贪心搜索,找到该层离 q 最近的点,作为下一层的入口点。
  3. 底层精细搜索 :在最底层(数据层)持续迭代:
    • 从 pool 中取出离 q 最近的点 u;
    • 重建 u 在 ts 时刻的邻居列表L_t,把未访问过的邻居加入 pool 和 ann;
    • 当 ann 的大小达到k'(搜索范围参数,k'>k)、且 pool 中所有点都比 ann 中最远的点远时,终止搜索。
  4. 返回结果:从 ann 中选出距离最近的 k 个点,作为 TANNS 查询结果。

三、动态增量更新全流程

向量数据库是持续写入的,新向量不断生效、旧向量不断过期,时间戳图支持增量更新,无需重建全图。

1. 新向量插入流程

和索引构建阶段的插入逻辑完全一致(对应 Algorithm 3):

  • 搜索候选→分配主备邻居→反向更新邻居→追加历史版本→同步归档 HNT。
  • 每次插入仅影响 O (M) 个节点的邻居列表,时间复杂度 O (log²N)。

2. 旧向量过期流程

当向量o到达过期时间,执行点过期算法(论文 Algorithm 4) ,核心是只修改受影响的邻居,保留历史数据。

过期步骤
  1. 遍历o的所有主邻居:
    • 如果o在对方的备份邻居列表里:直接删除即可,不影响主连通性。
    • 如果o在对方的主邻居列表 里:
      • 若对方备份列表非空,就从备份里选一个最近的点,补充到主邻居里;
      • 若备份列表为空,就重新执行一次搜索,为该节点选出新的 2M 个主备邻居。
  2. 所有被修改了主邻居的节点,都追加一条带当前时间戳的历史邻居版本,并同步更新 HNT。
  3. 对过期向量o本身:清空它的备份列表,但保留所有历史邻居记录,保证历史时间戳的查询仍然可以正常使用。

关键优势:每次过期仅影响 O (M) 个节点,绝大多数情况只需备份补位,无需重新搜索,更新复杂度 O (log²N),远优于朴素方案的全量重建。


完整流程闭环总结

复制代码
数据写入 → 按时间增量插入 → 构建时间戳图TG → 增量压缩进历史邻居树HNT
         ↑                                                  ↓
         │ 持续动态更新                                查询请求到来
         │                                                  ↓
         └──── 向量过期/新增 ←──── 重建ts时刻邻居列表 ←── 解析查询时间戳ts
                                                          ↓
                                                    多层贪心导航搜索
                                                          ↓
                                                    返回top-k结果

整个流程的核心设计思想是:利用时间局部性复用索引结构,通过主备机制保障动态稳定性,通过树状归档压缩历史冗余,最终在单索引的存储成本下,实现了全历史时间戳的高效近似近邻搜索。

相关推荐
海宇大数据1 小时前
零信任架构实战:基于海宇公安二要素认证即时版构建自动化证券账户实名鉴权网关
人工智能·架构·自动化·区块链
碳基硅巫师_丁宏宽1 小时前
警惕AI定制化茧房:当AI足够懂你,你可能被锁在它编织的虚幻世界
人工智能
for_ever_love__1 小时前
RAGAS 实战:一套能跑起来的 RAG 自动评测框架
人工智能·框架·工具·自动·rag·评估·评测
俊哥AI全栈工程师1 小时前
用 AI 重构老项目,我踩过的 5 个坑(真实项目复盘)
人工智能
七七安1 小时前
部分卷积(Partial Convolution)
人工智能
Xuantong_901 小时前
在AI时代,重新寻找「师傅」
人工智能
长弓三石1 小时前
用 JEV 决策模型接管 Agent 路由判断(一):为什么把「判断」从推理里拿出来
人工智能
晨间晓1 小时前
图片对比度够不够?用华为云码道开发 OpenCV 对比度分析工具
人工智能·opencv·华为云
ADAI_Bowen1 小时前
当AI读懂图纸:ADAI,用垂直大模型重构建筑方案的迭代逻辑
人工智能·重构