想象一座不断生长的城市。每天都有新街区交付使用,也有旧街区被废弃。如果城市只有一本每年重印一次的纸质地图,新住户就永远无法在同一天被访客找到。现实中的搜索引擎也面临一模一样的困境:商品、新闻、用户行为每分每秒都在变化,如果每次数据更新都要把整个索引离线重建一遍再发布,线上服务就会反复停摆。 幸运的是,我们有一种“会自己改路网”的索引——HNSW。它把每个向量想象成城市中的一个街区,把向量之间的相似关系想象成街区之间的道路。新街区落成时,施工队现场问路、现场接线,让访客当天就能抵达;旧街区需要拆除时,先贴一张“暂停接待”的封条,绕开即可,等夜深人静再集中清理。这整套能力统称为“索引的构建与增量更新”,还包含分区管理、异步合并、定期路网体检等配套手艺。这篇文章用“一座不断生长的城市”作主线,带你搞清楚生产环境中向量索引的动态维护到底在做什么、会踩哪些坑。
对 AI 从业者的职业价值:索引增量更新是向量数据库与搜索系统走向生产环境的必修课,掌握它,你才能让检索系统在数据流动中持续提供准、快、稳的服务。
可视化演示
展示HNSW层次导航小世界图的搜索过程,从顶层粗粒度入口逐层下降到底层精细搜索,以及增量插入时新节点连接构建和软删除标记机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
HNSW层次导航搜索 顶层稀疏图快速定位近似区域 逐层下降精细化搜索邻域 O(log N) 搜索复杂度 贪心搜索每层选最近邻
在线插入机制
想象一座不断扩张的城市。每天都有新街区投入使用,但它们不是孤立孤岛:施工队会把每一个新街区接入现有的道路网络,让它从一开始就被人们找到。老城区有一本厚厚的《城市路网图册》,每年才重新出版一次。如果你想在新街区公布当天就被人按图索骥找到,就不能等再版,而要在它落成的瞬间修好连接道路。这就是在线插入机制:不需要重印整本地图,只需要在旧路网中为新人开辟几条小路。
在 HNSW 这类基于图检索的向量索引中,“街区”就是向量,“道路”就是节点之间的边。静态索引像装订成册的地图,只能在离线阶段整体构建;但现实中的商品、新闻、用户画像随时在变,我们必须在索引运行的同时加入新向量。HNSW 的在线插入完全不需要重建图:它从图的入口节点出发,沿着已有的边做贪心搜索,像司机沿路标一路问路,找到距离新向量最近的几个现有节点,再把新节点与这些邻居连上双向边。
贪心搜索的每一步,都会评估当前节点所有邻居与新向量的距离,选择“当前最优方向”前进;到达底层后,从沿途经过的候选节点中挑出最近的 m 个邻居建立连接。由于 HNSW 是多层结构,搜索从高层低精度的入口开始,逐层下降到底层,最后在底层精细连接。整个过程只影响局部的少量节点,不需要全局重排,因此可以在服务不中断的情况下持续接收向量更新。
在线插入让索引维护不再是“重新印刷整本地图”,而是每个新街区落成时,修路队只需把几条关键路口连上。但代价是:每一条路都是“当时当地”的最佳选择,而不是全局规划的结果——这为后续“索引质量会慢慢退化”埋下了伏笔。
- 向量差计算两个向量在每一维上的差值,相当于测量两个街区在东西、南北方向上的距离。
- 平方累加把所有维度上的差平方后加在一起,避免正负方向互相抵消,这一步相当于把总路程的各段分量加起来。
- 开根号对总和开平方,得到真实的欧几里得距离,也就是新街区到已建成街区之间的最短直线距离。
软删除标记
城市的拆除工程往往比建设工程更麻烦。白天若全封闭某条道路,车辆就得绕大圈;直接拆掉整栋旧楼,还可能伤及相邻建筑。聪明的城市管理者会给危房贴上封条:“暂停接待”。地图上仍然画着这个地点,但巡警会提醒访客绕开它。
向量索引中的删除同样采用“贴封条”的思路,即软删除。当某个向量不再需要时,不会立刻把它从图结构中物理移除——如果直接拆除节点,与它相连的所有边都会断裂,周边区域会变成孤岛,后续查询无法继续。最简单的做法是给该节点加一个 is_deleted 标记,搜索时跳过标记过的节点。这样删除操作的成本变成 O(1),并且不会破坏图的连通性。
但“贴封条”久了,城市里到处都是死胡同:搜索每次都会路过被标记的节点,增加无效遍历;更严重的是,这些节点仍占用内存,新节点在构图时还可能把它们选为邻居。因此,系统需要监控删除占比,当比例超过阈值时触发异步清理或重建,把标记节点真正从邻接表中移除。
软删除就是把“拆除”推迟到夜间集中施工。日常查询宁绕勿堵;到了触发阈值,再派一支夜间施工队把整片危房清理干净。这就是生产索引“日常不中断、定期大扫除”的节奏。
- 删除占比分子是已打删除标记的节点数,分母是总节点数,比值反映索引被“封条”污染的程度。
- 阈值触发当删除占比大于预设阈值 θ(比如 0.2),系统在后台触发重建,把被标记节点真正清理掉。
分片索引架构
城市大到连片区主任都记不清全城路网时,管理者自然会把城市分成几个行政区,每个区独立维护自己的路网地图。游客先确认片区,再在片区内找路。对向量索引来说,这就是分片(Shard)。
分片是将一个大索引水平切分成多个独立的子索引。每个分片可以是一张相对较小的 HNSW 图,独立构建、独立更新,通过哈希路由将向量均匀分配到固定分片。比如对向量做哈希取模,同一个向量永远落进同一个分片,后续插入、删除、查询都只与这个分片打交道。
分片的核心收益有三点:第一,水平扩容——数据量翻倍时,增加分片即可,不需要重写整个系统;第二,更新隔离——某个分片在做合并或重建时,其他分片照常服务;第三,失败影响面受限——单个分片故障不会拖垮全局。代价是检索时路由层需要把查询分发到所有相关分片,再把各分片返回的 top-k 结果合并、重排序后返回给用户。
分片让城市变成“分区自治”:各片地图可以单独加印、单独修订,一个片区夜间施工不会让整座城市瘫痪。检索时管理员只要汇总各片区的结果再排序,市民就能在整座城市范围内找路。
- 哈希映射把向量通过哈希函数映射为一个整数,目的是让分布足够均匀,避免数据倾斜到少数分片。
- 取模路由用哈希值对分片总数 S 取模,得到 0 到 S-1 的分片编号,决定向量归属。
异步合并机制
白天车水马龙,施工队不会白日封路。所有大工程都安排在深夜进行,市民一觉醒来发现路网已经更新完毕——没有人等待施工队干完活才能出门。这就是异步合并的核心思想:把重建动作从查询的关键路径上挪走。
当新数据源源不断写入时,系统不直接修改主索引,而是先把数据写进“新片段”(独立的小分片)。当新片段积累到一定规模,后台任务在查询低峰期启动合并,把小片段并入主索引。合并完成后,通过一次原子切换让新索引统一生效,查询请求全程无需等待索引重建。
合并期间查询并不中断,而是同时访问旧片段和新片段,这保证了查询延迟不会出现尖锐毛刺。但异步并不是免费的:合并需要额外的 CPU 和 IO 开销,太频繁会浪费资源,太久不合并则会导致片段数量膨胀,查询时需要合并更多临时的结果集。因此,合并阈值的设置需要结合写入速率、查询延迟容忍度和存储成本综合权衡。
修路队把工程藏进深夜里,代价是市政要多付资源和加班费,但市民早晨出门依旧畅通。异步合并的本质就是用后台成本换取线上查询的稳定无感。
- 路由时间查询从入口分发到各分片所消耗的时间,通常是毫秒级常量。
- 分片查询累加对每个分片执行检索的时间求和,分片越多,这一项越大。
- 合并开销如果查询恰好发生在合并阶段,需要额外等待或执行双路读取,这一项表示合并附加影响。
索引质量影响
增量接入的路都是“当年当季问路问出来的”,而老城区主干道是全局规划严格设计的。日积月累,新城区绕路越来越多,导航时间明显变长。向量索引也有同样的尴尬。
HNSW 的在线插入始终以局部视图回答“该连谁”——新节点只连接搜索路径上遇到的最近邻居,并不保证图整体保持最优结构。这种质量损失不会在第一次插入时显现,而是随着插入、删除的持续累积逐步扩散:贪心搜索更容易绕进局部死角,真实邻居不再被访问,检索结果开始“偏离正确方向”。
生产环境里,索引质量最终反映为两个可观测指标:检索召回率(Recall@k)和查询延迟。召回率衡量“真实最相关的 k 个向量有多少被系统返回”,当图质量下降时,搜索容易停留在局部区域,召回随之减少;同样的查询要遍历更多节点和边,延迟也会上升。长期运行的增量系统如果不监控这两个指标,会在某次大批量更新后突然发现推荐效果变差——所以“能更新”不等于“更新后依然好用”。
新路越修越多,若没有全城体检,城市会逐渐变得拥堵。召回率下降和延迟上升就是城市“早晚高峰变长”的信号,及时预警才能避免检索系统在不知不觉中劣化。
- 命中交集分子是检索系统返回的候选集与真实最近邻集合的交集大小,表示“找对了几个”。
- 归一化除以 k,将命中数量转化为 0 到 1 之间的比例,便于跨系统对比。
压缩维护策略
城市每年都会做一次路网体检:把频繁拥堵的路升级为快速路,把常年无人使用的小巷封掉。虽然个别地段的捷径变少了,但整体通行效率反而更高。压缩维护之于向量索引,就是这样一个“定期体检 + 整形”的过程。
压缩维护的触发时机通常来自三个信号:删除标记占比过高、查询平均延迟持续上涨、累积新增写入向量达到预设阈值。当系统判定触发时,会启动一次重建或局部修复,把打标记的节点真正移除,把绕路的局部连接重新规划,让图结构恢复接近全量重建的质量。
重建是重活,所以生产系统常用量化压缩先行降低成本。乘积量化(PQ)把向量切成多个子空间,每个子空间用码本索引代替浮点坐标,高维向量从几十个浮点数变成几个整数;标量量化则把每个维度缩放到更低比特位宽。压缩后内存占用大幅下降,距离计算更快,重建和合并所需 IO 也随之减小。代价是精度损失:压缩越狠,距离近似误差越大,召回率可能轻微下降,因此压缩率与精度之间需要小心权衡。
路网体检不可能让每一条小巷都完美,但它让整座城市在数据不断流动的时代始终保持“搜得快、找得准”的大局。压缩维护就是这座城市永葆青春的秘诀。
- 子空间索引每个向量被切成 m 段,每段在一个聚类码本中找到最接近的码字 ID,只需存 \log_2 K 个比特。
- 总存储成本乘以向量总数 n,就是压缩后整个索引的存储体积,远小于原始浮点矩阵。
AI 应用场景
电商商品池的实时上架与下架
阶段:增量索引构建与在线更新
解决的问题:电商平台每天有数十万商品上新、下架、改价。如果采用全量重建索引,一次构建需要数小时,且更新期间线上推荐只能使用过期数据。在线插入 + 软删除让商品向量在分钟级内反映到搜索结果中。
具体案例:某电商的“相似商品推荐”服务将商品标题、图片、属性的 embedding 存入基于 HNSW 的向量库。高峰期每秒数百次写入:新商品写入内存缓冲区,积累到阈值后异步合并进线上分片;下架商品只打软删除标记,查询时过滤;夜间低峰期触发压缩,清理已删除节点、重建劣化分片。
RAG 知识库的高频增删改
阶段:向量知识库版本发布与维护
解决的问题:RAG 应用中知识库经常增删改(内部 Wiki、售后文档、工单记录),同时问答服务不能中断。分片 + 异步合并让知识库做到“文档改了马上能搜”。
具体案例:企业内部知识库问答 Bot 将文档分段向量化,文档变更时只重算变更段落,写入独立新分片;原大分片保持不变。查询时同时读取新旧分片并合并结果,后台任务把新分片并入主索引。当删除占比超过 15% 时,夜间自动重建对应分片,保持召回率稳定。
Python 演示
"""HNSW 简化版:演示在线插入与软删除的核心思想"""import heapqimport numpy as np class SimpleHNSW: def __init__(self, m=2): self.m = m # 每个节点最大邻居数 self.nodes = [] # 存储向量 self.neighbors = [] # 邻接表 self.deleted = [] # 软删除标记 def _dist(self, i, q): # 节点 i 到查询向量 q 的欧氏距离 return float(np.linalg.norm(self.nodes[i] - q)) def _search_candidates(self, q, k): # 简化版:对现有节点排序,取前 k 个候选邻居 dists = [(self._dist(i, q), i) for i in range(len(self.nodes))] return heapq.nsmallest(k, dists) def insert(self, vec): # 在线插入:新向量从入口出发搜索邻居,连双向边 q = np.asarray(vec, dtype='float32') if not self.nodes: self.nodes.append(q) self.neighbors.append([]) self.deleted.append(False) return 0 cand = self._search_candidates(q, self.m) new_id = len(self.nodes) self.nodes.append(q) self.neighbors.append([]) self.deleted.append(False) for _, cid in cand: self.neighbors[new_id].append(cid) self.neighbors[cid].append(new_id) return new_id def soft_delete(self, i): # 只打删除标记,不物理移除,避免图结构断裂 self.deleted[i] = True def search(self, q, topk=1): # 查询时跳过已删除节点 q = np.asarray(q, dtype='float32') dists = [] for i in range(len(self.nodes)): if self.deleted[i]: continue dists.append((self._dist(i, q), i)) dists.sort() return [idx for _, idx in dists[:topk]] if __name__ == '__main__': index = SimpleHNSW(m=2) # 初始构建:三个种子向量 for v in ([0., 0.], [1., 0.], [0., 1.]): index.insert(v) print('查询 [0.9, 0.1] ->', index.search([0.9, 0.1])) print('删除节点 1(软删除)') index.soft_delete(1) print('删除后查询 [0.9, 0.1] ->', index.search([0.9, 0.1])) print('删除占比:', sum(index.deleted) / len(index.nodes))
self.deleted = []用列表维护每个节点的软删除标记,删除操作只改标记不改图结构_search_candidates(q, k)简化版的邻居搜索,相当于是贪心搜索的“候选集生成”环节self.neighbors[new_id].append(cid)新节点与候选邻居建立双向边,对应在线插入的“修路”if self.deleted[i]: continue查询时跳过已删除节点,对应贴封条绕行sum(index.deleted) / len(index.nodes)统计删除占比,高到阈值就该触发重建维护
常见误区
误区删除向量时,一定要调整图结构把节点和边都删掉,否则索引一定有冗余,性能会变差。
正解HNSW 生产实现普遍采用软删除:给节点打标记,查询时跳过。立刻物理删除会导致与它相连的边全部断裂,周边节点变成孤岛,后续查询退化为低效的随机遍历。正确做法是把物理清理推迟到删除比例达到阈值后的异步重建。
为何常见:学过链表、数组删除的人习惯性认为“删除必须修改引用”,忽略了图结构中物理删除会引发连锁断裂的高成本。
误区HNSW 支持在线插入,所以增量更新的索引效果应该和定期全量重建完全一样。
正解在线插入做的是“局部最优连接”,新节点只连接到搜索路径上遇到的最近邻居,不是全局最优的图结构。长期增量更新后,图质量会逐渐低于一次全量重建,必须通过 Recall@k 指标监控,并定期压缩维护。
为何常见:人们容易把“可以更新”等同于“更新结果与离线重训一致”,忽略了局部决策与全局优化的本质差异。
误区分片就是按哈希路由到一块数据上,查询时哈希命中哪个分片就只查哪个分片,不需要处理其他分片。
正解ANN 是近似检索,无法预知目标向量的最近邻落在哪个分片,查询必须广播到所有分片(或按业务规则限定部分分片),再把各分片的 top-k 候选合并重排序。只查一个分片会系统性漏掉大量真实最近邻。
为何常见:把向量检索与确定性哈希查找混为一谈,忽略了 ANN 检索“不确定结果落在哪”的本质特性。
误区异步合并是在后台执行,所以线上查询完全无感知,不需要额外读旧数据,也没有任何成本。
正解合并期间查询仍然要同时访问旧片段和新片段(或者读一份“双视图”),直到原子切换完成后才统一走新索引。后台执行只是把重建动作移出请求关键路径,不等于合并过程中不存在双路读取和资源竞争。
为何常见:把“后台任务”想象成魔法,忽略了分布式系统中读写双路和切换时真实存在的成本与延迟小幅波动。
面试题
Q1. 在 HNSW 中插入一个新向量时,第一步应该做什么?
- A.删除一部分旧节点再连新节点
- B.随机挑选若干节点进行连接
- C.从入口点出发贪心搜索近邻← 正确答案
- D.重新构建整个图的边集
答案:C。HNSW 插入新向量的第一步是从图的入口点出发做贪心搜索,沿着边不断逼近新向量的近邻区域,再从候选邻居中选最近的 m 个建立连接。随机连接或删除节点都会破坏搜索路径的有效性,全图重建则失去了在线更新的意义。
Q2. HNSW 采用软删除的主要目的是什么?
- A.降低删除造成的重构与连接中断开销← 正确答案
- B.删除后立刻把节点从邻接表中移除
- C.使已删除向量不再占用任何内存
- D.让查询跳过已删除向量以外的内容
答案:A。软删除的核心价值是避免物理删除引发的边断裂和局部图瘫痪。删除只打标记,查询时跳过,代价低且不破坏连通性。物理删除和内存释放并不是软删除的目的,它们要等到异步清理阶段才完成。
Q3. 当索引中大量节点被软删除但尚未清理时,最可能发生什么?
- A.查询耗时保持初始水平不发生变化
- B.删除节点从索引中自动消失并可忽略
- C.搜索遍历开销上升,召回率也会下降← 正确答案
- D.已删除节点会显著加速后续的搜索
答案:C。被软删除的节点仍然存在于图结构中,搜索时可能被遍历到,增加无效跳转。同时这些节点继续占用内存,还可能被新节点选为邻居,导致索引质量下降、召回率降低。它们不会自动消失,更不可能加速搜索。
Q4. 关于分片索引架构,下列哪些描述是正确的?
- A.将大索引拆成多个独立构建的小索引← 正确答案
- B.支持数据隔离与水平方向扩容← 正确答案
- C.每个查询只需要访问其中一个分片
- D.分片之间可以完全共享一份邻接表
答案:AB。分片把大索引切分为独立的小索引,各自构建、各自更新,天然支持水平扩容和数据隔离。但 ANN 查询必须广播到多个相关分片并合并结果,不能只查一个分片;分片之间是隔离的,共享邻接表会破坏隔离性的意义。
Q5. 异步合并机制的核心做法是什么?
- A.在线查询停止后整体重建整个索引
- B.后台完成新片段合并,查询不断流← 正确答案
- C.新写入向量直接丢弃,等待全量更新
- D.每次请求都重新复制分片索引
答案:B。异步合并在后台把新片段并入主索引,合并期间查询继续访问新旧片段,最后通过原子切换生效。它不停止在线服务,也不丢弃新数据,更不会让每次请求都重复复制索引。
Q6. 下列哪些条件可以触发索引压缩维护?
- A.删除标记的比例超过设定的阈值← 正确答案
- B.平均查询延迟出现持续上涨趋势← 正确答案
- C.累积新增写入向量达到预设数值← 正确答案
- D.查询错误率下降到千分之一以下
答案:ABC。删除比例过高、延迟持续劣化、累积写入量过大都是典型的触发信号,它们反映索引需要清理废节点或修复局部连接。错误率下降说明系统状态健康,不构成维护触发条件。
Q7. “删除标记占比 = 50%” 的含义是什么?
- A.索引中已经有一半向量被物理删除
- B.一半向量带着删除标记但尚未清理← 正确答案
- C.系统拒绝了一半以上的在线查询
- D.动态请求会被路由到总索引的一半
答案:B。删除标记占比指被标记为已删除但还残留在图中的节点比例。这些节点仍占用内存、参与遍历,只是查询时被跳过。它们没有被物理移除,系统也没有拒绝查询或对请求做路由分流。
Q8. 召回率 Recall@k 的公式含义是什么?
- A.返回的k个结果中真实近邻所占的比例← 正确答案
- B.检索结果集的长度与总数据量之比
- C.一次查询返回结果的排序平均质量
- D.系统吞吐量与查询延迟的乘积值
答案:A。Recall@k 是检索系统返回的前 k 个结果中,真实最近邻所占的比例。它衡量系统“找对了多少”,不关心结果长度占比、排序质量或吞吐延迟。召回率下降往往是索引质量劣化的第一信号。
Q9. 下列哪些做法属于压缩维护策略的常规手段?
- A.定期重建劣化严重的局部图连接← 正确答案
- B.用向量量化压缩显著降低内存← 正确答案
- C.删除标记全部不做清理地保留下去
- D.数据量减少时自动回退纯线性扫描
答案:AB。局部重建和量化压缩都是压缩维护的典型手段,前者修复图结构,后者降低内存与重建成本。删除标记不做清理会持续劣化索引;回退线性扫描只适用于极小规模场景,不属于常规维护策略。
Q10. 查询过程中遇到一个被软删除的节点,通常会发生什么?
- A.搜索路径碰到该节点就跳过继续前进← 正确答案
- B.删除掉这个节点关联的所有邻居边
- C.把该节点设成后续搜索的唯一入口
- D.搜索到达该节点后立刻终止并返回
答案:A。软删除节点的查询行为是“跳过但保留边”:遍历到它时忽略它,继续探索它的邻居。物理删除要等异步清理阶段才进行,它不会被当作搜索入口,更不会让搜索提前终止。