十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HNSW算法:高维向量快速检索的核心原理与工程实践

HNSW算法:高维向量快速检索的核心原理与工程实践 1. 从“大海捞针”到“按图索骥”HNSW算法解决了什么在信息检索、推荐系统、图像识别这些领域我们常常会遇到一个看似简单实则棘手的问题给定一个高维空间里的一个点比如一张图片的特征向量、一段文本的嵌入向量如何从海量的数据点比如一个包含数亿张图片的数据库中快速找到与它“最相似”的若干个点这就是所谓的“最近邻搜索”Nearest Neighbor Search, NNS问题。当数据维度很高时比如128维、512维甚至上千维这个问题会变得异常困难我们称之为“维度灾难”Curse of Dimensionality。传统的线性扫描方法把查询点和数据库里每一个点都算一遍距离在数据量巨大时其计算开销是灾难性的完全不具备实用性。于是近似最近邻搜索Approximate Nearest Neighbor, ANN算法应运而生。它们牺牲一点点精度换来搜索速度几个数量级的提升。在众多ANN算法中HNSWHierarchical Navigable Small World分层可导航小世界图近年来脱颖而出成为了工业界和学术界事实上的“宠儿”。它不像一些算法那样需要复杂的参数调优也不像另一些算法那样对数据分布有严苛要求HNSW以其出色的性能、良好的可扩展性和相对简单的实现逻辑成为了许多向量数据库如Milvus, Weaviate, Qdrant和机器学习库如Faiss的核心索引算法。简单来说HNSW构建了一个多层次的图结构。你可以把它想象成一个多层的社交网络最底层第0层包含了所有的数据点它们之间随机地连接了一些“朋友关系”边。往上一层点的数量会减少但每个点依然保留着与少数“精英朋友”的连接。这个“精英朋友”网络更稀疏但连接距离更远。当你进行搜索时查询就像一个新来的人他从最高层人最少、视野最广的圈子开始快速定位到一个大致区域然后逐层向下在越来越密集的“朋友圈”里精确定位最终在底层找到最相似的那些点。这个过程避免了在全网漫无目的地瞎逛效率极高。2. HNSW的核心思想小世界网络与分层导航要理解HNSW我们需要拆解它的两个核心概念“可导航小世界”Navigable Small World, NSW和“分层”Hierarchical。2.1 小世界网络六度分隔理论的启示“小世界”现象在生活中很常见即任何两个陌生人之间平均只需要通过六个中间人就能建立起联系。在数学图论中小世界网络具有两个关键特性较高的聚类系数你的朋友之间很可能也是朋友和较短的平均路径长度任意两人之间的连接步数很少。NSW算法就是受此启发。它构建一个图图中的节点是数据点边代表点之间的“邻近”关系。构建过程很简单按顺序插入节点每个新节点随机连接到已存在于图中、且离它最近的若干个节点比如M个。这样形成的图天然具有小世界特性局部区域高度连接聚类同时存在一些“长连接”可以快速跳转到远处区域短路径。搜索时从一个随机入口点开始采用“贪婪搜索”策略不断移动到当前点的邻居中离查询点最近的那个点直到无法找到更近的点为止这个点就被认为是近似最近邻。但NSW有个问题它的搜索路径可能很长尤其是在数据量极大时需要“跳”很多步。这就引出了分层的思想。2.2 分层结构从宏观到微观的快速定位HNSW在NSW的基础上引入了分层。它构建的不是一个图而是一个图的多层集合Layer 0, Layer 1, ..., Layer L。构建规则如下层数分配每个节点都会被分配一个最大层数l这个l由一个衰减概率函数决定通常是floor(-ln(uniform(0,1)) * mL)其中mL是一个参数。这意味着大多数节点都在底层Layer 0越往高层节点越稀少。这模仿了跳表Skip List的结构。层内连接对于节点在第l层及以下的所有层中它都会像NSW一样连接到该层中离它最近的若干个节点最多M个。但连接策略更聪明它采用启发式方法在候选邻居中优先选择那些能最大程度“覆盖”不同方向的邻居避免连接一堆彼此已经很近的点从而保证图的导航性。这种分层结构带来了巨大的搜索优势。搜索时算法从最高层节点最少的一层开始。因为这一层图非常稀疏只需几步就能跨越很大的距离快速定位到查询点所在的大致区域。然后算法以下一层中从上一步找到的最近点作为入口点在更密集一层的图中继续搜索。这个过程逐层重复直到最底层Layer 0。在底层算法在目标区域附近进行精细搜索找到最终的最近邻。这个过程就像使用地图册先看世界地图高层确定国家再看国家地图中层确定省份最后看城市街道图底层找到具体地址。分层搜索将时间复杂度从接近 O(N) 降到了 O(log N)。3. HNSW的构建过程一步步画出导航图理解了思想我们来看HNSW索引的具体构建步骤。假设我们有一个包含N个d维向量的数据集。我们需要预先设定几个关键参数M每个节点在构建时试图连接的邻居最大数量影响图的连通性和内存占用。efConstruction动态候选列表大小用于控制插入时邻居选择的精度。mL控制节点层数分布的参数通常选择1/ln(M)效果较好。构建算法是逐个插入向量的过程确定新节点层数对于新插入的向量q根据概率分布函数随机生成其最大层数l。自上而下搜索入口点从最高层L当前已构建的最高层开始直到层l1。在每一层执行一次贪婪搜索找到该层中离q最近的一个节点作为进入下一层的入口点。这个搜索的ef参数通常设为1只找最近的一个。自下而上插入并连接从min(l, 当前最高层)层开始向下直到第0层在每一层执行 a.搜索当前层的最近邻以从上一步获得的入口点或上一层找到的最近点开始在当前层执行搜索寻找离q最近的efConstruction个节点放入候选列表W。 b.邻居选择从W中为q选择最多M个邻居。HNSW使用一种启发式选择算法通常称为“简单”或“启发式”连接。它按距离q从近到远遍历W中的节点c只有当c距离q比q已选邻居中任意一个距离c更近时才将c加入q的邻居列表。这保证了邻居的多样性防止所有邻居都挤在一个方向。 c.反向连接将q添加到其新邻居的邻居列表中。同时需要检查这些邻居的邻居数是否超过了M。如果超过需要对这些邻居执行同样的启发式选择算法修剪其邻居列表保持最多M个连接。这一步至关重要它保证了图的稀疏性和导航性是HNSW性能稳定的关键。完成插入在所有层完成插入和连接后节点q就成为索引的一部分。注意构建过程中的“邻居选择”和“反向连接修剪”是HNSW区别于简单NSW的核心。它主动维护了图的“可导航”属性而不是仅仅随机连接最近邻。这需要额外的计算但换来了搜索时更稳定的高性能。4. HNSW的搜索过程高效的逐层逼近索引建好后搜索查询过程相对直观它完美利用了分层结构的优势。给定一个查询向量q和参数efSearch搜索时动态候选列表大小控制召回率与速度的权衡搜索步骤如下初始化设置入口点ep为最高层的一个已知节点通常是插入的第一个节点或一个固定节点实际实现会维护一个顶层入口点列表。顶层粗略定位在最高层以ep为起点执行贪婪搜索但使用efSearch参数找到该层离q最近的一个节点将其作为下一层的入口点。因为顶层节点极少这一步非常快。逐层细化从次高层开始直到第0层对于每一层 a. 以上一层找到的最近节点作为本层的入口点。 b. 在本层执行贪婪搜索。这个搜索不是简单的“走到最近邻居就停”而是维护一个动态候选列表C大小为efSearch里面保存着当前发现的离q最近的efSearch个节点。搜索过程会不断从C中取出离q最近的未访问节点探索其邻居更新C直到C中所有节点都被探索完毕。此时C中距离q最近的节点就是本层搜索的结果并作为下一层的入口点。底层精确检索在第0层执行完同样的搜索后动态候选列表C中就包含了整个搜索过程中发现的、离q最近的efSearch个节点。最后从这个列表C中返回距离最近的k个用户所需的数量节点作为近似最近邻结果。为什么这个过程高效减少搜索范围高层快速跳过无关区域将搜索快速引导到目标区域。贪婪搜索动态列表底层的搜索虽然范围集中但通过维护一个较大的候选列表CefSearch避免了陷入局部最优。它本质上是在目标区域的一个小范围内做了一次小型但精确的扫描。参数控制efSearch是搜索精度和速度的“旋钮”。efSearch越大候选列表越丰富召回率越高但搜索越慢反之则越快但可能错过真正的最远邻。5. 关键参数深度解析与调优实践HNSW的性能和效果很大程度上依赖于几个核心参数的设置。盲目使用默认值可能无法发挥其最佳效能。下面我们来深入剖析5.1 构建参数为索引打下好基础M最大出度这是最重要的参数之一。它决定了图中每个节点有多少个“朋友”。影响M越大图越稠密搜索路径越短可能更快但构建时间越长内存占用越高每个节点需要存储M个邻居ID并且邻居选择的计算量也越大。M太小图太稀疏可能导致搜索路径变长甚至图变得不连通影响召回率。调优建议通常在8到48之间选择。对于维度较低100或数据分布均匀的数据集可以尝试较小的M如12-16。对于高维500或聚类明显的数据需要更大的M如24-48来保证连通性。这是一个需要权衡的参-数。一个实用的方法是在保证召回率的前提下选择能使搜索速度最快的那个M。efConstruction构建时候选集大小在插入节点选择邻居时会先搜索出efConstruction个最近候选再从中精选出M个。影响efConstruction越大构建时邻居选择的质量越高最终构建的图质量越好搜索性能越佳但构建时间会线性增加。调优建议通常设置为efSearch目标值的2到10倍或者直接设为一个较大的固定值如200-500。对于追求极致搜索性能的应用可以适当调高它例如800-1000这属于“用构建时间换搜索时间”。如果构建时间敏感可以降低到100-200但可能会轻微影响搜索效率。mL层数分布参数控制节点出现在高层的概率。公式level floor(-ln(uniform(0,1)) * mL)中mL越小节点出现在高层的概率越高。影响高层节点是“导航枢纽”。高层节点过多则高层图不够稀疏快速定位的优势减弱高层节点过少则可能顶层入口点离查询点太远导致初始定位不准。调优建议原作者论文建议设置为1 / ln(M)。这是一个经过理论推导和经验验证的合理值在绝大多数情况下无需调整。例如当 M16 时mL ≈ 0.36。5.2 搜索参数平衡速度与精度efSearch搜索时候选集大小这是搜索时最主要的“旋钮”。影响直接控制召回率Recall和搜索延迟Latency的权衡。efSearch值越大搜索过程中考察的候选点越多找到真实最近邻的概率越高召回率越高但搜索耗时也越长。调优实践这是线上服务需要动态调整的参数。通常做法是在离线测试集上绘制efSearch与召回率、搜索耗时的关系曲线。根据业务对延迟和精度的要求确定一个满意的efSearch值。例如要求召回率 95%延迟 10ms对应的efSearch可能是200。在线上可以根据服务负载动态微调efSearch。负载高时略微调低以保障延迟负载低时可以调高以提供更精确的结果。个人经验参数调优没有银弹。最有效的方法是在真实数据集的一个有代表性的子集上进行网格搜索Grid Search。固定其他参数遍历不同的M如12, 16, 24, 32, 48和efConstruction如100, 200, 400, 800为每一组参数构建索引然后用一个固定的efSearch比如200在测试查询集上评估召回率和搜索速度。选择在目标召回率下搜索速度最快的那组(M, efConstruction)作为生产参数。这个过程虽然耗时但一劳永逸。6. HNSW的优缺点与典型应用场景没有完美的算法只有适合场景的算法。HNSW的强大之处在于它在精度、速度和易用性之间取得了极佳的平衡。6.1 优势为何HNSW能脱颖而出高性能在中等召回率要求下如95%以上HNSW的搜索速度通常是其他主流ANN算法如IVF, Annoy, LSH中最快的之一尤其是在高维数据上。高召回率由于其图结构的特性HNSW能够达到非常高的近似精度常常接近暴力搜索的结果。无需训练与一些基于量化的方法如PQ, IVFPQ不同HNSW不需要一个独立的“训练”阶段来学习码本或聚类中心。它是增量构建的支持动态插入和删除删除需要标记为逻辑删除物理删除比较复杂这对流式数据场景非常友好。参数相对简单核心参数就M,efConstruction,efSearch几个比一些需要调整大量超参数的算法如基于树的方法更易于理解和调优。内存效率尚可虽然纯图结构的内存占用比量化方法高需要存储原始向量和邻居列表但通过结合标量量化SQ或乘积量化PQ可以大幅压缩向量内存形成HNSWPQ的经典组合在内存和速度间取得更好平衡。6.2 局限性与挑战构建速度较慢由于需要为每个新节点执行多次搜索和邻居选择HNSW的构建时间比一些简单索引如IVF要长。对于超大规模数据集百亿级别构建时间可能成为瓶颈。内存占用存储邻居列表需要额外内存。每个节点大约需要M * sizeof(id)的额外空间。对于十亿级数据这个开销不容忽视。删除操作复杂虽然支持但高效的物理删除会破坏图结构通常采用“逻辑删除”标记无效 定期重建索引的策略。对数据分布敏感虽然比许多算法更鲁棒但极端的数据分布如所有数据点聚集在几个非常远的簇中仍可能影响其性能需要调整M等参数来应对。6.3 典型应用场景HNSW几乎成为了现代向量检索的“标配”广泛应用于图像/视频检索以图搜图、视频内容去重。将图像通过CNN模型提取为特征向量用HNSW构建索引。文本语义搜索基于BERT、Sentence-BERT等模型将文本转换为向量实现“意思相近”的文档搜索。推荐系统将用户和物品嵌入到同一向量空间用HNSW快速寻找相似用户或物品。分子/化合物相似性搜索在生物信息学和药物发现中快速寻找结构相似的分子。多模态检索跨文本、图像、音频的联合搜索背后依赖的是统一的向量表示和高效的HNSW索引。7. 实战使用Faiss库实现HNSW索引与查询理论说得再多不如动手一试。我们以Facebook AI开源的Faiss库为例展示HNSW索引的构建和查询全流程。Faiss的HNSW实现IndexHNSW非常高效并且支持与量化器结合。假设我们有一个包含100万个128维向量的数据集data以及1000个查询向量queries。我们的目标是找到每个查询的top-10最近邻。import numpy as np import faiss # 1. 生成模拟数据 d 128 # 向量维度 nb 1000000 # 数据库大小 nq 1000 # 查询数量 np.random.seed(1234) data np.random.random((nb, d)).astype(float32) queries np.random.random((nq, d)).astype(float32) # 2. 创建纯HNSW索引 M 32 # 每个节点的连接数 index_hnsw faiss.IndexHNSWFlat(d, M) # Flat 表示存储原始向量 print(f索引是否已训练: {index_hnsw.is_trained}) # HNSW无需训练应返回True # 3. 可选设置构建参数 # index_hnsw.hnsw.efConstruction 200 # 默认值为40通常需要调高 # index_hnsw.hnsw.efSearch 128 # 默认值为16搜索前需设置 # 4. 添加数据构建索引 print(开始构建索引...) index_hnsw.add(data) print(f索引中的向量数: {index_hnsw.ntotal}) # 5. 设置搜索参数并执行搜索 k 10 # 寻找每个查询的10个最近邻 index_hnsw.hnsw.efSearch 256 # 提高efSearch以获得更高召回率 print(开始搜索...) D, I index_hnsw.search(queries, k) # D是距离矩阵I是索引ID矩阵 print(f搜索结果形状: D {D.shape}, I {I.shape}) print(f前5个查询的第一个最近邻ID: {I[:5, 0]}) print(f对应的距离: {D[:5, 0]}) # 6. 与暴力搜索对比评估召回率 print(\n--- 评估召回率 ---) index_flat faiss.IndexFlatL2(d) # 创建暴力搜索索引 index_flat.add(data) D_flat, I_flat index_flat.search(queries, k) # 计算召回率HNSW结果中有多少出现在真实top-k中 recall_at_k 0 for i in range(nq): recall_at_k len(set(I[i]) set(I_flat[i])) / k recall_at_k / nq print(fTop-{k} 平均召回率: {recall_at_k:.4f})代码解读与注意事项IndexHNSWFlat创建了一个使用L2距离欧氏距离的HNSW索引并存储原始向量。内存占用约为nb * d * 4 nb * M * 4字节假设ID为int32。efConstruction和efSearch是index_hnsw.hnsw对象的属性可以在构建前和搜索前分别设置。务必在add()数据前设置efConstruction在search()前设置efSearch。构建过程add包含了插入和建图的所有步骤对于100万数据可能需要几十秒到几分钟取决于参数和硬件。搜索返回的D是距离的平方对于L2距离I是对应的向量在数据库中的索引。与IndexFlatL2暴力搜索的结果对比是评估ANN索引召回率的常用方法。性能优化进阶 对于十亿级数据纯IndexHNSWFlat内存会爆掉。此时需要结合量化# 使用HNSW 乘积量化 (PQ) 压缩向量 m 16 # PQ子空间数量必须能被维度d整除 nbits 8 # 每个子量化器的比特数 quantizer faiss.IndexHNSWFlat(d, M) # 使用HNSW作为量化器的粗量化器 index_hnsw_pq faiss.IndexHNSWPQ(quantizer, d, m, nbits) # 或者使用更常见的IVF_HNSW结构 # nlist 4096 # 聚类中心数 # quantizer faiss.IndexHNSWFlat(d, M) # index_ivf faiss.IndexIVFPQ(quantizer, d, nlist, m, nbits) # index_ivf.train(training_data) # 需要训练PQ码本 # index_ivf.add(data)IndexHNSWPQ将向量用PQ压缩大大减少了内存占用但搜索时需要解量化计算距离会稍微增加计算开销。这是工业界处理超大规模向量的标准做法之一。8. 避坑指南HNSW实践中的常见问题与解决思路在实际项目中应用HNSW你可能会遇到以下几个典型问题8.1 召回率不达标但搜索速度很快现象efSearch已经调到很大比如500但召回率依然低于预期。根因分析这通常不是搜索参数的问题而是索引构建质量不高。efConstruction可能设置得太低导致在构建图时邻居选择不够优化图的“导航性”差。即使搜索时看再多候选点也因为图结构不好而无法抵达真正的最远邻区域。解决方案首要检查efConstruction将其大幅提高例如设为400、800甚至1000重新构建索引。这是提升召回率最有效的手段之一。检查M参数M太小可能导致图连通性不足尝试增大M如从16增加到32或48。验证距离度量确保构建和搜索使用的距离度量如L2、内积与你的相似性定义一致。Faiss的IndexHNSWFlat默认使用L2距离。如果你的相似度是余弦相似度需要对向量进行L2归一化后使用内积或L2距离。8.2 构建时间过长无法接受现象对于大数据集索引构建需要数小时甚至数天。根因分析构建时间复杂度大致为O(N * log(N) * efConstruction)。efConstruction和M是主要影响因素。解决方案降低efConstruction这是最直接的方法但会牺牲搜索性能召回率或速度。需要根据业务容忍度权衡。降低M同样会牺牲图质量需谨慎。并行化构建Faiss的HNSW实现是单线程插入的。对于超大数据集可以考虑将数据分片Sharding为每个分片独立构建HNSW索引搜索时查询所有分片并合并结果。或者寻找支持多线程构建的HNSW实现如Hnswlib库。使用更强的硬件构建过程是CPU密集型的使用更多核心、更高主频的CPU能直接加速。8.3 内存占用过高现象索引加载后内存使用远超原始向量数据大小。根因分析纯HNSWIndexHNSWFlat内存开销 原始向量内存 邻居列表内存。邻居列表内存约为N * M * sizeof(id)。对于10亿数据M32使用int64作为ID额外内存约为1e9 * 32 * 8 bytes ≈ 256 GB非常庞大。解决方案结合向量量化使用IndexHNSWPQ或IndexIVF_HNSW。PQ可以将向量压缩到原来的1/4甚至更少是减少内存的终极武器。例如用m16, nbits8的PQ一个128维float32向量512字节可压缩为16字节压缩比32倍。使用更小的数据类型如果向量维度允许可以使用float16甚至int8来存储原始向量Faiss支持。优化ID类型如果数据量小于2^32可以使用uint32而不是int64来存储邻居ID内存减半。8.4 搜索性能随数据量增长而下降明显现象数据量从百万级增加到千万级后搜索延迟增长不符合对数预期。根因分析HNSW的理论复杂度是O(log N)但常数项很大。当数据量极大时即使是对数增长延迟也可能达到业务上限。此外如果M参数没有随数据量调整图的质量可能会下降。解决方案调整参数增大数据量后可能需要适当增大M来维持图的连通性和导航效率。引入粗量化器采用多级索引结构如IndexIVF_HNSW。先用倒排文件IVF进行粗筛选找到最相关的几个聚类再在聚类内部使用HNSW进行精细搜索。这可以极大减少HNSW需要搜索的基数。硬件与工程优化使用多线程并行搜索Faiss的search方法本身支持多线程或使用GPU加速Faiss-GPU。对于分布式场景采用分片索引。8.5 动态插入新数据后性能下降现象索引支持动态add但随着新数据不断加入搜索效率逐渐变差。根因分析HNSW的增量插入虽然方便但新插入的节点只能连接到当时已存在的节点。这可能导致后期插入的数据形成的“局部图”与早期数据的“全局图”连接不够充分破坏了图的整体最优结构。解决方案定期重建索引这是最可靠的方法。设定一个阈值如数据量增长20%在业务低峰期触发全量索引重建。批量插入尽量以批量batch的方式插入数据而不是单条插入。批量插入时可以暂时调高efConstruction来改善这一批数据内部的连接质量。使用“边缘软化”策略一些高级实现允许在插入新节点时不仅连接新老节点还会对老节点之间的边进行有限的重新评估和优化但这会显著增加插入成本。HNSW是一个强大而实用的工具但它不是黑盒。理解其原理谨慎调参并结合具体业务场景和数据特性进行优化才能让它真正成为你解决向量检索难题的利器。从我自己的经验来看花时间在离线阶段做好参数扫描和基准测试远比在线上盲目调整和故障排查要划算得多。
返回列表