Elasticsearch查询实战:从倒排索引到聚合优化全解析

发布时间:2026/10/21 23:54:55
Elasticsearch查询实战:从倒排索引到聚合优化全解析 写Elasticsearch查询说难其实也不难会了match和term就能应付不少场景但说容易也真不容易真到线上排查慢查询、优化聚合性能的时候才发现自己对Query DSL的理解还停留在能跑就行的阶段。这篇内容是我在实际项目中用ES做日志检索和数据统计时沉淀下来的一套查询经验从最常用的基础查询到组合查询、聚合分析、性能优化和踩坑记录一次性梳理清楚希望能帮刚接触ES的同学少走弯路也给已经在用的人提供一些可参考的排查思路。1. 先把检索的底层逻辑搞明白后面写查询才不迷糊很多人上手ES就直接背查询语法match怎么写、term怎么写背得滚瓜烂熟但一旦查询结果不符合预期就完全不知道从哪里排查。问题的根源其实不在语法而在对ES底层检索模型的理解。ES之所以能在大数据量下做到秒级返回核心靠的是倒排索引。我们平时写SQL查MySQL是遍历表数据去做匹配而ES在写入文档时就已经把文本拆成词项建立了一张词汇表到文档列表的映射查询时直接查这张表效率完全不是一个量级。1.1 倒排索引和相关性打分是怎么回事倒排索引可以粗浅地理解成一本新华字典的偏旁部首目录你查一个字不是从第一页翻到最后一页而是直接按部首定位到某个范围再精确找到目标字所在的页码。ES的倒排索引干的是同一件事对文档的每个字段值做分词生成词项然后记录每个词项出现在哪些文档里。比如Elasticsearch 查询实战这串文本如果按标准分词器切分可能被拆成elasticsearch、查询、实战几个词项每个词项背后挂着一串文档ID。但光能查到还不够搜索结果得有个排序规则否则用户搜手机返回的1000条结果谁排在前面这里就引入了相关性打分也就是我们常说的_score。ES默认使用BM25算法计算相关性核心逻辑大致是词项在文档中出现频率越高得分越高词项在所有文档中出现频率越高说明这个词越普遍它的权重就越低。这很容易理解搜的和搜Elasticsearch相比前者几乎出现在所有中文文档里区分度极低所以它的权重会被压得很低后者只在少数文档中出现一旦匹配就说明这篇文档和查询高度相关。实际使用中不要只盯着_score的绝对值看它的数值大小受文档长度、词频影响很大不同索引之间没有可比性。我更关注的是有评分查询和无评分查询的差别这在后面讲bool查询时会用到。简单说match查询会计算相关性得分term、range这类结构化查询默认不计分但如果你把它们放进must而不是filter里ES还是会额外算一遍分白白消耗CPU。1.2 查询上下文与过滤上下文的本质区别这是我特别想强调的一点因为项目里见过不少同事把这两个概念混在一起导致查询性能下降。ES的查询条件可以运行在两种上下文里查询上下文和过滤上下文。查询上下文回答的是这篇文档有多匹配这个问题它除了判断文档是否满足条件还会计算相关性得分_score然后按得分排序。过滤上下文回答的是这篇文档匹不匹配这个问题只做二值判断匹配或者不匹配不算分。因为不算分过滤上下文可以走缓存第二次执行同样的过滤条件时直接命中缓存返回结果性能开销非常小。举个直观的例子。假设你要查状态为已支付且标题包含耳机的订单状态是结构化字段标题是全文检索字段。正确的写法是将状态放进filter里{ query: { bool: { must: [ { match: { title: 耳机 } } ], filter: [ { term: { status: PAID } } ] } } }filter里的term查询不需要算分ES会缓存这个过滤结果。如果把它放进must里每次查询都要重新计算_score纯属浪费。在数据量达到千万级以上时这种写法的性能差距会非常明显。2. 最常用的三类叶子查询match、term、range2.1 match查询全文检索的主力match是ES里使用频率最高的查询它会把查询字符串先做分词再用分词后的词项去倒排索引里匹配最后合并结果并按相关度排序。这里有个非常容易踩的坑很多人以为match是精确匹配这是不对的。比如你搜手机壳match会把手机和壳分开匹配只要文档里包含任意一个词就算命中。这在大多数全文搜索场景下是符合预期的但在某些要求严格的场景下会出问题。如果你希望整个短语作为一个整体去匹配要用match_phrase{ query: { match_phrase: { title: 无线耳机 } } }它要求分词后的词项必须按顺序出现在文档中并且默认允许词项之间有slop参数指定的间隔。slop默认是0也就是严格相邻如果设置成1就允许无线降噪耳机这种中间多一个词的情况也能匹配。这个参数在搜索场景里很实用比如用户输入无线 耳机你可以通过调整slop来容忍输入中的多余字符。match还有一个容易忽略的operator参数。默认是or也就是分词后任意词项匹配即可如果你想要求所有词项都出现设置operator: and。我在做商品搜索时就遇到过这个问题用户搜红色连衣裙默认or逻辑会把红色或连衣裙单独匹配的文档都返回结果搜出来一堆红色T恤和蓝色连衣裙体验很差。改成and之后才真正做到了既包含红色又包含连衣裙。2.2 term查询精确匹配的陷阱term查询用于精确匹配结构化数据比如数字、日期、布尔值以及对keyword类型字段做精确匹配。它的逻辑是不分词直接把整个值拿去倒排索引里查。看起来很简单但在使用long、integer等数字类型字段时没问题一旦对text字段执行term查询就很容易出现明明文档里有这个值却查不到的情况。原因是text字段在索引时会经过分词器处理比如Elasticsearch实战会被切分成elasticsearch和实战两个词项term查询拿Elasticsearch实战这个完整字符串去匹配当然什么都匹配不到。正确做法是查询时也使用和索引时相同的分词规则或者更简单点对需要精确匹配的字段使用keyword类型。我这里有一个实践习惯除非确实需要全文检索否则一律用keyword类型。比如订单号、状态、用户ID、商品SKU这些字段用keyword存储term查询直接精确命中又快又准。如果当初建索引时不小心把字段设成了text可以通过给字段加keyword子字段来弥补{ mappings: { properties: { orderNo: { type: text, fields: { keyword: { type: keyword } } } } } }这样查询时用orderNo.keyword做term就能精确匹配了。这类多字段设计在ES里非常常见也是官方推荐的做法。2.3 range查询范围检索的隐藏细节range用于范围查询最常见的是数字和日期范围。比如查价格在100到500之间的商品{ query: { range: { price: { gte: 100, lte: 500 } } } }参数包括gt大于、gte大于等于、lt小于、lte小于等于。日期范围查询稍微特殊一点ES支持用now和日期数学表达式。比如查最近7天的订单{ query: { range: { orderDate: { gte: now-7d/d, lte: now/d } } } }这里的now-7d/d含义是当前时间减去7天再向下取整到当天零点now/d则是当前时间向下取整到今天零点。日期数学表达式是ES里一个很灵活但不太好记的特性我用久了总结出一个技巧在做报表统计时如果服务器时区不是东八区日期范围查询的结果可能会差8个小时。ES对日期的解析默认受时区设置影响排查这种问题时要先检查索引的date字段映射时是否指定了format和time_zone。最简单的方式是在查询参数里显式带上时区避免依赖环境配置。3. bool查询组合条件的核心引擎3.1 四种逻辑子句的职责边界实际业务中几乎不会只用一个叶子查询更多时候需要把多个条件组合起来这时就要用bool查询。bool查询有四个子句must、should、must_not、filter理解它们各自的语义是写出正确查询的前提。must子句中的条件必须满足并且会贡献相关性得分。比如搜索标题包含耳机且价格大于100的商品两个条件都放在must里。filter子句同样是必须满足但不影响得分适合放状态类目这类结构化过滤条件。must_not子句表示必须不满足比如排除某些状态它同样不计分。should子句则是可以满足也可以不满足当bool查询里同时存在must或filter时should的作用是提升满足条件的文档的得分当只有should而没有其他子句时至少满足一个should条件的文档才会被返回。这里有一个经常让人困惑的点should到底什么时候影响结果集什么时候只影响排序我一开始也在这上面栽过跟头。记住结论就够了在只有should的bool查询里默认情况下至少要匹配一个should条件一旦bool里还有其他must或filtershould就退化为纯加分项不再影响结果集除非你设置了minimum_should_match参数。这个参数可以控制最少需要匹配几个should条件比如minimum_should_match: 1就强制要求至少满足一个should。3.2 用bool查询实现且、或、非的完整组合来看一个相对完整的例子。假设电商平台要做商品搜索需求是搜索词耳机命中的商品优先类目必须是数码配件价格在200到800之间排除已下架商品如果商品有免邮标签则排名靠前。翻译成bool查询{ query: { bool: { must: [ { match: { title: 耳机 } } ], filter: [ { term: { category: 数码配件 } }, { range: { price: { gte: 200, lte: 800 } } } ], must_not: [ { term: { status: OFF_SHELF } } ], should: [ { term: { tags: 免邮 } } ] } } }在这个查询里match保证标题相关性filter里的term和range确保类目和价格范围精确匹配且不计分可缓存must_not排除下架商品should给免邮商品加分。这个结构几乎涵盖了业务查询里90%的组合场景剩下的就是往对应的子句里填充不同类型的叶子查询。我在实际项目中用过类似结构处理订单中心的检索需求因为条件多、逻辑层级深还嵌套过两层bool。嵌套时要注意bool里可以再放bool比如某个filter条件本身是一个复杂的OR组合可以写成{ bool: { filter: [ { terms: { channel: [APP, H5] } }, { bool: { should: [ { term: { orderType: NORMAL } }, { term: { orderType: GIFT } } ], minimum_should_match: 1 } } ] } }terms是term的复数版一次可以匹配多个值这个也很常用。处理渠道是APP或H5这种场景时用terms比用boolshould更简洁高效。3.3 filter上下文里的缓存机制前面提到filter不计分、可缓存这里展开讲一下缓存的机制和收益。ES对filter查询结果会做bitset缓存一个文档要么在集合里要么不在用位图表示非常省空间。当下一次执行相同的filter时直接复用位图结果省去了重新遍历倒排索引的开销。这个特性在做时间范围过滤时特别有用。比如日志检索场景通常都会加一个timestamp的时间范围限制大部分查询请求命中的都是最近一小时的数据。把时间范围放在filter里ES会缓存最近一小时这个过滤结果对应的文档集合后续相同范围的查询直接走缓存延迟能下降一个数量级。但要注意查询的DSL必须完全一致才能命中缓存哪怕只是把gte从now-1h改成now-60m缓存也是无效的。所以生产环境里我习惯把时间范围的粒度做成固定的参数占位而不是每次动态拼接now-xxx这种表达式否则缓存利用率会很低。4. 聚合查询让ES从检索引擎升级为分析引擎4.1 三种聚合类型metric、bucket、pipelineES的聚合分为三大类指标聚合Metric、分桶聚合Bucket和管道聚合Pipeline。指标聚合对一组文档计算统计值比如sum、avg、min、max、cardinality计数去重分桶聚合把文档按条件放入不同的组比如按时间、按类目、按状态分组管道聚合则是基于其他聚合的结果再做二次计算比如计算百分比、求分桶间的比值。做日志统计最常用的是时间分桶。比如统计每小时的错误日志数量{ size: 0, aggs: { hourly: { date_histogram: { field: timestamp, fixed_interval: 1h }, aggs: { error_count: { filter: { term: { level: ERROR } } } } } } }这里size: 0表示不返回原始文档只要聚合结果。date_histogram是ES时间分桶的核心聚合fixed_interval支持1h、1d、30m等固定间隔还有另一个参数calendar_interval用于日历感知的间隔比如month、quarter。两者的区别在于calendar_interval会考虑月的天数不同适合做自然月统计fixed_interval则严格按固定时长切分适合做精确的时间窗口统计。用错的话按月统计时会出现每个月桶长度不一致的问题。4.2 嵌套聚合实现先分组再统计聚合最强大的地方在于可以嵌套。先用bucket分组再在每组内做metric统计甚至可以在组内再做下一层bucket。比如统计每个类目下商品的最高价和平均价同时对每个类目再按品牌细分{ size: 0, aggs: { by_category: { terms: { field: category.keyword, size: 10 }, aggs: { max_price: { max: { field: price } }, avg_price: { avg: { field: price } }, by_brand: { terms: { field: brand.keyword } } } } } }terms聚合根据字段值分组默认返回文档数最多的前10个分组。这里要注意size参数它决定返回多少个桶不是决定每个桶里有多少文档。很多人刚接触时会困惑以为size: 10是每组返回10条文档实际上聚合结果里根本没有文档列表这个size控制的是桶数量。嵌套聚合在业务报表里非常实用。我之前帮某团队做过一个数据分析看板的后端接口原来是从ES查出所有明细再在Java内存里做分组统计数据量一上来就OOM。后来改成全链路用ES聚合一个查询搞定多层分组和统计响应时间从原来的几十秒降到几百毫秒效果非常明显。4.3 去重计数的坑cardinality不是精确值cardinality聚合用来统计去重数量比如统计某一天有多少个独立用户访问。它的实现基于HyperLogLog算法是一种近似算法默认情况下精度在95%以上但并不是精确值。如果你对结果精度要求极高比如财务对账、精确PV/UV统计需要重新评估是否能用这个聚合。HyperLogLog的原理可以理解为一种哈希映射和概率估算的结合它不保存每个元素本身而是通过记录哈希值中最长连续零的个数来推算基数。因为节省了存储它能在大数据量下用很小的内存估算出海量去重值。实际使用中如果希望提高精度可以设置precision_threshold参数值越大越精确但内存占用也越高。默认的3000已经能应对大多数场景把值调到10000以上对精度提升帮助有限内存却可能翻好几倍性价比不高。另外千万注意cardinality对字段类型有要求text字段需要开启fielddata或使用keyword子字段否则直接报错。这个错误信息有点隐蔽排查时容易走弯路提前了解能省不少时间。5. 真实场景拆解日志分析、商品搜索、权限过滤5.1 业务日志异常分析场景以一个典型的业务日志系统为例。日志索引按天命名字段包括timestamp、service服务名、level日志级别、message日志内容、traceId链路ID、userId用户ID。现在要排查最近30分钟某个服务报错最频繁的接口同时要把慢请求也捞出来。一次查询可以直接做到按服务名过滤限定最近30分钟只查ERROR级别然后按接口URI分组统计数量再筛选出数量大于100的接口。用bool加filter做前置过滤用terms聚合加bucket_selector管道聚合完成分组和筛选{ size: 0, query: { bool: { filter: [ { term: { service: order-service } }, { term: { level: ERROR } }, { range: { timestamp: { gte: now-30m } } } ] } }, aggs: { by_api: { terms: { field: api.keyword, size: 20 }, aggs: { error_count: { value_count: { field: traceId } }, error_filter: { bucket_selector: { buckets_path: { count: error_count }, script: params.count 100 } } } } } }bucket_selector是管道聚合里的筛选器它在分桶完成后根据每个桶内的子聚合结果决定保留或丢弃该桶。这里的buckets_path指定要读取的子聚合结果路径script里的params.count就是从error_count读取到的值大于100的桶才会保留。这个查询一次就能拿到最近30分钟内、错误数超过100的接口列表省去了把原始日志捞出来再逐条统计的麻烦。实际排查问题时我还会叠加一层percentiles聚合来看接口耗时的P99值。这比看平均耗时靠谱得多平均耗时容易被极少数慢请求拉高P99能更真实地反映大多数用户的体验。percentiles聚合也是metric聚合的一种按百分比分组返回阈值比如P99对应的耗时值就是99%的请求都在这个耗时以内。5.2 商品搜索查询相关性、过滤、排序的整合商品搜索是ES最常见的业务场景之一核心需求无非是三块相关性搜索、属性过滤、销量或价格排序。设计这个查询时我会把整个搜索需求拆成几个独立的部分分别考虑。搜索词部分用multi_match同时在标题、品牌、描述等多个字段里检索。multi_match支持对多个字段执行match查询还能给每个字段设置不同的权重比如标题匹配的权重是3品牌匹配的权重是2描述匹配的权重是1{ query: { bool: { must: [ { multi_match: { query: 无线 耳机, fields: [title^3, brand^2, description] } } ], filter: [ { term: { status: ON_SALE } }, { terms: { category: [耳机, 音响] } } ] } } }排序部分用sort比如按销量降序、按价格升序。如果希望综合相关性和销量排序就需要用到function_score查询来把销量纳入相关性计算这是我下一节要展开的内容。商品搜索里有几个产品层面的细节用户搜索词的分词是否符合预期、同义词是否覆盖、错别字是否能容忍。这些不是单纯靠改查询DSL能解决的需要结合索引分词器和查询分词器的配置。比如中文搜索经常用IK分词器索引时用ik_max_word最大化切分查询时用ik_smart做粗粒度切分两者配合能显著提升召回率和准确率。这块内容展开又是一个长篇这里先点到为止。5.3 基于权限的数据隔离过滤做B端系统的人一定会遇到这样一个需求同一个索引里的数据不同角色只能看到自己权限范围内的部分。比如CRM系统里销售A只能看自己负责的客户销售经理能看到自己团队的所有客户。这种场景不适合给每个团队建一个索引更合理的做法是在查询时动态注入过滤条件。我的做法是在服务端拦截所有ES查询请求根据当前登录用户的权限范围动态生成一个term或terms条件强制放进bool.filter里。这样既不影响查询相关性的计算又能利用filter缓存提升性能。关键点是权限过滤条件必须放在filter而不是must里否则不仅浪费算分资源还会导致权限条件参与相关性排序这是绝对要避免的。还需要考虑的一个细节是权限过滤条件和用户搜索条件之间的嵌套关系。如果用户的搜索条件本身是一个bool查询权限过滤条件应该放在最外层bool里确保它不会被其他should条件影响而失效。6. function_score把业务指标融入相关性排序6.1 为什么要用 function_score默认情况下ES只根据文本相关性打分排序。但很多业务场景里光靠文本相关度决定排名是不够的。比如电商搜索耳机两个商品标题都包含耳机文本相关度几乎相同但一个销量10万一个销量100用户显然更愿意看到销量高的那个。这时候就需要function_score查询在原始相关度评分的基础上叠加业务函数得分。function_score的结构是这样的原查询负责选出候选集并计算基础分函数部分负责计算额外的得分最后通过boost_mode决定基础分和函数分如何合并。常用的函数类型有weight、field_value_factor、script_score、random_score等对我来说field_value_factor用得最多它能把文档字段的值直接映射成得分。6.2 用 field_value_factor 平衡文本相关性和销量{ query: { function_score: { query: { multi_match: { query: 耳机, fields: [title] } }, field_value_factor: { field: sales_count, factor: 0.001, modifier: log1p }, boost_mode: multiply } } }这里有几个关键参数需要说明。field_value_factor直接拿字段值乘以factor作为函数得分但如果不对原始值做处理销量100万和销量10万的差异会被放大到失控。所以一定要用modifier做一次数学变换log1p是取log(1x)能把原始值压缩到对数尺度避免头部商品垄断排名。factor决定函数分的缩放比例这个值需要结合业务数据分布反复调。boost_mode参数可选multiply相乘、sum相加、replace替换等。multiply表示最终得分等于基础分乘以函数分两者是相乘关系sum表示相加对基础分的影响相对温和。我通常会先跑几组数据观察_score分布再决定用哪个模式。调function_score有一个从实战中得来的经验不要开完函数分就完事一定要在kibana里对比开启前后的_score分布。如果发现销量高的商品把文本相关度完全碾压了说明函数分权重过高需要调低factor或改用sum模式如果排名几乎没有变化说明函数分的量级太小需要增大factor。这个过程和调机器学习模型的超参有点像得根据实际数据反馈逐步逼近。6.3 按时间衰减的加权排序让新鲜内容排前面另一个常见的业务需求是让新发布的内容排在旧内容前面比如资讯列表、帖子搜索。可以用gauss衰减函数对时间字段做距离衰减打分发布时间越接近当前时间得分越高{ query: { function_score: { query: { match: { title: Elasticsearch } }, functions: [ { gauss: { publishTime: { origin: now, scale: 7d, decay: 0.5 } } } ], boost_mode: sum } } }origin是参考点用now表示以当前时间为最优scale定义衰减的尺度7天表示距发布7天时得分衰减到decay指定的值也就是0.5超过7天继续衰减最终趋近于0。用sum模式把时间加分叠加到基础相关度分数上既能保证相关性是主导又让新鲜内容获得一定优势。7. 排错思路与性能优化我踩过的几个典型坑7.1 深分页与内存消耗问题在用ES做搜索时一个很常见的需求是翻到第100页。但ES的fromsize分页方式在深分页场景下有严重性能隐患。比如from: 9990, size: 10ES会把每个分片的前10000条全部取出汇总到协调节点后在内存里排序再取出第9990到9999条返回。这个过程中间结果集的体积会随页数增大线性膨胀最终导致内存溢出或GC频繁。项目里真实遇到过的问题是一个数据量几千万的索引用户用旧版系统连续翻页到第200页时查询耗时从几十毫秒飙升到几秒甚至超时最后把协调节点内存打满影响了整个集群的稳定性。排查过程花费了不少功夫才定位到是深分页导致的。解决深分页有三种思路search_after游标查询、scroll滚动查询、以及限制最大翻页深度。对于实时搜索类页面推荐用search_after它通过上一页最后一条数据的排序值来定位下一页避免了大偏移量带来的中间结果集开销。对于数据导出这类全量遍历任务用scroll更合适它会在初始化时生成一个快照之后基于快照滚动拉取数据。但scroll在游标存活期间会占用服务端资源不能长时间挂在那里不关闭。7.2 wildcard 查询和 prefix 查询为什么那么慢ES里有两个看起来很好用但实际容易踩坑的查询prefix和wildcard。prefix查前缀wildcard支持通配符比如*耳机*这种模糊匹配。但它们都无法利用倒排索引高效定位必须遍历大量词项做字符串匹配性能非常差。特别是wildcard如果以*开头实际上等于全表扫描数据量一上来延迟直接爆炸。我接手过一个搜索系统查询条件里带了一个wildcard匹配商品编码的逻辑用户输入编码片段后就触发全表扫描。最严重的时候一次查询消耗了数秒把整个集群的CPU打到90%以上。后续把商品编码的匹配逻辑改成了用ngram分词器在索引阶段做拆分查询时用match或term就能搞定性能提升非常明显。当然ngram方案也有代价索引体积会变大因为每段文本要拆成N个连续子串存储。但它属于索引时多消耗查询时快得多的典型思路在需要模糊匹配的场景下比wildcard可靠得多。如果业务上确实需要模糊搜索match_phrase配合slop往往也能替代一部分通配符场景。7.3 字段映射不对导致的查询结果异常这是最隐蔽的一类问题因为它不会报错只是结果看起来不对。比如某个字段在索引里被映射成了text你在查询时以为它是keyword用term去查结果一条都查不到。又比如某个字段的format只支持yyyy-MM-dd但查询时传了2024-01-01 10:00:00ES解析失败会返回异常信息但这类报错在下游系统里很容易被吞掉。我的经验是在排查一个查不到数据的问题时第一步不是看查询语法而是去看索引映射。用GET /your_index/_mapping看一眼所有字段的类型和格式基本能排除80%的人为失误问题。另外检查一个查询到底匹配了多少文档可以在查询体后面加_source: false再配一个track_total_hits: true确保返回精确的命中总数避免ES为了性能用近似值误导判断。7.4 聚合性能优化从内存到执行计划聚合查询对内存的消耗比普通查询大得多。尤其是terms聚合ES需要在内存里维护分组信息和计数。当分组的基数很大时比如一个字段有几百万个不同的值内存很容易被打爆。优化思路有几个方向限制聚合的size减少返回桶数、只对keyword类型字段做聚合、在聚合前先用filter缩小数据范围。还有一个容易被忽略的优化点doc_values。聚合用到的字段最好开启doc_values默认开启它的作用是在索引写入时额外构建一列列式存储的数据结构让聚合和排序不必从倒排索引里临时取数据还能用更紧凑的方式加载字段值。text字段默认没有doc_values如果要对text字段聚合必须开启fielddata但它会占用大量堆内存一般强烈不建议。说实话遇到需要对某个字段做聚合但它是text类型的需求时更合理的做法是重建索引把字段类型改成keyword加上doc_values。8. 查询性能观察善用Profile和慢日志定位瓶颈8.1 Profile API看清每个查询阶段的耗时ES的Profile API是排查慢查询的利器。在普通查询请求上加一个profile: true返回结果里会展示查询被拆分成哪些阶段每个阶段耗时多少以及命中了哪些倒排索引。当年排查慢查询时我就是靠Profile发现某个查询在rewrite阶段耗时占了60%以上原因是一个复杂的bool查询里嵌套了多层should条件导致查询重写时反复展开组合。具体做法很简单Kibana的Dev Tools里直接执行带profile的查询然后观察输出中的Query数组按time降序排列就能快速定位耗时最高的子查询。但有一点要提醒Profile API本身有额外开销生产环境不要对全量请求开只针对排查问题时的抽样请求使用。8.2 慢查询日志的配置经验ES支持配置慢查询日志把超过阈值的查询记录到日志文件。一个我在生产环境使用的配置思路index.search.slowlog.threshold.query.warn设置为1秒index.search.slowlog.threshold.fetch.warn设置为500毫秒这样一旦有查询超过阈值日志里就会打出完整的查询DSL和执行信息。排查问题时直接搜慢日志找到对应的DSL基本就能锁定问题查询。不过慢日志只能告诉你哪个查询慢不能告诉你为什么慢。要回答这个为什么还是得结合Profile分析查询计划再看索引映射和数据分布。9. 一个完整查询语句的构建过程复盘最后我用一个综合案例来串一下前面提到的所有知识点。假设你要实现一个客户工单检索功能需求包括按关键词搜索工单标题和描述按状态、优先级、处理人过滤按创建时间范围过滤只看当前用户有权限的数据结果按相关度排序同时让未完成工单略微靠前。第一步设计索引映射。工单标题和描述用text类型配IK分词器状态、优先级、处理人用keyword类型创建时间用date类型。这一步决定了后面的所有查询是否高效。第二步构建查询主体。外层bool查询里must放multi_match匹配标题和描述filter放权限过滤terms限定可见的客户ID列表、状态过滤term和时间范围rangemust_not排除已关闭工单。第三步叠加function_score提升未完成工单的排名。在functions里加一个filter加weight的组合函数对状态为处理中或待处理的工单增加固定权重。weight函数在if函数配合filter使用时会引起文档集重新评分需要注意这一点但在数据量可控时问题不大。最终查询结构类似{ query: { function_score: { query: { bool: { must: [ { multi_match: { query: 退款 失败, fields: [title^3, description] } } ], filter: [ { terms: { visible_customer_id: [C1001, C1002, C1003] } }, { term: { status: OPEN } }, { range: { createdAt: { gte: now-30d } } } ] } }, functions: [ { filter: { terms: { priority: [URGENT, HIGH] } }, weight: 2 } ] } }, sort: [ { _score: desc } ] }构建这类查询时我有一个固定的自查清单过滤条件是否都放进了filter上下文需要全文检索的字段是否用到了match系列查询需要精确匹配的字段是否用的是keyword类型加term查询排序依据是否和业务预期一致这条清单帮我挡掉了不少上线后才暴露的隐性Bug。最后的经验小结写ES查询和写SQL很不一样它对相关性、分词、缓存机制的理解要求更高。我的体会是先把倒排索引和上下文概念吃透再去背各种查询语法效率会高很多。平时多抓几个真实的慢查询日志用Profile看一看执行细节比看多少文档都有用。上面这套查询体系和排错思路是我在几个不同业务场景里反复验证过的希望对你也有参考价值。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询