向量数据库入门:从Embedding到ANN索引的完整解析

📅 发布时间:2026/10/15 21:09:37
向量数据库入门:从Embedding到ANN索引的完整解析
1. 从一篇踩坑日记说起为什么我需要向量数据库事情是这样的几个月前我在做一个图片相似度检索的小项目。数据量不算大大概几万张商品图需求是用户上传一张图系统找出最相似的几张。我当时的第一反应是用传统方案先把图片打标签再用SQL的LIKE或者倒排索引去匹配。试了两周效果一言难尽——两朵白云和一片奶油这种语义上的相似关键词根本表达不出来。后来了解到了embedding这个概念也就是把图片、文本、音频这类非结构化数据转成一串固定长度的数字数组然后通过计算数组之间的距离来判断相似度。思路是对的但紧接着问题就来了这串数字数组怎么存、怎么算传统的关系型数据库可以存数组但压根没有高效的相似度搜索能力我用某个开源搜索引擎试过分词和精确匹配很强对向量检索的支持却非常鸡肋。几万条数据暴力算距离还能勉强接受等数据量涨到百万级CPU直接烧穿一条查询要好几秒完全没法上线。后来我才系统地接触了向量数据库花了两三周做研究、写Demo、跑基准测试总算把整体脉络摸清楚了。这篇笔记就是我当时的学习记录去掉了大部分过时信息保留了真正有用的部分。如果你也在做推荐系统、语义搜索、图像识别、大模型知识库这类项目或者刚接触向量检索一脸懵这篇文章应该能帮你把概念和实操一次串起来。先说结论向量数据库不是什么玄学核心就两件事——把数据变成向量然后用高效的索引结构快速找到最相似的向量。所有花里胡哨的功能都是围绕这两件事展开的。2. 先搞明白向量是什么从词袋到语义坐标2.1 One-Hot编码为什么不好用要理解向量数据库必须先理解向量的意义。我最早接触文本向量化是很多年前学自然语言处理时碰到的one-hot编码。比如有三个词苹果、香蕉、手机用one-hot表示就是苹果 [1, 0, 0]香蕉 [0, 1, 0]手机 [0, 0, 1]这种表示方式的致命问题是每个词之间距离完全相等。苹果和香蕉的距离是√2苹果和手机的距离也是√2。但在真实语义里苹果和香蕉显然比苹果和手机更接近。one-hot只解决了怎么表示的问题完全没有解决语义关系的问题。后来有了词嵌入技术比如Word2Vec、GloVe这类方法。它们把每个词映射到一个多维空间里的点语义相近的词在空间里也离得近。同样的三个词学到的向量可能长这样苹果 [0.82, 0.31, 0.15]香蕉 [0.78, 0.28, 0.19]手机 [0.12, 0.55, 0.87]这时候苹果和香蕉的余弦相似度很高手机和它们都远。这个把语义编码成坐标的过程就叫做embedding生成的数组就叫向量。2.2 用生活类比理解Embedding我经常用一个类比来帮助新手理解embedding想象一个巨大的超市货架上分区域摆放商品。蔬菜区里生菜和菠菜放在一起牛奶和酸奶放在冷柜挨着榴莲则被单独隔离在角落里还盖着罩子。embedding做的事情就是把商品名称这个字符串映射到超市里的一个货架位置。位置越近语义越接近。一个训练好的embedding模型本质上就是一张语义地图的生成器。你给它一句话、一张图、一段音轨它告诉你这张地图上的坐标点。比如大模型领域常用的文本embedding模型输入今天天气真不错输出一个768维或者1024维的向量图像领域常用的一些视觉模型输入一张224x224的图片输出一个2048维的特征向量。向量数据库干的事情就变成了你把一堆坐标点存起来用户拿着一个新坐标来问我的坐标附近都有谁数据库负责用最快速度回答这个问题。2.3 向量维度不是越多越好这里必须提一个新手常犯的认知误区以为向量维度越高越精确。理论上确实如此比如一个物体用1000个特征描述肯定比用3个特征描述得更全面但在实际工程中有一个叫维数灾难的现象——维度越高数据在空间里的分布越稀疏距离度量越不可靠计算开销也越大。我实测过一个案例用同一个embedding模型生成512维向量和把向量降维到128维后再做相似度检索128维的结果在某些语义模糊的样本上反而更稳定。原因就是过高的维度引入了大量噪声特征。实际项目中向量维度通常在128到1024之间少数场景会用到2048以上。具体选多少取决于你的embedding模型和业务需求不是越大越好。3. 相似度计算向量数据库的核心数学向量数据库的比较不是靠数据库的等值查询而是靠距离函数。你要知道向量本身只是坐标真正决定谁和谁相似的是距离的计算方式。不同距离函数适应不同数据类型和业务场景这个选择直接影响检索质量。3.1 三种最常见的度量方式余弦相似度Cosine Similarity计算两个向量之间的夹角余弦值只关心方向不关心长度。公式是cosine_similarity(A, B) (A·B) / (|A| * |B|)这个度量在文本领域用得最多因为文本向量经常受文章长度影响——同一个话题的长文和短文embedding向量长度差异很大但方向接近。余弦只关心方向的特性正好削弱了文本长度的干扰。取值范围是[-1, 1]越大越相似。欧氏距离L2距离就是大家在中学学过的两点之间直线距离。公式是euclidean_distance(A, B) sqrt(Σ(Ai - Bi)^2)适合图像特征向量、音频向量这类绝对位置有意义的场景。数值越小越相似。注意一点余弦相似度的值域是[-1, 1]而欧氏距离理论上是[0, ∞)两者不能直接混用阈值。内积Dot Product直接把两个向量对应位置相乘再求和。公式dot_product(A, B) Σ(Ai * Bi)内积同时考虑方向和长度越大越相似。在推荐系统里很好用因为长度往往代表了某种热度或偏好强度。3.2 向量归一化带来的意外收益我在实际项目里发现一个很实用的技巧在做余弦相似度检索时先把所有向量做L2归一化也就是让向量长度变成1这时候余弦相似度、内积、欧氏距离三者就产生了等价关系。数学上可以证明对于归一化后的向量A和B|A - B|² 2 - 2 * cos(A, B)也就是说对归一化向量来说距离越小等价于余弦相似度越大等价于内积越大。很多向量数据库在内部就是这么优化的——你配置了余弦相似度引擎可能先把向量归一化然后实际用的是内积计算因为内积在硬件层面上有更快的指令支持。这个细节对调参非常重要。我在做某图像检索项目时就遇到过一个怪问题索引配的是内积数据没归一化查出来的结果排序完全不对。后来才意识到内积对长度敏感同一个类别的图片亮度高的向量长度大排序时全被顶到前面去了。归一化以后问题立刻消失。3.3 原始向量必须存下来这里先埋一个伏笔后面讲索引时再展开向量数据库里存在两套数据——原始向量和压缩后的索引数据结构。原始向量必须完整保留因为索引结构尤其是量化类的只会存储近似数据最终计算距离时仍然要用原始向量做精确比对。所以向量数据库的存储成本包含两部分索引开销加上原始数据开销这个账要算清楚别以为压缩完就完事了。4. 暴力搜索和ANN向量数据库快的原因4.1 暴力搜索的真实代价假设现在有100万条向量每条128维。用户查询时最朴素的做法是逐一计算这100万条向量和查询向量的距离然后排序取TopK。这个过程叫暴力搜索Brute Force Search也叫线性扫描。算一笔账100万次128维的浮点数距离计算在我的测试机器上大约需要几十毫秒到几百毫秒。听起来好像还行但你要知道这仅仅是一条查询。如果每秒有100个请求进来CPU基本被打满延迟飙到秒级数据量涨到1亿条单次查询就要好几秒。更重要的是暴力搜索没法用传统索引加速。关系型数据库的B树索引能加速等值查询和范围查询是因为数据在某个维度上有序。高维空间里没有天然的全序关系你必须遍历每一个点才能确定哪些点离你最近。4.2 ANN的核心思想用精度换速度近似最近邻检索ANN, Approximate Nearest Neighbor的思路是我不保证每次都找到全局最优解但我保证大部分时候找到的解足够好同时速度比暴力搜索快几个数量级。打个比方你要在100万人的城市里找某个人的朋友。暴力搜索的做法是挨个儿见所有人ANN的做法是先通过某种机制把城市划分成若干街区根据目标人的特征先确定他大概率住在哪个街区然后只在这个街区里仔细寻找如果实在找不到再扩大到附近街区。损失一点确定性换来的是极大的速度提升。向量数据库几乎全部依赖ANN索引这是一切性能的基础。同一个数据集插上不同的ANN索引算法查询性能天差地别。4.3 从暴力搜索评估索引效果我在刚接触向量数据库时总觉得暴力搜索虽然慢但至少准确而ANN不靠谱。后来做了一个对比实验10万条128维向量暴力搜索召回率100%单次查询80毫秒用HNSW索引后召回率97%左右单次查询0.3毫秒。接近300倍的性能差距只损失3%的召回率。在绝大多数业务场景里97%的召回率完全够用300倍的速度提升却是巨大的体验差异。如果追求极致准确性很多系统还支持混合方案先用ANN召回一个稍大的候选集比如Top100再用暴力重排拿到最终Top10。这个套路在推荐系统里很常见效果和性能兼顾。5. 主流ANN索引算法拆解HNSW和IVF背后的直觉向量数据库的索引算法有很多种但当前实际生产环境里最主流的有两类基于图的HNSW和基于倒排的IVF以及它们的各种组合变体。我学习的时候走了很多弯路这里尽量用大白话把核心机制讲清楚。5.1 HNSW多层图导航像一个先远后近的GPSHNSW全称是Hierarchical Navigable Small World分层可导航小世界网络。听起来很高深拆开看其实每层都很简单。底层逻辑是一个跳表图的混合体构建索引时把向量放在多个图层里最上层向量数最少、节点之间的距离很远越往下层节点越多、联系越密。查询时从上往下走一开始用很粗的粒度跳到目标区域附近然后逐层细化最后在最底层做精细搜索。我用一个类比理解它你要去一个陌生城市找烧烤店不会挨条街找而是先看城市地图——城东美食街附近区域——确定大方向然后进入街区地图确定几条街最后才走进小巷子找店门。每层地图的详细程度不同第一层地图帮你跳过大量无关区域最后一层地图精确定位。HNSW的优点非常突出查询速度快、召回率高、对高维数据友好。缺点是索引构建时消耗的内存很多而且它本质上是图结构很难被分片存储。如果你的数据规模能放进单机内存HNSW几乎是无脑首选。5.2 IVFPQ先聚类再压缩内存友好的老牌方案IVFInverted File倒排文件的思路完全不同先对数据集做KMeans聚类比如聚成1024个簇每个簇有一个簇中心向量。查询时先算出和查询向量最近的几个簇中心然后只在这些簇里面做暴力搜索。数据量越大剪枝效果越明显。但有个问题如果每个簇里的向量数量还是很多搜索范围依然很大。这时候就引入了PQ乘积量化Product Quantization技术——把一只向量拆成若干子段比如128维拆成8段每段16维每个子段单独做量化压缩。相当于每个向量只存一段残差编码而不是完整的浮点数组。IVFPQ的优点是内存占用非常低官方数据里可以把内存降到原始向量存储的1/10甚至更低特别适合超大规模数据集的场景。缺点是召回率通常比HNSW差一截而且查询速度在低并发下不如HNSW那么惊艳。5.3 选索引类型的落地建议我实践之后的建议是内存够就无脑上HNSW数据太大必须压缩存储才选IVFPQ如果是日志检索这种写多读少的场景可以考虑基于磁盘的扁平索引就是牺牲速度保证写入吞吐。不要盲信某一个参数配置最好用你自己的数据在真机上跑一遍对比。很多向量数据库允许设置两个重要参数efConstruction建索引时动态列表大小和M每个节点的最大连接数。这两个值越大索引质量越高但构建越慢。我在调HNSW时有个经验先用默认参数跑通流程再逐步调高每次只改一个参数最后固定一版合适的配置不要一边调参一边改业务逻辑。6. 生产环境常见的过滤和混合检索问题这一节是实战中经常踩坑的点。很多初学者以为向量数据库只能查相似但实际项目中几乎都要求在某个范围内查相似也就是带条件的相似度检索。6.1 标量过滤先滤后查还是先查后滤场景商品库里有100万条商品向量用户要求只查价格在100到200元之间、品牌为A或B的相似商品。这就是常说的混合检索Hybrid Search——标量过滤条件 向量相似度条件同时生效。实现方式也完全不同而且性能和效果天差地别。先过滤再检索Filter-then-Search先把满足标量条件的商品选出来形成一个子集然后在这个子集里做向量检索。优势是结果精确问题是子集范围越小越能体会到速度优势但如果标量过滤条件很宽泛子集仍然巨大索引发挥不了作用。先检索再过滤Search-then-Filter先用向量索引取回Top1000候选再做标量过滤。优点是快因为向量检索本身只做了一个粗召回缺点是如果过滤条件很严格Top1000里可能只有几条符合条件结果会不够用。我在项目里加大过候选集数量才把召回率拉回来。现在很多主流向量数据库支持在索引级别配置过滤条件让过滤动作在索引搜索过程中完成而不是事后补做。这类优化用好了效果极佳但要注意不同向量数据库对过滤字段类型、过滤操作符的支持程度不同。6.2 一个业务上的建议过滤条件尽量前置即便数据库支持了索引内过滤我也建议在进入向量库之前先做一轮粗过滤。比如上面那个商品场景如果价格、品牌这些信息已经存在业务系统里可以先在业务数据库里查出符合条件的商品ID列表再把这些ID集合传给向量数据库作为过滤条件。这样能大幅减少向量库侧的无效计算。另外注意向量数据库里的标量字段默认可能没有建索引每次都走全量扫描。如果字段能提前定义为带索引的类型过滤性能会好很多。6.3 阈值设置和召回率关系相似度检索一定会涉及阈值问题到底返回几个结果是固定TopK还是按距离阈值截断我用下来的体会是不要过于相信某个固定的距离阈值因为不同embedding模型给出的向量分布差异很大。同一个余弦相似度0.8在某个模型里可能是非常好的结果在另一个模型里可能是平均水平。我做过一个实验同一批文本用两个不同的embedding模型生成向量然后分别算组内相似度分布。模型1的相似度中位数在0.7左右模型2的中位数只有0.5左右。如果你统一用0.7当阈值模型2的结果会被大量过滤掉。所以每个项目都应该先用真实数据统计相似度分布再来定阈值而不是拍脑袋选一个数字。7. 向量数据库的选型和性能实测记录7.1 市面上的几种方案分类如果打开搜索引擎搜向量数据库会看到一大堆产品新手很容易迷路。我把它们大致分成三大类仅代表个人项目经验类别代表思路适合场景注意事项基于库的轻量方案在传统数据库中加装向量索引插件或用独立工具库数据量百万内、单机部署功能简单但胜在轻量适合学习和原型验证独立向量数据库专为向量检索设计的分布式系统提供SDK和完整接口大规模生产环境需要在线扩容、高可用功能全面但部署运维成本高信创/内部自研某些企业内部基于开源框架二次开发对数据安全要求极高必须私有化需要大量底层优化建议有足够人力再搞我的个人建议是先别急着上重型方案。如果你的数据量在百万以内、查询并发不高用轻量方案完全够用等真要大规模上线再迁移到独立向量数据库也不迟因为核心API逻辑基本是相通的迁移成本可控。7.2 一个完整的上手实例下面我用一个具体的例子说明向量数据库使用流程。假设我们要做一套文章相似推荐的Demo数据是5万篇新闻文本。第一步准备文本并生成向量。这一步通常用某个开源embedding模型完成代码类似from sentence_transformers import SentenceTransformer model SentenceTransformer(some-embedding-model-name) documents [北京时间3月2日某地发生地震..., 苹果发布新款手机..., ...] embeddings model.encode(documents) print(embeddings.shape) # (50000, 768) 之类的输出每个文档变成了一个768维的浮点向量。这里一定要记住上线环境使用的embedding模型必须和索引时的模型一致否则向量空间完全错位相似度检索结果会变得毫无意义。第二步把向量写入数据库。以某独立向量数据库的Python SDK为例为了防止暴露具体产品名这里做了脱敏流程是先建立连接创建集合定义向量维度然后批量插入from some_vector_db import Client client Client(hostlocalhost, port19530) client.create_collection(news_articles, dimension768) # 批量插入向量和对应的元数据文章ID、标题、发布时间等 client.insert(news_articles, { vector: embeddings.tolist(), id: list(range(50000)), title: titles, publish_time: publish_times })注意两点一是向量数据的类型必须与集合定义时的类型完全一致二是建议一次性批量插入而不是逐条插入否则写入性能会差很多。第三步执行相似检索query_vector model.encode([广州今天天气怎么样]) results client.search(news_articles, query_vector, top_k10) for item in results: print(item.id, item.title, item.score)这里返回的就是距离最近的十篇文章score就是相似度分数。这个流程如果用普通数据库实现通常只能靠遍历全表而向量数据库在这5万条数据上单条查询延迟通常在毫秒级。7.3 我做的几轮性能对照我在一台普通的开发机上跑过一组测试数据量从1万到20万条主要观察两个指标写入吞吐量和查询P95延迟。结果符合预期写入吞吐主要受批次大小影响。批次从1条提升到500条时吞吐提升了接近40倍再往上提升批次大小收益递减而且对内存的占用显著增加。查询延迟方面数据量在5万条以内HNSW索引和暴力搜索差距还不算明显基本都在几毫秒到十几毫秒之间。但数据量超过10万后暴力搜索延迟骤增而HNSW依然稳定在毫秒级。这个结果验证了前面说的向量数据库在大数据量下才体现出真正的优势。另外要提一下性能测试不能只看均值P99和P95才是真实用户体验的关键。我在测试时发现索引刚构建完成后前几十个查询延迟会偏高因为涉及内存预热和缓存。这种冷启动效应在上线前要提前跑一轮预热脚本。8. 知识库/大模型场景下的一个特殊问题增量更新和传统数据库不同向量数据库的索引技术尤其是HNSW对增量写入并不友好。很多人第一次做知识库应用时以为像MySQL那样一行一行往里插就行了结果发现查询越来越慢、召回率下降、甚至出现新写入的数据查不到的情况。8.1 为什么增量插入会变慢以HNSW为例它本质上是一个图结构。插入新节点时需要从顶层开始搜索合适的位置然后逐步更新相邻节点的连接关系。如果并发插入太多图结构频繁变动一方面构建开销直线上升另一方面检索路径可能被破坏出现明明在附近却搜不到的情况。很多向量数据库的推荐做法是分段合并先把小批量数据写入一个临时小索引达到一定规模后和主索引合并。在使用中我养成的一个习惯是批量写入隔一段时间合一次而不是边来边插。如果是实时性要求很高的场景比如新文章必须立刻能被搜到通常会把新数据单独建一个小集合查询时同时查多个集合并合并结果。虽然麻烦但能保住性能。8.2 删除和更新同样麻烦向量数据库的删除操作也不是物理删除而是打标记索引结构本身不会立刻变化。更新的本质也是插入新版本标记旧版本。所以频繁更新数据时索引的尸体会越堆越多索引效率逐渐下降。这时候需要定期重建索引或者做Compaction操作部分数据库内部会做部分需要手动触发。我在一个社区项目里就遇到过每天增量写入几万条连续跑了三周查询质量肉眼可见地下降。后来加了一次全量重建效果立刻回来。所以如果你的数据有大量更新和删除一定要规划好索引重建周期别等线上出问题再救火。8.3 把索引看成能持久化的缓存而不是表这是我学习向量数据库半程时的一个顿悟传统数据库里表是事实的唯一来源索引只是加速工具但向量数据库的设计逻辑更接近索引即数据没有索引你的向量就只是一堆数组只能暴力扫描。所以索引的构建方式、更新策略、存储占用应该是整个系统设计的一等公民而不是事后叠加的优化手段。这个认知转变是从一次数据损坏恢复的教训来的某次索引文件损坏直接导致全库查询失败但因为原始向量还在我重新构建索引后恢复如初。后来我给自己定了一条运维红线原始向量数据必须备份单独一份索引挂了可以重建数据丢了就彻底完蛋。9. 几个非常实用但常被忽略的细节9.1 查询超时需要设置向量数据库在数据量极大时单次查询可能超时。如果你的服务依赖方对延迟有硬性要求一定要在客户端设置超时时间。我见过很多团队配置了索引却忘了配置超时导致数据库卡在索引扫描阶段调用方全部阻塞最终拖垮整个服务。9.2 确认向量数据库的内存模型不同向量数据库对内存的消耗差别很大。有的索引结构常驻内存查询快但成本高有的索引默认落盘查询时再加载性能相对差一些。上线前建议先压测摸清每百万条向量大概占多少内存再按容量规划服务器。千万别等到一半数据放进去了才发现内存不够那时候迁移成本已经很高。9.3 写入和查询的隔离高并发场景下大批量写入和查询同时进行可能会互相争抢CPU和IO资源。我在做某推荐系统的压测时就发现后台在跑全量索引重建时线上查询延迟从5毫秒飙到了500毫秒。后来我们在架构上做了隔离写入和查询走独立的节点或者至少独立的线程池才把问题解决。如果你的数据有定期更新任务这一点一定要提前设计好。9.4 我踩过的一个坑embedding模型版本管理最后说一个最隐蔽的问题embedding模型的版本管理。很多团队上线时没有把embedding模型锁定版本后来某天有人顺手升级了模型依赖包结果新生成的向量和老向量不在同一个语义空间里整个检索系统失灵。正确的做法是把embedding模型的相关元信息名称、版本号、输入格式、维度大小作为一个字段存在向量表里。每次查询时校验版本一致性模型升级时宁可做一次全量数据重新向量化也不要让新旧向量混在同一个集合里。这个坑我每次和团队交流都会强调一遍向量数据库本身只是个尺子embedding模型才是决定量什么的关键。尺子换了刻度的标准量出来的结果自然不可信。回顾这两个月的学习和踩坑过程我认为向量数据库的学习路径可以按这个顺序走先理解embedding是什么再熟悉距离计算接着对比几个索引算法最后动手跑一个Demo并观察性能变化。不要一上来就陷入参数调优的细节先把主流程跑通遇到问题再针对性深挖效率会高很多。