尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从Embedding到向量数据库:ANN与HNSW原理及实战指南

从Embedding到向量数据库:ANN与HNSW原理及实战指南 1. 起步之前为什么我会写这样一篇入门文章先交代一下背景。我最早接触向量数据库是在做大模型问答系统的时候当时最头疼的问题不是模型效果而是“怎么从几千上万条知识里快速找出和用户问题最相关的那几段”。最开始用传统数据库的 LIKE 查询后来试过 Elasticsearch 的关键词匹配效果都差强人意。直到有天同事甩给我一个词Embedding。后来我又在项目里逐步研究 ANN、HNSW、IVF 这些概念才真正把整套链路跑通。这篇内容我打算写得非常直白不讲虚的。核心主线就一条从文本变成向量的过程也就是 Embedding再到向量数据库如何用近似最近邻检索ANN快速找相似向量。中间会把原理、实操选型、常见坑全部拆开说清楚。适合谁看如果你正在做大模型知识库、推荐系统、图片搜索、去重系统或者只是听到“向量数据库”这个概念但不知道它到底解决什么问题那这篇文章可以帮你省不少摸索时间。我默认你有一点 Python 和数据库基础但即使没有概念部分也能看懂。2. 从“文本”到“向量”Embedding 到底是什么2.1 一个简单的类比把语言变成坐标我第一次理解 Embedding 是靠一个类比想象你把一个人放到一张地图上地图上的每个点代表他“整体的状态”比如爱好、职业、性格。如果两个人的点离得近说明他们相似离得远说明差异大。Embedding 做的事情就是把一段文本、一张图片甚至一条用户行为序列映射到这样一个“多维地图”上只不过这个地图不是二维的而是几百上千维。常规说法叫“语义向量化”。比如“猫”和“小狗”这两个词的向量距离会非常近而“猫”和“汽车”的距离会很远。更关键的是向量不仅能表达“词义相近”还能表达“语义关系”。比如经典的“国王 - 男人 女人 ≈ 女王”这类操作虽然实际工程中不一定都理想但至少说明向量空间里保留了语义结构和关系。2.2 常见 Embedding 模型有哪些很多人一听“Embedding 模型”就觉得高深实际上可以理解为一个“文本到向量的转换器”。输入是一句话输出是一串浮点数。不同模型输出的向量维度不一样语义表达能力也不一样。我自己用过的几类模型大概包括OpenAI 系列text-embedding-ada-002、text-embedding-3-small、text-embedding-3-large。优点是效果稳定使用简单直接调 API 就行。缺点是数据要出网对数据敏感场景不友好而且有成本。开源中文模型BGE 系列如 bge-large-zh、bge-m3、M3E、text2vec 等。这些在中文语义匹配上表现不错可以本地部署特别适合私有化项目。多模态模型CLIP 可以把图片和文本映射到同一个向量空间这样就能用文字搜图片或者用图片找相似图片。LLM 的隐式向量有些方案直接用大模型的中间层输出作为向量比如近年讨论较多的 Qwen3-VL 这类多模态模型中的 embedding 结构主要用在跨模态理解任务上门槛偏高普通业务场景暂时不太需要。选择模型时我一般会看三个指标向量维度、语义效果、推理速度和成本。维度不是越高越好维度太高存储和计算成本都会增加效果上必须拿自己的业务数据测网上榜单只能作为参考。2.3 Embedding 模型的“微调”什么时候需要做有人问过我“Embedding 模型要不要微调”我的答案通常是先别急着微调先用通用模型跑一遍基线。如果发现某些专业词汇、特殊语境下的相似度排序明显不对再考虑用 LoRA 这类轻量方案微调 Embedding 模型。LoRALow-Rank Adaptation是现在比较主流的微调方式核心思想是冻结原模型的大部分参数只训练一小部分低秩矩阵。好处很明显显存占用小训练速度快几十分钟到几小时就能跑完一个中小规模数据集。我曾经在某个法律文书场景中用几千条标注好的“相似问题对”对 bge-large-zh 做 LoRA 微调效果提升确实比通用模型明显尤其是对法条简称、案由表述这类领域文本。但有一点要提醒微调不是万能的。如果你的数据量很小比如几百条微调很容易过拟合如果你用的模型本身就是大型多模态模型微调成本更高需要谨慎评估收益。2.4 从文本到向量的实操流程在实际项目中把一个文本库变成向量集合大致流程如下准备文本数据做清洗去掉无关字符、统一格式、处理空值。设置切分策略长文档需要切分成 chunk切分大小和重叠率直接影响检索质量后面我会专门讲。批量调用 Embedding 模型生成向量。把向量连同原始文本、元数据一起写入向量数据库。查询时把用户问题转成向量再执行 ANN 搜索拿到最相似的若干条。这里要特别注意第 2 步和第 3 步。很多人一开始图省事直接把整篇长文档塞给模型生成一个向量结果检索时命中率很低。原因很简单一个上千字的文档里包含多个主题压缩成一个向量之后很多细节就被“平均”掉了。所以切分策略很关键。我常用的切分方式有两种一是按固定长度切分带一定 overlap二是按段落或语义结构切分。固定长度简单稳定适合大多数场景语义切分效果好但依赖额外模型成本更高。常规项目里我会优先用固定长度加 overlap 的方式切分长度视文档语言和模型能力而定中文常见 200 到 500 字左右。3. 向量数据库它和普通数据库到底差在哪3.1 为什么不能用传统数据库存向量先说结论不是完全不能而是“查询效率”跟不上。传统关系型数据库比如 MySQL存向量本身没问题一个 BLOB 或者 JSON 字段就能塞进去。但问题是当你要找“与某个向量最相似的 10 个向量”时它只能把所有向量取出来逐一计算距离这就是暴力搜索。数据量小还好几千条可以接受但到了几十万、几百万条每次查询都全表扫描延迟会爆炸。向量数据库的核心价值就是通过专门的索引结构和算法把“精确但慢”的暴力搜索变成“近似但快”的近似最近邻检索。这里的“近似”意味着不保证每次都返回全局最优结果但会尽量接近最优同时在召回率和查询速度之间做平衡。3.2 向量数据库的典型架构一个向量数据库通常包含这些部分向量索引引擎负责构建和更新索引是性能核心。存储引擎负责持久化向量数据和元数据。查询引擎负责解析查询请求、调用索引、返回结果。标量过滤配合 SQL 或 API支持按标签、时间、状态等标量字段过滤后再检索向量。分片与高可用大规模场景下支持将数据分布到多节点保证扩展性和可用性。这些能力组合起来让向量数据库可以当作一个完整的检索服务来用而不只是存储工具。3.3 主流向量数据库选型对比现在市面上可选的向量数据库非常多兆维一下常见的几类Milvus开源分布式向量数据库支持大规模数据、丰富的索引类型、完整的 API 和生态适合生产环境尤其在推荐、搜索场景下用得多。缺点是组件多部署和运维复杂度偏高。QdrantRust 编写性能好部署相对简单而且提供了比较友好的 Python 客户端和 Web UI。我近期很多项目直接用 Qdrant几个命令就能在本地跑起来配合 Docker 很顺手。Redis 向量检索如果你已经有 Redis可以通过 Redis Stack 的向量集合能力做简单 ANN 搜索适合轻量级或缓存型场景。它不算全功能向量数据库但胜在简单。Elasticsearch传统搜索引擎也加入了向量检索能力比如 dense_vector 字段适合已有 ES 体系、希望统一关键词与向量搜索的场景。Chroma、Weaviate、Pinecone各有特点。Chroma 简单轻量适合原型Pinecone 是托管服务省心但数据要上云Weaviate 在语义搜索和插件生态上有自己的优势。选型没有标准答案要结合数据规模、部署环境、团队技术栈、成本预算综合判断。我个人的经验是原型验证用 Chroma 或 Qdrant 起步生产环境如果数据量大、并发高优先考虑 Milvus如果想省运维就选云托管服务。3.4 Qdrant 的下载安装与初步体验因为 Qdrant 上手简单我以它为例子做个实操演示。最方便的方式是 Docker 启动docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant启动之后控制台会监听 6333 端口RESTful API和 6334 端口gRPC。如果你不习惯命令行Qdrant 还提供了 Web UI访问http://localhost:6333/dashboard就能看到。之后用 Python 客户端连接pip install qdrant-client创建一个 collection 并插入向量的示例from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(hostlocalhost, port6333) client.recreate_collection( collection_namedemo, vectors_configVectorParams(size768, distanceDistance.COSINE), ) points [ PointStruct(id1, vector[0.1, 0.2, ...], payload{text: 苹果}), PointStruct(id2, vector[0.3, 0.1, ...], payload{text: 香蕉}), ] client.upsert(collection_namedemo, pointspoints)这里的size必须和 Embedding 模型输出的维度一致比如 bge-large-zh 输出 1024 维你的 size 就要填 1024。distance 通常用 Cosine 或 Dot Product下面会讲。4. ANN 检索核心从暴力搜索到近似最近邻4.1 距离度量怎么选在做 ANN 之前先明确向量之间的“相似度”怎么算。常见有三种欧氏距离L2衡量空间中的直线距离。值越小表示越相似。适合向量分布比较均匀、绝对距离有意义的场景。余弦相似度Cosine衡量两个向量的方向一致性不关心向量长度。值越接近 1 表示越相似。文本 Embedding 最常用这种。点积Dot Product相当于余弦相似度还没归一化。如果模型输出已经做了归一化点积和余弦结果等价而且计算更快。我一般做文本检索直接用 Cosine如果预训练模型本身默认使用点积优化或者向量已经归一化就用 Dot。具体用哪个强烈建议在测试集上分别跑一下看最终召回效果而不是凭直觉拍板。4.2 暴力搜索的局限暴力搜索Brute Force很好理解把库里每个向量和目标向量算一遍距离排个序取前 K 个。这个做法在数据量小的时候完全可行准确率 100%。但时间复杂度是 O(N)N 是向量总数。向量维度越高每次距离计算的开销也越大所以当 N 达到百万级一次查询可能就要几十毫秒甚至更久并发上来就扛不住。这就是 ANN 存在的意义牺牲一点点召回率换来数量级的速度提升。4.3 主流 ANN 算法与索引原理ANN 算法有很多种但主流思路可以概括为几类基于树的索引如 KD-Tree、 Annoy把向量空间不断划分成子空间查询时只在部分子树里搜索。树结构简单构建快但不适合超高维数据维度一高容易退化。基于哈希的索引如 LSH局部敏感哈希设计一组哈希函数让相似的向量以较大概率被分到同一个桶里。查询时只需要在少数几个桶里搜索。LSH 理论优雅但实际效果依赖哈希函数的参数调节通常需要多个表来保证召回。基于量化的索引如 PQ乘积量化把高维向量切分成若干子向量对每个子向量分别聚类用聚类中心的 ID 去近似原向量。这样可以大幅压缩存储空间查询时用查表方式快速估算距离。IVF倒排文件常和 PQ 结合使用先通过聚类缩小搜索范围再在候选集里做精确计算。基于图的索引如 HNSWHNSW 是目前最流行的 ANN 索引之一。它构建一个多层图上层图负责“快速跳跃”底层图负责“精细搜索”。查询时从顶层开始逐层向下最终在底层找到最近邻。HNSW 的召回率高、查询快但内存占用较大索引构建时间也比其他方法长一些。4.4 走近 HNSW为什么它效果好HNSW 全称是 Hierarchical Navigable Small World层次化可导航小世界图。名字听起来拗口其实思想很简单类似“六度分隔”理论社交网络里任意两个人通过少量中间人就能建立联系。HNSW 就是让向量空间里的节点通过“短途连接”实现高效导航。具体来说图中有多层结构上层节点少连接稀疏负责快速接近目标区域下层节点多连接密集负责精细定位。搜索时从上往下每层都用贪心策略找局部最近点然后跳到下一层继续找直到底层。HNSW 有三个重要参数M每个节点的最大连接数。M 越大图连接越密召回越高但内存和构建时间也增加。efConstruction构建索引时动态候选列表大小。越大索引质量越好但构建越慢。efSearch查询时动态候选列表大小。越大召回越高但查询越慢。在实际工程里调参目标就是找到“速度、内存、召回”三者都可接受的平衡点。比如我用 Qdrant 的 HNSW 时M 默认 16efConstruction 默认 100查询时 ef_search 设 64 到 128具体看业务容忍的延迟和召回要求。4.5 ANN 的度量指标不少人在做完 ANN 后只问“快不快”很少问“准不准”。其实业界有一个核心指标叫RecallK含义是用 ANN 取回的结果中有多少比例是暴力搜索的 Top-K 结果。比如暴力搜索 Top-10 结果是 A、B、C……ANN 返回的结果里包含 A、B、C 中的 8 个那么 Recall10 就是 80%。一般生产环境要求 Recall10 在 95% 以上才比较放心。如果低于 90%说明索引参数或者索引类型选得不够合适。5. 完整项目实操搭建一个本地知识库问答的向量检索链路说了这么多概念下面我用一个具体场景把它们串起来做一个简单的本地知识库问答系统。用户上传几篇文档系统切分、Embedding、写入向量数据库用户提问时系统先从知识库里检索最相关的片段再交给大模型生成回答。这一步不依赖外部 API数据不出本地适合对敏感信息有要求的场景。5.1 环境准备我用到的组件如下Python 3.9 以上QdrantDocker 运行sentence-transformers 库用于加载 Embedding 模型一个开源 Embedding 模型比如BAAI/bge-large-zh-v1.5可选的 LLM 生成模型比如本地部署的 Qwen、ChatGLM 系列生成环节可选也可以先用检索结果做展示安装 Python 依赖pip install sentence-transformers qdrant-client我的建议是先把检索链路跑通生成回答的环节再单独接大模型否则调试时问题太多不好定位。5.2 文档切分与向量化切分我用固定长度加 overlap 的方式做了一个轻量函数def split_text(text, chunk_size300, overlap50): chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks这段代码会按 300 个字符切分每两个相邻 chunk 有 50 个字符的重叠。重叠的目的就是防止句子被生生切断在边界上造成语义残缺。然后加载模型生成向量from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-large-zh-v1.5) def embedding_texts(texts): return model.encode(texts, normalize_embeddingsTrue)这里normalize_embeddingsTrue很关键它能保证后续用点积和余弦相似度等价还能稍微提升检索效果。5.3 写入 Qdrant把切分好的文本和向量一起写入 Qdrantfrom qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client QdrantClient(hostlocalhost, port6333) collection_name knowledge_base client.recreate_collection( collection_namecollection_name, vectors_configVectorParams(size1024, distanceDistance.COSINE), ) points [] for idx, chunk in enumerate(chunks): vec embedding_texts([chunk])[0] points.append( PointStruct( ididx, vectorvec.tolist(), payload{text: chunk, source: doc1.txt} ) ) client.upsert(collection_namecollection_name, pointspoints)这里size1024是因为我选的模型输出 1024 维。你换成其他模型维度可能不同一定要确认。5.4 检索并输出查询时把用户问题同样转成向量然后调query_pointsquery 这份文档里提到了哪些注意事项 query_vec embedding_texts([query])[0] result client.query_points( collection_namecollection_name, queryquery_vec, limit5, search_params{hnsw_ef: 64, exact: False} )返回结果里带 payload可以直接把 text 字段提取出来拼成上下文喂给 LLM。如果不接 LLM也至少能拿到“最相关的5段原文”这一步对调试 Embedding 模型效果特别有用。5.5 实测结果分析与参数调整跑通第一版之后我通常会做几个小实验用不同的 query 测试看返回的片段是不是真的相关。如果不相关先检查切分长度再换 Embedding 模型试效果。调整hnsw_ef从 32 到 128看召回变化。数据量小时差异不明显数据量大时效果就拉开了。对比exactTrue和exactFalse的结果。如果 ANN 结果和精确结果差异很大说明索引参数太激进需要增大hnsw_ef。这种“先精确后近似”的对比习惯我推荐大家保留下来它是判断索引质量最直接的手段。6. 检索链路里的常见问题和排查技巧6.1 切分不合理导致上下文语义断裂这是知识库问答里最容易踩的坑。固定长度切分虽然简单但很可能把一个完整句子拦腰切断检索到的片段读起来前后不通大模型拿它生成答案自然效果差。我后来的做法是先按段落粗分段落太长再用固定长度切切分边界尽量保持在换行符或标点之后。如果预算允许也可以用专门的语义切分模型。还有一个技巧切分时保留文档标题作为 chunk 的前缀比如“第一章 总则”这样每个片段都自带上下文信息检索效果会明显更好。6.2 维度不匹配或向量未归一化很多人第一次写代码时会报错Vector dimension mismatch。原因很简单collection 的size设置和模型输出维度不一致或者模型换过但 collection 没重建。解决办法固定模型记录维度模型变更时重建 collection。还有一类坑是漏了normalize_embeddingsTrue。向量未归一化时用 Cosine 距离虽然理论上不受长度影响但有些数据库底层实现为了性能可能先做点积未归一化会产生偏差。所以我的习惯是无论用什么模型都先归一化省掉后患。6.3 ANN 召回率不达标怎么调如果你发现 ANN 召回率偏低按以下顺序排查先调ef_search这个参数提升空间最直接通常从 64 调到 256召回能明显涨。再调MM 越大连接越密但内存占用增加索引时间也变长。确认索引类型是否合适。比如数据量特别小几百条以内用精确搜索可能比 ANN 更省心数据量达百万HNSW 一般够用如果维度极高且存储压力大考虑 IVFPQ。检查 Embedding 模型本身是否足够好。通用模型在专业领域效果不佳时调索引参数只是杯水车薪该微调就微调。6.4 脏数据造成的检索偏差一个容易被忽略的问题是文本清洗。文档里经常有页眉页脚、多余换行、乱码字符、表格残缺内容。这些脏数据切分后产生大量无意义 chunk检索时可能突然冒出来拉低整体效果。我习惯在切分前做一遍规则清洗去除连续空白字符、特殊符号。去掉以页码、日期开头的行。过滤明显过短的 chunk比如少于 20 字。表格尽量转为 Markdown 格式保持行列关系可读。清洗做得好检索效果提升立竿见影而且能减少无用向量占据存储空间。6.5 问题速查表现象可能原因排查方向检索结果完全不相关Embedding 模型不适合领域数据换行业语料微调或换更强模型召回率低HNSW 参数过小调大 hnsw_ef再调大 M报向量维度错误索引 size 与模型输出不一致重建 collection 并匹配维度查询延迟高数据量大且索引参数过大降 ef_search或改用量化类索引结果老带无用片段切分不干净或 chunk 太短优化切分策略过滤短文本相似度分数整体偏高未归一化导致余弦计算异常模型输出时开启 normalize文档新增后查不到未执行 upsert 或索引未刷新检查写入结果确认 collection 正确这里的每一个问题我都实际碰到过尤其是“未归一化导致相似度分数偏高”这个问题乍一看分数很高实际排序乱套排查了很久才发现是归一化的锅。7. 粗浅实践之外一些进阶方向如果你已经完整跑通了入门链路还想再深入可以考虑下面几个方向。第一混合检索在纯向量检索之外配合关键词检索BM25做结果融合。向量擅长语义相似关键词擅长精确匹配两者互补在很多场景能有效提升召回质量尤其是技术文档、法律条文这类含大量专有名词的内容。第二多模态向量检索CLIP 让文本和图片进入同一个向量空间你可以实现“文字搜图”“以图搜图”。再往下扩展视频片段、音频也可以抽象成向量做成跨模态知识库。第三微调 Embedding 模型并上线LoRA 微调只是第一步真正上线时还要考虑量化、推理服务化、热更新模型等问题。届时你可能需要引入模型服务框架把 Embedding 推理做成独立服务。第四列式存储与资源估算向量占用内存比想象中更快。一个 1024 维向量用 float32 存储约 4KB100 万条就是约 4GB 内存加上 HNSW 图的额外索引开销实际内存可能是原始向量的好几倍。做容量规划时一定先算账避免内存爆掉。8. 最后分享几点实操心得写到最后说几个我自己的真实体会。向量数据库和 Embedding 都不是多高深的东西难的是把每个环节的细节做扎实。数据清洗、切分策略、模型选型、索引参数、召回评估任何一个环节草率最终检索质量都会打折。很多项目最后效果不好并不是数据库或算法的问题而是最前面的数据处理没做好。我踩过一次很深的坑项目刚开始时为了图快直接用 OpenAI 接口生成所有向量一轮跑下来花了不小费用后面换成本地模型时才发现两套向量并不能直接混在同一个 collection 里所有数据都得重新生成。所以我的建议是项目前期就要定好 Embedding 模型中途不要随便换如果一定要换就要接受重新向量化的成本。另外别把向量检索当成万能药。对于有些需求比如精确 ID 查找、简单关键词过滤、数值范围筛选传统数据库反而更合适。合理的架构通常是传统数据库、搜索引擎、向量数据库各司其职组合使用而不是一套方案通吃所有查询。最后再分享一个小技巧调试阶段不要一上来就配置超大索引参数先在几千条数据上跑通全流程确认检索效果能达到预期再放大数据规模调优参数。这样既节省时间也更容易定位问题。如果你正准备做自己的知识库或语义搜索项目希望这篇文章能帮你少走几步弯路。后续我还会写关于 HNSW 参数调优细节、混合检索实现、以及 Embedding 模型微调的相关内容欢迎交流。
返回列表