《Spring ES实战》第5招 Query要分两类
# Elasticsearch Query DSL 实战:把“相关性”与“过滤”分开 ```text 旧方案:所有条件都塞进 must,Text 字段用 term 硬匹配,模糊词用 wildcard 兜底 冲突:数据明明存在却查不到,评分被无关条件污染,通配符查询拖垮 CPU 后果:搜索结果时灵时不灵,排序忽高忽低,集群在促销高峰频繁 GC ``` 很多团队把 Elasticsearch 当“能分词的 MySQL”,把 SQL 的 WHERE 习惯直接搬进 Query DSL。 结果就是:Text 字段用 term 查不到,数字字段用 match 乱匹配,wildcard 满天飞。 我是老李,过去半年在深圳一家电商公司做搜索中台,亲眼见过一个“明明有货却搜不到”的线上事故,根因就是 Query DSL 里把全文查询和精确过滤混成了一锅粥。 为什么数据存在却查不到? 什么时候条件应该参与评分,什么时候只负责过滤? 如果把评分和过滤混在一起,线上会付出什么代价? # 01、故事 ```text 周五 23:47→电商搜索中台→大促预热搜索“华为手机”→冲突:有货却搜不到 ``` 那天是周五晚上 23:47,我还在公司。运营在群里发了一张截图:App 搜索“华为手机”,结果页空白,但商品后台明明有 300 多台库存。 任务是十分钟内恢复搜索,否则大促预热页要回滚。我打开 Kibana,先跑了一遍线上查询,确实返回 0 条。 再看商品索引,文档存在,status 是 1,keyword 字段里也有“华为手机”。我第一反应是字段映射有问题,但 mapping 显示 keyword 是 Text 类型。 时间一分一秒过去,群里消息越刷越快。我盯着那个查询体,突然意识到问题可能不在数据,而在查询本身。 > 深夜告警催人醒, > 有货无果似幽灵。 > 索引文档皆安在, > 查询深处藏祸根。 # 02、问题 ```text 需求变化:从“关键词匹配”扩展到“品牌+类目+价格+状态”多维筛选 冲突:Text 字段用 term 查询,分词后索引词与查询词不一致 后果:数据存在但查不到,过滤条件参与评分,排序被无关字段干扰 ``` 最初的搜索只做关键词匹配,一个 match 就够。后来运营要求按品牌、类目、价格、状态筛选,开发就把所有条件都塞进 bool 的 must 里。 问题出在 keyword 字段。它是 Text 类型,写入时被 standard 分词器拆成“华为”和“手机”两个词。而 term 查询不做分词,拿“华为手机”整串去倒排索引里找,当然找不到。 更隐蔽的问题是:brand、category、status 这些精确值字段也放在 must 里,它们会参与评分。一个“华为”品牌匹配的得分,可能比关键词“手机”的得分还高,排序就乱了。 可验证的验收标准是:搜索“华为手机”能返回结果;按品牌“华为”筛选后结果只含华为;价格区间 3000-6000 能正确过滤;status=1 只返回在售商品。 > 分词写入倒排里, > 整串查询空手归。 > 过滤误入评分场, > 排序错乱用户离。 # 03、原理 ```text 核心机制:Text 字段分词后建倒排索引,term 查询不做分词直接匹配索引词 ``` Elasticsearch 的查询分两大类:Full-text Query 和 Term-level Query。前者对查询词做分词,后者不做分词。 match 查询会对输入做分词,然后拿每个词去倒排索引里找。比如“华为手机”会被拆成“华为”和“手机”,分别匹配。 term 查询不做分词,拿整个输入去倒排索引里精确匹配。如果字段是 Text 类型,索引里存的是分词后的词,term 拿整串去匹配,必然失败。 一个反直觉的判断是:Text 字段上 term 查询“数据存在却查不到”不是 bug,而是符合设计预期的行为。 要精确匹配 Text 字段,应该用 keyword 子字段,或者把字段映射为 Keyword 类型。 另一个反直觉判断:filter 不参与评分,但它的性能通常比 must 更好,因为它可以利用缓存。把精确条件放进 filter,既正确又快。 > 全文查询问相关, > 词项查询问相等。 > 分词与否是分界, > 张冠李戴必成空。 # 04、架构 ```text 输入:用户关键词+品牌+类目+价格区间+状态→本模块:Query DSL 组装→输出:相关度排序的过滤后结果 ``` 查询层分两条路径:关键词走全文查询,负责评分;品牌、类目、价格、状态走过滤,只负责筛选。 bool 查询是组合器,四个子句各司其职:must 必须匹配且参与评分;should 可选匹配,匹配了加分;filter 必须匹配但不评分;must_not 必须不匹配。 收益是排序只由关键词相关性决定,过滤条件不干扰评分。代价是查询体变复杂,需要团队约定字段类型和查询选择规范。 适用条件是:有明确的关键词搜索需求,同时有多个精确筛选维度。如果只有精确筛选没有关键词,直接用 filter 就够了。 > 一层查询两条路, > 相关过滤各司职。 > 评分只问关键词, > 筛选不扰排序分。 # 05、第一版实战 ```text 环境→配置→代码→启动→验证→V1 ``` 环境:Elasticsearch 8.11.3,Kibana 8.11.3,本地 Docker 单节点。版本需按当前官方文档核验。 先创建索引,keyword 用 Text 类型并带 keyword 子字段,brand、category、status 用 Keyword 类型,price 用 double: ```json PUT /products { "mappings": { "properties": { "keyword": { "type": "text", "fields": { "raw": { "type": "keyword" } } }, "brand": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" }, "status": { "type": "integer" } } } } ``` 写入测试数据: ```json POST /products/_bulk { "index": { "_id": 1 } } { "keyword": "华为手机 Mate60", "brand": "华为", "category": "手机", "price": 5999, "status": 1 } { "index": { "_id": 2 } } { "keyword": "华为手机 P60", "brand": "华为", "category": "手机", "price": 4388, "status": 1 } { "index": { "_id": 3 } } { "keyword": "小米手机 14", "brand": "小米", "category": "手机", "price": 3999, "status": 0 } ``` V1 查询:关键词用 multi_match 查 keyword 和 brand,品牌、类目、价格、状态全部塞进 must: ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } }, { "term": { "brand": "华为" } }, { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 启动验证:在 Kibana Dev Tools 执行上述请求。示例输出: ```text 示例输出: { "hits": { "total": { "value": 2 }, "hits": [ { "_id": "1", "_score": 2.5 }, { "_id": "2", "_score": 2.2 } ] } } ``` 成功标准:返回 2 条华为手机,价格都在 3000-6000,status 都是 1。V1 看起来能跑通。 > 索引建好数据入, > 查询体里五条件。 > 看似结果皆正确, > 隐患已在暗处生。 # 06、排查 ```text 诊断树:现象→怀疑→检查→证据→根因→修复 ``` 问题一:搜索“华为手机”时,如果用户输入“华为 手机”中间有空格,multi_match 能匹配。 但如果用户输入“华为手机”整串,而 keyword 字段被分词,match 依然能匹配。 真正的问题出在另一个场景。 运营反馈:筛选品牌“华为”后,搜索“华为手机”返回 0 条。我怀疑是 term 查询在 Text 字段上失效。 检查 mapping: ```json GET /products/_mapping ``` 证据:keyword 字段是 text 类型,brand 是 keyword 类型。但查询里 term 用在 brand 上,这是对的。问题不在这里。 继续排查:运营说“华为手机”搜不到,是在一个旧索引上。那个索引的 keyword 字段没有 keyword 子字段,且查询里用了 term 查 keyword。 检查旧索引查询: ```json POST /old_products/_search { "query": { "term": { "keyword": "华为手机" } } } ``` 证据:返回 0 条。根因确认:Text 字段被分词,term 拿整串匹配失败。修复:改用 match 或 keyword.raw 子字段。 **错误尝试:** 我一开始以为是 status 字段类型问题,把 status 从 integer 改成 keyword,结果查询依然时好时坏。后来才发现真正的问题是 term 查 Text 字段。 > 现象诡异查映射, > 怀疑类型先入主。 > 证据指向分词处, > 根因方显真面目。 # 07、优化 ```text V1:所有条件塞 must→证据:过滤条件参与评分,排序被干扰→V2:精确条件移入 filter ``` V1 的问题是 brand、category、status、price 都放在 must 里,它们会参与评分。 一个品牌完全匹配的文档,可能因为 brand 字段短、匹配度高,得分超过关键词更相关的文档。 优化:把精确条件全部移入 filter,只保留 multi_match 在 must 里: ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } } ], "filter": [ { "term": { "brand": "华为" } }, { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 这样排序只由 multi_match 的相关性决定,过滤条件不干扰评分。同时 filter 可以利用 Elasticsearch 的查询缓存,性能更好。 > 五条件里四过滤, > 何必都进评分场。 > 移入 filter 不评分, > 排序纯粹性能强。 # 08、演进 ```text V2:must 只留关键词,filter 管筛选→能力:排序纯净+缓存加速→遗留:should 场景未覆盖 ``` 用 V1 同一组请求验证:搜索“华为手机”,品牌华为,类目手机,价格 3000-6000,status=1。 V2 返回 2 条,得分只由关键词相关性决定。示例输出: ```text 示例输出: { "hits": { "total": { "value": 2 }, "hits": [ { "_id": "1", "_score": 1.8 }, { "_id": "2", "_score": 1.5 } ] } } ``` 收益是排序稳定,过滤条件变化不影响得分。代价是查询体变长,需要团队理解 bool 四个子句的语义。 遗留问题:should 子句还没用上。比如“华为”和“荣耀”两个品牌,用户可能都想要,should 可以加分但不强制匹配。 > 同组请求前后比, > 排序纯净分不虚。 > 缓存加速过滤快, > should 场景待补齐。 # 09、洞见 ```text 从现象到判断:全文查询回答“有多相关”,过滤回答“符不符合条件” ``` ## 把“相关性”和“过滤”分开是 Elasticsearch 查询设计的第一原则 ```text must 管评分,filter 管筛选,两者不可混用 ``` 全文查询回答“有多相关”,过滤回答“符不符合条件”。把两者分开,排序才纯净,缓存才生效。混在一起,既污染评分,又拖慢查询。 ## 反直觉判断:Text 字段上 term 查询查不到,不是 bug,是设计预期 ```text Text 分词建索引,term 不分词去匹配,必然错位 ``` 很多人遇到“数据存在却查不到”,第一反应是 Elasticsearch 出问题了。实际上这是 Text 字段分词机制的直接结果。要精确匹配,用 keyword 子字段或 Keyword 类型。 ## 反直觉判断:wildcard 不应替代 Analyzer ```text wildcard 是精确匹配的兜底,不是分词匹配的替代品 ``` wildcard 查询不做分词,它是对词项做通配符匹配。用它来模拟模糊搜索,会扫描大量词项,CPU 飙升。模糊搜索应该靠 Analyzer 的分词和同义词,而不是 wildcard。 ## filter 不评分,但通常比 must 更快 ```text filter 结果可缓存,must 每次都要算分 ``` 精确条件放进 filter,Elasticsearch 可以缓存过滤结果。下次相同过滤条件直接复用,跳过评分计算。这是性能优化的关键一步。 > 相关过滤本两途, > 混为一谈必迷途。 > 词项查询不分词, > 通配岂能替分词。 # 10、系统落地 ```text 入口:App 搜索框→服务:搜索中台→本模块:Query DSL 组装→存储:Elasticsearch→未实现:should 多品牌加分 ``` 原来搜索中台只有一个简单的 match 查询。本篇增加了 bool 组合查询,把关键词、品牌、类目、价格、状态分开处理。 现在能做什么:用户搜索关键词,同时按品牌、类目、价格、状态精确筛选,排序只由关键词相关性决定。 下一篇缺什么:should 子句的多品牌加分场景还没实现,比如用户搜“华为荣耀”,两个品牌都匹配的文档应该排在前面。 官方文档参考:Elasticsearch 官方文档主页 https://www.elastic.co/docs > 搜索中台初长成, > 多维筛选各归位。 > 关键词定排序分, > 过滤条件不扰分。 # 11、小结 ```text 三问三答:为什么查不到→term 不分词;何时评分→must/should;选错后果→排序乱性能差 ``` 为什么数据存在却查不到?因为 Text 字段被分词,term 查询拿整串去匹配分词后的索引词,必然失败。 什么时候条件参与评分,什么时候只负责过滤?must 和 should 参与评分,filter 和 must_not 只负责过滤。关键词放 must,精确条件放 filter。 选错会付出什么代价?排序被无关条件污染,用户看到不相关的结果排在前面;filter 缓存失效,查询性能下降,高峰时集群压力增大。 当前状态:V2 已上线,搜索“华为手机”能正确返回结果,筛选条件不干扰排序。 > 三问三答理分明, > 查询设计有章循。 > 相关过滤分开走, > 线上稳定用户欣。 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:match 和 term 在 Text 字段上的行为有什么区别? match 会对查询词做分词,然后拿每个词去倒排索引里匹配。term 不做分词,拿整串去匹配。Text 字段写入时被分词,所以 term 查 Text 字段通常查不到,match 能查到。 ## 实战题:给商品索引增加一个 should 子句,让搜索“华为手机”时,品牌为“荣耀”的文档也能返回但得分低于华为。 ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } } ], "should": [ { "term": { "brand": "荣耀" } } ], "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 荣耀品牌文档会返回,但得分低于华为品牌文档,因为 should 只加分不强制匹配。 ## 排障题:线上搜索“苹果手机”返回 0 条,但商品后台有货。请写出排查步骤。 第一步,检查索引 mapping,确认 keyword 字段类型。 第二步,用 match 查询测试,如果能查到,说明是 term 查询在 Text 字段上失效。 第三步,检查查询体,看是否用了 term 查 Text 字段。 第四步,修复:改用 match 或 keyword.raw 子字段。 ## 架构判断题:有人说“把所有条件都放进 must,这样最安全,不会漏掉任何条件”。这个说法对吗? 不对。must 里的条件都参与评分,精确条件放进 must 会污染排序。而且 must 不能利用 filter 缓存,性能更差。正确做法是:关键词放 must,精确条件放 filter。 > 理解实战排障题, > 架构判断见真章。 > 查询设计有原则, > 相关过滤不可混。 # 13、升华 ```text 工程结论:查询设计的第一原则是把“相关性”和“过滤”分开 ``` 有依据的结论是:Full-text Query 回答“有多相关”,Filter 回答“符不符合条件”。把两者分开,排序才纯净,缓存才生效,性能才稳定。 term 查 Text 字段失效不是 bug,是分词机制的直接结果。wildcard 不应替代 Analyzer,模糊搜索应该靠分词和同义词。 下一篇可以深入 should 的多品牌加分场景,以及 function_score 如何做更精细的相关性调权。 > 查询设计第一则, > 相关过滤两分明。 > 词项通配皆有界, > 工程路上步步清。 **系列项目:** 电商搜索中台,服务 App 与运营后台 **已完成:** 关键词搜索、品牌/类目/价格/状态筛选、bool 组合查询 **本篇:** 区分 Full-text Query 与 Term-level Query,掌握 match、multi_match、term、terms、range、exists、prefix、wildcard、bool 及 must、should、filter、must_not **下一篇:** should 多品牌加分与 function_score 相关性调权