资讯动态

Elasticsearch向量相似性搜索实战:从索引设计到混合查询

发布时间:2026/10/13 1:15:31 来源:尧图企业网站定制
你是不是也遇到过这种场景数据里明明就有答案但用一个“关键词”搜出来的结果就是不对。用户问的是“怎样卸载老旧的杀毒软件”你文档里写的是“移除已停止维护的安全程序”传统搜索引擎根本不会把这两句话关联起来。后来我试着在Elasticsearch里做向量检索把这些内容映射到向量空间里再用量距离衡量相似度结果直接改观了。这篇文章就是我完整做过一次“Elasticsearch向量相似性搜索”的实战记录从向量怎么生成、索引怎么设计到混合查询怎么调、生产环境怎么估算容量都会讲。适合想给现有搜索服务加上语义能力、又不希望额外引入一套专用向量数据库的团队参考。1. 为什么在搜索引擎里塞进向量空间——倒排索引到余弦距离的转变很多人第一次听说Elasticsearch能做向量检索时第一反应是“不就用BM25吗”其实这两个本质不同。传统倒排索引维护的是一个从词到文档的映射它擅长布尔匹配和词频统计但对“意思相近但字不同”的表达无能为力。向量检索则把文档和查询同时映射到同一个高维坐标空间每个文档是一个点每个查询也是一个点然后通过余弦相似度或欧氏距离找出离它最近的K个点。这个思路让你能搜到“语义上有关联”但不含任何相同关键词的内容。1.1 从“含词”到“含义”搜索引擎的思维切换我更喜欢把倒排索引理解成字典查字你想找“苹果”字典里能帮你定位到包含“苹果”这两个字的段落。而向量空间像一张地图Apple、苹果、iPhone、库克这些信息虽然字面上千差万别但在地图上会被自动聚类到“水果”或“科技公司”附近。你给一个坐标点地图就把周围最近的点都拿给你这就是相似性搜索。这个变换对很多业务是颠覆性的。我在一个客服知识库里试过用户问“我的订单怎么还没到”系统用关键词只能匹配到包含“订单”“未签收”的条目但用向量检索能同时召回“物流延误”“配送异常”“查件流程”这几类答案覆盖率明显提升。所以向量搜索解决的不是“搜索快”的问题而是“搜得准”的问题。1.2 向量化的本质把内容变成坐标点不管输入是文本、图片还是音频向量化就是用一个模型把它压缩成一串固定长度的浮点数比如256维或768维。这串数字在空间里就对应一个坐标。衡量两个内容是否相似最常用的是余弦相似度夹角越小说明方向越一致内容越接近欧氏距离则更关注绝对距离对向量长度变化更敏感。比如“今天天气真好”和“天气不错”两句话经过同一个模型后会生成两个向量它们之间的夹角通常很小。而“我昨晚失眠”和“这家店的拉面好吃”就会相距很远。这个“距离”就是你可以直接用来排序的分数。明白了这个原理后面所有参数都不是拍脑袋设置的。1.3 为什么不用专用向量数据库而选Elasticsearch市面上确实有专门为向量而生的数据库但我选择在Elasticsearch里做有三个现实理由第一绝大多数团队已经有一套ES集群运维经验成熟再引入新组件会成倍增加学习成本。第二ES允许我同时保留传统倒排索引和向量字段在同一个查询里做“关键词召回向量召回”混合排序这是专用向量库很难顺手做到的事。第三业务里经常要按品牌、品类、时间过滤然后再做相似性检索ES的过滤器体系可以和KNN查询无缝叠加。当然ES的向量性能在极端十亿级规模下未必比得上专用引擎但对多数百万级、千万级体量的业务场景它足够稳也足够好用。2. 数据准备如何把内容变成高质量的向量向量检索的效果七成取决于向量本身的质量。这一步的核心是从“文本/图片/音频”到“干净、归一化的向量数组”。我建议把这个过程当成独立的ETL管道来做不要每次查询时现算向量那样延迟和成本都受不了。2.1 选向量模型不要盲目追求大维度刚开始做时我犯过一个错谁的模型维度高就选谁觉得维度高信息就全。实际上维度和领域匹配度、推理成本都要一起看。以文本为例我通常按这三步选模型先看语言覆盖如果内容是中英混合就选跨语言模型如果只有中文一个针对中文优化的轻量模型可能比通用大模型更稳。再看领域适配法律、医疗这类专业场景建议用领域微调过的模型通用场景用社区常用句向量模型即可。最后算推理成本768维的模型是256维内存和计算开销的三倍但效果未必提升三成。业务体量小的时候256维往往已经足够把相似文档分得很开。我做过一次对比测试同样是商品描述用256维模型跑出来的Top5召回准确率能达到87%768维模型只提了约3个百分点但索引体积几乎翻了3倍。最后线上选的是更轻的版本因为查询延迟对UI体验影响更大。这个取舍没有标准答案建议你在自己的数据上跑一轮批量离线评测再决定。2.2 一张表看清向量方案的选择维度下面这张表是我在选型时常用的横向对比思路重点是关注“生成成本”和“查询成本”的平衡点。字段列不是真实产品而是三类方案的通用画像方案类型典型维度生成速度内存占用适合场景轻量通用模型128-256快CPU可跑低分类标签、商品名、短文本召回中量跨语言模型512-768中建议GPU批量中多语言文档、用户评论语义匹配重量级全模态模型1024慢需要加速卡高图片文本音频统一检索规模较大实践里真正影响交付的往往是“生成向量的速度”而不是模型理论效果。如果存量数据有上千万条模型每秒只能处理十条那要跑好几天上线节奏会被拖垮。所以我会在选型时先拿一万条样本测吞吐再决定要不要上GPU加速。2.3 实操批量生成向量并组装成文档这里给一个可参考的伪代码式流程核心动作是“加载模型 - 批量编码 - L2归一化 - 写入ES”。具体模型接口不同但套路一致# 伪代码基于某个已经训练好的向量编码器 from your_embedding_module import get_encoder encoder get_encoder( model_nameyour_chosen_model, batch_size64, devicecuda # 数据量大时建议用GPU ) docs [ {id: 1, title: 无线鼠标连接指南, body: 插上USB接收器后打开开关...}, {id: 2, title: 键盘功能键说明, body: Fn组合键用于调节亮度...}, ] records [] for doc in docs: text doc[title] \n doc[body] vec encoder.encode(text) vec vec / (vec ** 2).sum() ** 0.5 # L2归一化非常重要 records.append({ id: doc[id], title: doc[title], body: doc[body], title_vector: vec.tolist(), body_vector: vec.tolist() }) # 后续用bulk接口写入ES为什么非要归一化因为很多库在计算余弦相似度时会默认先归一化但Lucene底层实现里dense_vector的评分逻辑各不相同。如果你希望“余弦相似度”真正只关注方向、忽略向量长度就把向量预归一化。等后面讲到得分坑时你会回来感谢这一步。3. 索引设计mapping、度量函数与HNSW参数向量索引设计比普通分词索引更讲究因为dense_vector字段不仅要存原始值还要为每个向量建一个近似最近邻图结构。这个结构决定了写入成本和查询精度之间的平衡点。我在第一次建索引时什么参数都没调直接上了默认值结果查询召回率不理想后来才一点点摸索出来。3.1 dense_vector类型与相似度函数怎么选从Elasticsearch 7.x开始定义向量字段就是用dense_vector类型。较新版本还支持按维度拆分的量化存储但基础思路没变。声明一个向量字段只需要三个信息维度dims、元素类型element_type和相似度度量similarity。similarity有三个常见选项cosine计算余弦相似度dot_product计算点积l2计算欧氏距离。我的建议是如果向量做过归一化dot_product和cosine结果等价但dot_product计算更快适合大压力场景l2则适合你用距离画聚类边界的场景。需要注意的是如果你的向量不是单位向量又选了dot_product结果会被向量长度干扰导致某些长向量内容实际不相似却得分很高。这时候就必须回到“先归一化再入索引”这个动作上。下面是一个mapping示例我把普通字段和向量字段定义在一起{ mappings: { properties: { id: {type: keyword}, title: {type: text, analyzer: ik_max_word}, body: {type: text, analyzer: ik_max_word}, category: {type: keyword}, creator: {type: keyword}, title_vector: { type: dense_vector, dims: 256, element_type: float, similarity: cosine, index: true, index_options: { type: hnsw, m: 16, ef_construction: 100 } } } } }我特意把索引参数显式写出来了而不是留默认。原因很简单这些参数直接影响写入性能和查询精度显式写出来方便后续调优和跨环境复现。虽然很多教程说默认就够但生产环境我还是建议全部显式声明。3.2 索引参数m、ef_construction与ef_search的配合逻辑HNSW是Elasticsearch底层用的向量索引算法它是一个“跳表图”结构。m代表每个节点最多连接多少个邻居ef_construction代表构建图时考虑多少候选节点ef_search代表查询时扩展搜索多少候选节点。这组参数的核心关系是越大召回率越高但内存和耗时也随之增加。我习惯用“先粗后细”的方式调参m设成16时索引体积适中查询也够快如果召回率不够把m调到32能带来明显提升。构建阶段ef_construction建议从100起步数据分布比较分散时可以开到200但构建时间会成倍增加。查询阶段的ef_search可以按需调整。线上我一般先设100如果P99延迟还扛得住就再往上调。不要一上来就把所有参数拉满。我在一次采集项目里把ef_construction设成了400索引构建时间直接翻了四倍但准确率只提升了一个百分点非常不划算。调参一定要拿真实query集去评测而不是凭感觉。3.3 写入调优不要忽视refresh_interval和segment向量索引构建时会产生大量临时数据。如果你的业务允许文档写入后延迟几秒才可见强烈建议关闭高频刷新把refresh_interval调整到30秒或完全关闭刷新等批量写完后手动触发一次refresh。此外用bulk批量写入比单条逐插快一个数量级以上单批次大小可以从1000条起测。写完大批量文档后记得执行一次force merge把segment数压下去。HNSW图是按segment构建的segment越多查询时跨图的代价越高。我见过一个案例索引有80多个segment查询延迟比合并前高了两倍多合并到1个segment后彻底恢复正常。这条很容易被忽略但收益立竿见影。4. 查询实战从基础KNN走向混合检索索引搭好之后查询才是真正见真章的地方。我建议先跑通纯向量KNN再逐步叠加过滤和文本检索。上来就直接写混合查询出了问题不好定位。4.1 基础KNN查询的三种打开方式Elasticsearch里做向量查询最直接的是knn查询语法。它把K值、目标向量、候选数量一次性传进去底层直接走HNSW图。下面是一个简单查询{ knn: { field: title_vector, query_vector: [0.12, -0.24, 0.33], k: 10, num_candidates: 100 } }这里k是最终返回的最近邻数量num_candidates是探索的候选节点数量。经验上num_candidates应该远大于k比如k10时num_candidates设成100保证召回更准确。如果对延迟不敏感设置到200也没问题。除knn查询外还可以用script_score在已有查询基础上对向量字段打分。比如你先按关键词过滤再对命中的文档算余弦距离适合“先缩小范围再精排”的场景。但这种方式不能走HNSW的高效检索路径性能相对较差不建议在大集合上直接用。4.2 关键词与向量得分的混合RRF是更省心的方案在绝大多数业务里只做向量检索会漏掉那些关键词精确匹配的记录只做关键词搜索又不够智能所以必须混合。我最推荐的是使用RRFReciprocal Rank Fusion方案它不直接累加分数而是基于排名的倒数加权融合好处是不需要手动调权重。下面是一个简化示例{ query: { rrf: { rank_window_size: 200, queries: [ { match: { title: 无线鼠标连接指南 } }, { knn: { field: title_vector, query_vector: [0.12, -0.24, 0.33], k: 50, num_candidates: 100 } } ] } } }RRF把两路结果按名次融合每路结果就像两个评委分别给选手打分最后用名次而不是严重程度决定胜负。它最大的好处是“一刀切”可用不用反复调boost系数。如果两路结果有交叉用RRF天然对排位靠前的文档给更多权重比简单相加更抗噪。4.3 过滤与KNN的正确配合不要让HNSW跑全量业务里通常会有“只在某些分类下搜”的需求。knn查询支持在query参数里加filter或使用pre_filter。区别是filter会在HNSW搜索时同步过滤节点pre_filter则先把文档集切成子集再构建临时图搜索。前者适合过滤条件不太严苛时后者适合过滤后剩余数据量很小但要求极高精度的场景。我的建议是如果过滤条件能把候选集收缩到总集合的30%以内直接使用filter字段就好速度不错如果过滤后只剩下百分之一的数据pre_filter反而能显著提升精度因为它在小集合上跑得很快但会额外消耗一些内存。实际验收时一定要专门测“带过滤的召回率”因为HNSW在过滤条件下可能出现“被过滤的邻居把真实最近邻挤掉”的问题。4.4 分页与深度翻页别把from拉得太狠向量检索是用图结构取最近邻天然不支持大偏移量深度翻页。如果你还在用from来翻到第5000条性能会急剧恶化。对于Top100以内的查询直接用knn没问题但如果用户需要批量取回大量结果我推荐用search_after基于上一页最后一个文档的排序值继续向后取配合稳定排序字段避免重复和跳页。如果需要导出全量相似集可以分多次用不同时间批次过滤再逐段做向量搜索。5. 我踩过的坑召回率下降、内存爆炸与得分分布异常这一节的内容基本都是我在真实项目里摔过的跟头写出来希望你能绕开。有时候问题不是出现在模型或数据而是一个不起眼的索引参数。5.1 向量维度太高索引构建内存踩穿第一次上线时我用了768维模型数据量是500万条集群单节点32GB内存。HNSW索引构建时不仅要把向量本身放内存还要维护邻接表和候选队列。结果构建进行到三分之一堆外内存直接飙升节点频繁Full GC最终OOM。后来我把向量降到256维并把一个节点拆成三个分片才稳定下来。建议在规划容量时用下面这个粗略公式估算原始向量存储占用约等于数量乘以维度乘以4字节加上HNSW邻接结构整体再乘以1.3到1.5倍。500万条、256维的裸向量大约是5GB加上图结构后约6.5到7.5GB不算大。但同样的数据量换成768维就会接近20GB单节点就会很吃力。别把32GB内存全分给堆要给文件系统缓存留空间因为Lucene的向量数据主要在堆外访问。5.2 HNSW参数太保守召回率只有60%刚上线时我在ad搜索场景跑评估集正确结果出现在Top10的比例只有62%非常难看。查了一圈发现ef_search只有20m也只有8。我把ef_search提升到100、m提升到16后召回率恢复到了93%。这个差别在低维数据上可能不明显但在高维稀疏分布的数据上很致命。调参的时候别只看QPS要拿一个带标准答案的小测试集反复跑对比recallK。5.3 余弦相似度得分集中到0.999附近用cosine做度量时如果向量做了归一化所有得分其实会落在[-1,1]内而真实相似内容之间的得分往往压缩在0.9以上区分度很差。我一度以为模型出了问题后来才意识到这就是余弦相似度的天然特点。如果你需要用分数做阈值过滤可以考虑改用dot_product加归一化或者对得分做sigmoid式的放缩。更稳妥的办法是不依赖绝对分数而是靠RRF排名融合结果让不同来源的相对名次说话。5.4 filter条件下返回结果集有空洞这里有个隐蔽问题HNSW在搜索时虽然会跳过被过滤的节点但它本质上是一个近似搜索可能因为过滤条件把部分真最近邻排除在候选集之外最终结果量不足k个或准确率下降。我在一个品牌过滤场景中发现带过滤的结果明显比全量搜索差。解决思路是把过滤条件也作为查询的“硬性限制”同时提供一部分低排序兜底结果或者把pre_filter开起来重新构建子图。实际效果需要按数据分布压测不要盲信默认行为。6. 生产环境容量估算与持续迭代建议做开发的时候跑通Demo很容易真正上生产要考虑的是数据量、内存、查询并发和模型更新后如何重算。我见过不少项目在联调时很流畅一接线上流量就崩核心原因就是没有提前估算好向量索引对资源和查询延迟的压力。6.1 一个可操作的容量估算模板假设你有100万条文档每条有一个256维float向量。向量本身的内存消耗为100万 * 256 * 4字节 1,024,000,000字节约0.95GBHNSW邻接结构等额外开销按40%估算最终约1.3GB如果开启带原始_source存储还要额外算上浮点转字符串和JSON开销会更大再看查询并发如果一个节点分配4GB堆外内存用于向量缓存并发10个查询、每个查询num_candidates100数据在内存中的随机读取压力会很大。我建议在线指标重点关注两个P99查询延迟和JVM堆外内存使用率。当堆外内存达到系统物理内存的70%以上时就要考虑扩容或做量化压缩。下面这个表格是我在项目里用来做决策的典型场景数据规模向量维度节点数量建议单节点内存预计单查询P9910万256116GB30ms500万256332GB30-80ms2000万128 量化532GB50-120ms这组数字只是我做过的某类文本场景的参考不同业务差异很大但有一个规律是通用的查询延迟和节点数不是线性关系分片太多会导致跨节点聚合开销上升分片太少又会让单节点压力过大。需要拿自己的数据做一次压测矩阵才能定准。6.2 降维与量化用小损失换大容量如果数据规模实在太大我会先考虑降维再考虑量化。降维的方式有很多从模型输出层直接改小维度到用PCA做后处理都可以。量化则是把float32转成int8减少到四分之一内存。Elasticsearch新版支持int8量化向量代价是召回精度轻微下降。我在一个日志相似性分析场景里验证过int8量化后内存下降了72%recall10只下降了1.8个百分点完全值得。使用量化前务必确认模型生成向量的尺度稳定如果向量分布差异过大量化误差会被放大。稳妥做法是先抽样统计分布再决定量化阈值。6.3 模型更新与存量向量重建的流程设计还有一个很容易踩的坑线上跑得好好的某天数据标注团队觉得“语义差不多但聚类不准”于是重新训练或换了一个更好的向量模型。这会导致新旧向量不在同一空间新旧文档不能混在一个索引里比相似度。此时最稳妥的做法是“全量重建索引”而不是原地更新部分字段。我的建议是设计一个小基座比如用版本号字段管理向量模型版本。新数据写入新索引旧索引保留给历史查询。等全部数据用新模型重新向量化后再用别名切换流量。这个流程虽然烦琐但能避免“模型漂移”导致结果质量忽高忽低。6.4 最后几件容易被忽略的小事配置向量索引时记得把_source字段精简掉不需要的大字段减少IO压力。查询时能请求指定字段就别拉整个_source。如果对相似度要求没那么苛刻可以关闭“获取总命中数”的追踪因为向量查询的精准总命中数计算代价较高。另外向量检索和传统检索混合使用时建议把相同过滤条件抽出来只写一遍避免两端各写一遍漂移不一致。我在实际项目中还有一个体会向量搜索不是要取代关键词搜索而是要和它配合。纯向量方案在小规模Demo里很好玩但一碰到长尾词、专有名词、数字型号往往会输给BM25的精确匹配。最关键的技术决策其实就是“什么时候该相信向量什么时候该相信词汇”。设计好一套混合通路远比纠结单个模型精度更让人安心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑