拓冰建站拓冰建站
首页 / 资讯中心 / 正文

向量数据库底层揭秘:ANN、HNSW、LSH、PQ算法解析与实战

向量数据库的底层秘密ANN、HNSW、LSH、PQ 到底在解决什么问题这两年做 AI 应用你会发现一个尴尬的事实模型能力上来了但你的知识库、记忆体、检索模块还停留在“关键词匹配”的阶段。无论是给大模型接企业知识库还是做 AI 智能体的长期记忆最终都绕不开一个问题怎么在海量向量数据里快速找到“最相似”的那一批向量数据库就是干这个的。它的核心不是存数据而是用一套高效的索引结构和检索算法把“暴力遍历所有向量”这件事变得尽可能快。你去看市面上主流的向量数据库Milvus、Qdrant、Weaviate、Redis 的向量模块底层说到底都是在对 ANNApproximate Nearest Neighbor近似最近邻算法做工程化封装。这篇文章我尽量用大白话加实操视角把这套东西的核心讲清楚。包括最常用的 HNSW、经典的 LSH、工业界标配的 PQ乘积量化以及最容易踩坑的相似度度量问题。看完之后你应该能搞清楚为什么向量数据库这么快、HNSW 到底好在哪里、什么场景该用哪个算法以及当你往项目里引入向量检索时哪些参数直接决定系统能不能跑起来。这篇文章适合刚接触向量检索的开发者也适合已经在用 Milvus 之类产品、但对内部机制一直“知其然不知其所以然”的朋友。1. 向量数据库到底是个什么东西先把你对“数据库”的固有印象放一边。传统关系型数据库存的是结构化数据比如用户表、订单表查找靠的是 B 树索引或者哈希索引。传统全文搜索引擎比如 Elasticsearch存的是文档查找靠的是倒排索引。它们的共同点是匹配逻辑是基于“精确值”或“关键词”的。向量数据库不一样。它存的是向量——也就是一组浮点数。比如你把一篇文章用 OpenAI 的 embedding 模型转成一个 1536 维的向量或者用 BGE 模型转成一个 1024 维的向量这个向量本质上就是这段文本在高维空间里的“坐标”。问题来了怎么从这个坐标系统里找到“语义最接近”的另一个坐标你不能像 SQL 那样写WHERE vector ?因为语义相似不是精确匹配而且向量也不可能完全相等。真正的做法是算距离两个向量在空间里距离越近语义上就越接近。所以向量数据库的检索逻辑本质上是一个 KNNK 近邻问题给定一个查询向量在 N 个向量里找出距离最近的 K 个。最笨的办法是全表扫描一一比较N 如果是一百万每次查询都要算一百万次距离虽然现代 CPU 算距离不慢但问题是你的业务不可能只承载一次查询QPS 一上来就崩。于是有了 ANN也就是近似最近邻。核心思想很简单不追求百分之百找到真正的最近邻而是用“足够接近”的结果换取数量级上的性能提升。这就像你在一个十万人城市找一个人不挨家挨户敲门而是先通过“同姓”“同小区”“同年龄层”这些特征快速缩小范围。ANN 的“缩小范围”策略就是各个算法百花齐放的地方。1.1 一次完整检索流程召回、粗排、精排实际工程里向量召回通常不是单独用的。以企业知识库问答为例典型流程是用户提一个问题比如“我们的报销流程是什么”把问题文本通过 embedding 模型转成查询向量 q。在向量数据库里用 ANN 索引召回 top-100 候选片段注意这里已经近似了不是全量比对。有可能再结合关键词检索BM25做混合召回把语义和字面两个维度的结果合并。最后有一个重排rerank环节用更精细的模型比如 cross-encoder在这 100 条候选里精排选出 top-5 喂给大模型。认识到这个流程很重要因为它决定了你对向量数据库的“要求”。在实际项目中向量数据库保证的是召回阶段的“查得全”和“速度快”它会故意召回多一些候选比如 100 条把精度问题丢给后面的精排去解决。这也是为什么很多向量数据库的默认 topK 设置都不会太小。1.2 相似度度量怎么选内积、余弦、欧氏距离很多人上手向量数据库第一个忽略的问题就是距离度量。你以为这不重要实际上它直接影响你能不能召回正确结果。欧氏距离L2计算的是空间中的直线距离数值越小越相似。它适合向量经过归一化处理的场景对向量绝对位置敏感。内积IP数值越大越相似。适合向量没归一化、长度本身携带信息量的场景比如某些推荐场景中向量模长代表热度。余弦相似度衡量的是向量方向的一致性范围是 -1 到 1值越大越相似。它只关心方向不关心模长是文本语义场景用最多的。关键坑在于很多向量数据库的索引结构是为特定的距离度量设计的。比如 HNSW 在实现时距离函数的选择会影响图的构建过程。你在 Milvus 里如果在创建索引时选了 L2在查询时又指定用 IP 去算返回的结果就是错的。我自己就见过同事在 Milvus 里建 collection 时用了 L2然后查询时用 cosine 相似度去过滤分数小于 0.8 的结果直接导致召回为空。文本场景我给你的建议是embedding 模型如果没做归一化优先用余弦如果你的 embedding 本身已经 normalize 过了那用内积其实等价于余弦而且计算更快。欧氏距离在向量值本身有意义、需要区分绝对大小的时候更合适。2. 三类最主流的 ANN 算法思路完全不同2.1 暴力搜索所有方案的基线在讲聪明算法之前先提一下暴力搜索Flat 索引。所谓暴力搜索就是不管索引结构直接在全部向量上遍历计算距离然后取最小。它是所有 ANN 算法的精度上限和速度下限。优点100% 召回率也就是不丢结果。缺点数据量稍大就扛不住。在 Milvus 里对应的是 FLAT 索引类型。适合数据量很小几万条以内、对精度要求严格、或者作为 Baseline 测试的场景。实际项目里我一般不推荐直接用 FLAT除非你的数据量真的小到无所谓。2.2 HNSW当前综合体验最好的图算法HNSWHierarchical Navigable Small World分层可导航小世界图的思想来源可以追溯到“六度分隔”理论也就是小世界网络。在实际图结构里每个节点连接其邻居只要连接关系足够合理从任何一个点出发几步之内就能到达目标附近。HNSW 的聪明之处在于“多层”。想象一个只有一层的图网络。如果这个图里每个节点都只连接最近的两个邻居那么从起点到目标路径可能会很长因为路上只能一家一家地“跳”。但如果把图的尺度拉开——上层是很稀疏的“远距离通道”下层是很密集的“短距离连接”就可以做到在高层快速接近目标区域再到低层精细定位。图 1示意第 0 层包含所有向量连接细密负责精确定位。第 1 层包含约 1/M_l的节点连接更稀疏负责快速跳过无关区域。第 2 层更稀疏属于“高速公路”。这就是 HNSW 的“分层”含义——它结构上非常像跳表。构建时每个节点会以指数衰减概率分配一个层数搜索时从顶层开始在当前层的邻居里贪心找“距离查询向量最近的邻居”然后下沉到下一层重复这个过程直到第 0 层。几个关键参数我直接给经验值参数含义建议值M每个节点的最大连接数控制图的密度16~32常见 16efConstruction构建图时的动态候选集大小越大图质量越高但构建越慢100~200常用 200efSearch查询时动态候选集大小越大召回率越高但延迟越高查询时动态调整常用 64~256我踩过的一个很深坑efSearch设太小。项目里用了 HNSW结果召回率一直不达标折腾半天最后发现只是查询时 efSearch 设成了 10。其实 HNSW 的设计里M决定索引构建质量efSearch决定查询质量。召回率不够优先调大efSearch而不是去重建索引。HNSW 的缺点也很明确索引全在内存里内存占用偏高增量插入性能差一些。这在数据千万级以上时尤其明显内存翻好几倍。另外如果你的数据流经常有大量实时删除和更新HNSW 也显得笨重——它删除节点后图结构不会自动优化需要周期性重建。2.3 LSH用哈希把“近邻”分到同一个桶LSHLocal Sensitive Hashing局部敏感哈希的思路和 HNSW 完全不同它是给向量做“哈希分桶”但是呢这个哈希函数是精心设计的满足一个特性两个越相似的向量哈希到同一个桶的概率越大。这和普通哈希正好相反——普通哈希要求“哪怕只差一位输入输出也天差地别”而 LSH 要求“越相似哈希值越接近”。以文本向量为例最朴素的 LSH 实现是随机投影法Random Projection。它的哈希函数是随机生成一组超平面然后判断向量在超平面的哪一侧。多个超平面组合成一个哈希值。相近的向量大概率在每个平面上的相对位置一致因此哈希值一致被分到同一个桶。查询时只需要计算查询向量的哈希值然后去对应的桶里做精确匹配就行。复杂度从 O(N) 降到了 O(桶内数量)大幅提高速度。但 LSH 的问题也很明显实际项目里我很少直接用它做主索引召回质量不稳定。哈希分桶是概率性的而且桶与桶之间有“边界效应”——两个很相似的向量刚好落在桶边界两侧就可能被分到不同桶。理论上可以通过多表多个 LSH 函数并行缓解但代价是存储量乘以表数。对高维数据不友好。维度越高随机投影越难保持局部性需要更多的哈希表才能维持召回率内存直接起飞。现在的向量数据库里LSH 更多是作为某些特定场景的补充方案比如均匀分布的低维向量或者数据分布比较规律的情况。主流产品的默认索引里很少是 LSH。2.4 PQ压缩向量用查表代替计算PQProduct Quantization乘积量化的思路和前面两个都不一样它不做索引结构而是做“向量压缩 距离查表”。它的核心思想是把高维向量拆成若干个子向量对每个子向量空间单独做聚类比如 KMeans然后用聚类中心的编号来“编码”原始向量。举个例子。一个 128 维的向量拆成 8 段每段 16 维。对每一段做 256 个聚类中心也就是每一段可以用 8 个 bit 编码2^8256。这样一来一个 128 维向量32 个 float128 字节可以被压缩成 8 个 byte。查询时的距离计算也不用重建完整向量而是用“查表”的方式把查询向量对应段与聚类中心的距离预先算好存成表格再根据目标向量的编码从表里查出近似距离。实际工程中直接用 PQ 的情况少更多是配合倒排索引使用这就叫 IVF-PQInverted File with Product Quantization。流程是先对整个数据集做一次粗聚类比如 KMeans聚类数设为 nlist。每个向量归属一个聚类中心建立倒排列表。查询时先找到查询向量最近的几个聚类中心nprobe 参数控制搜几个簇只在这几个簇内部用 PQ 做精确重排。这在 Milvus、Faiss 里是最常见的工业级方案。它最大的优点是内存占用极小适合亿级以上的大规模场景。缺点则是精度有损尤其当编码数m太小的时候召回质量会明显下降。2.5 三种算法横向对比为了让你更直观地选择我做了一张表算法核心思路内存占用查询速度召回精度适用场景HNSW多层图近似最短路径高极快高千万级以下对召回率要求高的在线场景LSH哈希分桶中快依赖桶内数据量中低极大规模且对精度要求不苛刻或低维数据IVF-PQ倒排 向量压缩低快依赖 nprobe中亿级以上内存有限允许一定误差这不是绝对的因为实际工程里还有各种魔改版本比如 Milvus 的HNSW_SQ、HNSW_PQ等混合索引目的就是结合 HNSW 的精度和 PQ 的内存优势。但作为入门先把这三个核心思路吃透后面看什么都顺。3. 动手实战用 hnswlib 搭一个可运行的向量检索 Demo原理讲再多不如跑一次代码。这里我用一个非常轻量级的 Python 库——hnswlib它是 HNSW 算法的 C 封装接口简单性能很好。同时用numpy生成模拟的 embedding 数据来演示整个构建和查询过程。按下面的步骤一步步来。3.1 准备环境与数据pip install hnswlib numpy生成 10 万条 128 维的模拟向量数据import numpy as np import hnswlib dim 128 num_elements 100000 # 生成随机向量模拟 embedding 数据 data np.random.random((num_elements, dim)).astype(np.float32) # 生成 100 条查询向量 queries np.random.random((100, dim)).astype(np.float32)3.2 构建索引# 初始化索引 # space 可选 l2欧氏距离、ip内积、cosine余弦 p hnswlib.Index(spacecosine, dimdim) # 初始化索引结构 # max_elements 是索引最多容纳的元素数量 # ef_construction 是上面讲过的构建参数 p.init_index(max_elementsnum_elements, ef_construction200, M16) # 添加数据 p.add_items(data) # 设置查询时的 efSearch 参数 p.set_ef(64)每一步在干什么说明一下spacecosine告诉索引用余弦距离来计算向量之间的远近。如果换成 L2同样的 HNSW 图边的“距离”衡量方式会完全不同。M16每个节点最多连 16 个邻居。M 越大图越密查询越慢召回率越高M 越小图越稀疏查询越快但容易丢邻居。ef_construction200插入每个节点时为了找到合适的邻居会先找 200 个候选然后从中择优。这个值越大构建越慢但图质量越高。3.3 查询与评估召回率# 查询前 10 个最近邻 labels, distances p.knn_query(queries, k10) # 输出第一条查询的结果 print(第一个查询向量的 top-10 结果索引, labels[0]) print(对应的距离余弦距离越小越相似, distances[0])要直观感受 HNSW 的“近似”到底损失了多少可以做一个召回率测试先构建一个 FLAT 暴力索引查询同一个 top-10 集合然后计算 HNSW 的 top-10 与暴力搜索 top-10 的重合度。# 用暴力搜索作为 ground truth精确结果 flat_index hnswlib.Index(spacecosine, dimdim) flat_index.init_index(max_elementsnum_elements, ef_constructionnum_elements, M16) flat_index.add_items(data) # 把 ef 设到最大的等效暴力搜索 flat_index.set_ef(num_elements) flat_labels, _ flat_index.knn_query(queries, k10) # 计算召回率 hit 0 total 0 for i in range(len(queries)): hit len(set(labels[i]) set(flat_labels[i])) total 10 recall hit / total print(fHNSW 召回率{recall:.2%})正常跑下来召回率一般在 95% 以上。如果调低ef或者M速度上去了但召回率会掉下来。这就是精度与性能的权衡这个代码完全可以拿来做实验直观感受参数对召回率的影响。3.4 换个方案试试用 Faiss 实现 IVF-PQ再看一下工业级方案 Faiss 怎么实现 IVF-PQ这样你对工程里的主流方案有直观感受pip install faiss-cpuimport faiss import numpy as np dim 128 num_elements 100000 data np.random.random((num_elements, dim)).astype(np.float32) # 使用 IVF-PQ1024 个聚类中心每个向量编码成 16 字节 nlist 1024 m 16 quantizer faiss.IndexFlatIP(dim) index faiss.IndexIVFPQ(quantizer, dim, nlist, m, 8) index.train(data) index.add(data) # 查询时搜索最近邻的 8 个聚类中心 index.nprobe 8 D, I index.search(data[:5], k10) print(I)这里的逻辑是nlist1024意味着把数据聚成 1024 个簇m16意味着把一个 128 维向量切分为 16 段每段 8 维每段用 256 个聚类中心编码。如果数据量大这个索引的内存占用大概只有原始向量的 1/8 到 1/16。4. 工程落地时绕不开的选型与调优问题原理通了代码跑了下一步就是在真实项目里选型和调优。这一部分我说的都是实战经验不一定写在官方文档里。4.1 怎么选HNSW 还是 IVF-PQ 还是混合一个核心的决策依据是数据量和内存预算。如果数据量在百万级以内内存不紧张HNSW 是综合最优解实现简单、召回率高、查询快。如果数据量到了千万级以上或者索引需要常驻内存但机器内存有限IVF-PQ 或 HNSW-PQ 这类“有损压缩”方案就该上了。代价是精度损失和召回率波动需要接受。如果你的业务是“导入大量历史数据 增量实时写入”混合模式需要注意HNSW 的增量插入会导致图结构逐渐劣化官方推荐定期重建索引。所以这种场景下可以考虑对热数据用 HNSW冷数据用 IVF-PQ分层处理。4.2 参数调到什么程度算“好”各参数经验值参数名适用算法经验区间调参逻辑efConstructionHNSW100~300建索引时用越大图质量越高构建越慢MHNSW8~48空间换时间M 翻倍内存约增 20%efSearchHNSW64~512查询时调越大召回越高延迟越高nlistIVF数据量开根号左右比如 1000 万数据nlist ≈ 10000nprobeIVF-PQ8~256每增大一倍速度约为原来的一半但召回率提升递减mPQ1/8 向量维度编码越小压缩越大召回越低调参宗旨是先保证召回率达标再看性能要不要优化。很多人上来就把 efSearch 调到 16 追求极致性能结果召回率掉到 80%业务直接不可用。4.3 Milvus、Redis 等产品选型参考现在的向量数据库产品非常多但底层索引大多基于 Faiss、HNSWLib 这类组件。区别主要在工程能力上。产品底层索引特点MilvusFaiss、HNSWLib、DiskANN功能全支持混合检索和标量过滤适合大规模生产Qdrant自研 HNSW 实现轻量Rust 编写RESTful API 友好WeaviateHNSW 为主自带模块化插件和 GraphQL 深度整合Redis Stack模块化实现 HNSW适合轻量级、已有 Redis 技术栈的场景pgvectorIVFFlat、HNSW适合 PostgreSQL 用户不需要额外引入数据库如果你是做 AI 智能体的知识库团队已有 PostgreSQL数据量不大pgvector 是最快上手的方案。如果数据量大、检索要求高、且有独立的向量检索服务需求Milvus 是更靠谱的选择。5. 常见问题与排查技巧实录最后把这几年被问得最多、我自己也踩过的坑整理成一张速查表建议收藏。症状可能原因解决方案召回结果明显不相关距离度量选错如该用余弦却用了 L2重建索引统一 space 参数召回率一直上不去efSearch 太小或 nprobe 太小调大 efSearch/nprobe观察召回变化索引构建极慢efConstruction 太大或 M 太大适当降低 efConstruction 到 100M 到 16内存占用爆炸HNSW 的 M 偏大 数据量超预期用 IVF-PQ 或者加节点扩容删除数据后检索变慢HNSW 图结构劣化定期重建索引或者业务层做“软删除”为什么加了标量过滤后变慢向量索引无法直接做多条件过滤Milvus 使用标量倒排索引或调整过滤比例后选择底层实现更新向量后检索结果不对旧向量未被真正删除图里存在“幽灵”节点检查产品删除语义必要时触发索引重建还有一个很重要的独门经验是在生产环境上线前一定要做一个“召回率回归测试集”。用一批已知正确答案的 query把向量数据库的召回率作为监控指标。这个测试集可以在改参数、升级版本、换向量模型之后自动跑一遍防止“怎么变差了都没发现”。我从去年开始在多个项目里强制要求加这个回归测试。一个真实案例是有次同事升级了 embedding 模型旧向量没重新生成结果线上检索结果质量大幅下降但系统本身没报错要不是回归测试根本发现不了。写到这里回头看这个领域你会发现向量数据库从来不是“新技术”它更像是把老算法KNN、聚类、哈希、图论在大规模数据和新场景下重新做得更极致。HNSW、LSH、PQ 各有各的适用面没有银弹只有理解和权衡。我个人在实际项目中的体感是如果只是做个 Demo 或者数据量很小别折腾复杂索引FLAT 都够用。一旦数据量跨过百万级优先上一个经过验证的索引方案HNSW 优先并从一开始就把召回率监控、参数调优流程搭好。这套东西越早做后面越省心。最后分享一个小技巧当你不确定某个索引的参数怎么配先用数据集的 1/10 做一轮小规模测试观察召回率和延迟的曲线再决定全量参数配置。磨刀不误砍柴工这个习惯能帮你省掉大量线上调试的时间。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门