干货 | 拆解一个 Elasticsearch Nested 类型复杂查询问题
1、线上实战问题
前置说明:本文是线上环境的实战问题拆解,涉及复杂 DSL,看着会很长,但强烈建议您耐心读完。
问题描述:
有个复杂的场景涉及到按照求和后过滤,user_id是用户编号,gender是性别,time_label是时间标签,时间标签是nested结构,intent_order_count是意向订单数量,time是对应时间。
现在要筛选出在20210510~20210610,意向订单数总和为26的男性用户,请问应该怎么写dsl语句?
感觉这个场景很复杂,涉及到array判断后求和,然后求和结果做筛选条件。
请帮忙看看有什么好的dsl语句,或者改变现有mapping结构。
这个是mapping结构 如下:
PUT index_personal
{"mappings": {"properties": {"time_label": {"type": "nested","properties": {"intent_order_count": {"type": "long"},"time": {"type": "long"}}},"user_id": {"type": "keyword"},"gender": {"type": "keyword"}}}
}下面是我构造的数据:PUT index_personal/_doc/1
{"user_id": "1","gender": "male","time_label": [{"time": 20210601,"intent_order_count": 3},{"time": 20210602,"intent_order_count": 2},{"time": 20210605,"intent_order_count": 20},{"time": 20210606,"intent_order_count": 1},{"time": 20210611,"intent_order_count": 15}]
}PUT index_personal/_doc/2
{"user_id": "2","gender": "female"
}PUT index_personal/_doc/3
{"user_id": "3","gender": "male","time_label": [{"time": 20210102,"intent_order_count": 12},{"time": 20210202,"intent_order_count": 33}]
}
问题扩展解释:
1、"intent_order_count"代表:是订单数,不过都可以抽象成这个用户某个时间买了几个。
比如第三条数据,表示用户编号为 3 的用户,是男性用户,曾经在 20210102 时有12个意向订单(跟订单一个意思),在 20210202 有 33 个意向订单,
2、每个用户除了性别还有很多属性,篇幅受限,没有列出。
问题来源:https://t.zsxq.com/FmEeaIY
2、数据建模探讨
2.1 原问题 Nested 模型
原有数据,以 Nested 建模,存储结构如下:
user_id | gender | time_label {time:intent_order_count} |
---|---|---|
1 | male | [ {20210601:3} {20210602:2}{20210605:20}{20210606:1}{20210611:15}] |
2 | female | |
3 | male | { 20210102:12}{20210202:33} |
以上表示并不严谨,仅是为了更直观的阐述问题。
2.2 宽表建模方案
拿到问题后,我的第一反应:建模可能有问题。
第一:time 存储的是日期,应该是日期类型:date。
第二:宽表拉平存储是不是更好?!也就是说:针对:“user_id” 的用户,一个时间数据,对应一个 document 文档。
原有的 nested 结构,改成如下的一条条的记录,也就是“宽表”,类似简化存储如下:
user_id | gender | time | intent_order_count |
---|---|---|---|
1 | male | 20210601 | 3 |
1 | male | 20210602 | 2 |
1 | male | 20210605 | 20 |
1 | male | 20210606 | 1 |
1 | male | 20210611 | 15 |
2 | female | ||
3 | male | 20210102 | 12 |
3 | male | 20210202 | 33 |
“宽表”是典型的以空间换时间的方案,我们肉眼看到的:对于 user_id=1 的 用户,user_id, gender 信息会存储 N 份(每多一次 time,就多存储一次)。
如前所述,每个用户除了性别还有很多属性,也就是属性非常多的话,会产生大量的冗余存储。
宽表方案优缺点如下:
优点:更利用用户理解,写入和更新非常方便且效率高。
缺点:存在大量冗余存储,耗费空间大。
针对“宽表”方案,问题提出者球友的反馈如下:
“这确实也是个思路。但是我的这个场景下,每个用户除了性别还有很多属性,这样会每天都会产生大量的冗余数据。
是否有办法将一个用户的时间信息聚集到一个文档下,然后也能够查询,对查询效率要求不高。”
所以,还得从 Nested 建模角度基础上,考虑如何实现查询?
2.3 Nested 建模方案
原有建模问题无大碍,只需将:time 字段由 long 类型改为 date 类型,其他保持不变。
# 新的 Mapping 结构(微调)
PUT index_personal_02
{"mappings": {"properties": {"time_label": {"type": "nested","properties": {"intent_order_count": {"type": "long"},"time": {"type": "date"}}},"user_id": {"type": "keyword"},"gender": {"type": "keyword"}}}
}# 还是原来的构造数据,改成bulk,占据行数更少PUT index_personal_02/_bulk
{"index":{"_id":1}}
{"user_id":"1","gender":"male","time_label":[{"time":20210601,"intent_order_count":3},{"time":20210602,"intent_order_count":2},{"time":20210605,"intent_order_count":20},{"time":20210606,"intent_order_count":1},{"time":20210611,"intent_order_count":15}]}
{"index":{"_id":2}}
{"user_id":"2","gender":"female"}
{"index":{"_id":3}}
{"user_id":"3","gender":"male","time_label":[{"time":20210102,"intent_order_count":12},{"time":20210202,"intent_order_count":33}]}
良好的数据建模就好比盖大楼的地基,地基自然是越稳、越实、越牢靠越好!
3、查询方案拆解
3.1 分步骤拆解用户查询需求
问题拆解成如下几个部分:
3.1.1 筛选出在20210510~20210610
铭毅拆解:这是个范围查询,range query 搞定。
DSL 写法如下:
{"nested": {"path": "time_label","query": {"bool": {"must": [{"range": {"time_label.time": {"gte": 20210510,"lte": 20210601}}}]}}}}
正常写 Query 不会涉及 Nested,只有涉及 Nested 数据类型,才必须在检索的前半部分加上 Nested 声明,其目的无非告诉 Elasticsearch 后台,这是针对 Nested 类型的检索。
Path 指定的Nested 最外层,在本文指定的是:time_label。
3.1.2 意向订单数总和为26的男性用户
铭毅拆解:
关于男性用户,这里可以基于性别检索做过滤。
DSL 写法如下:
{"term": {"gender": {"value": "male"}}
}
关于意向订单:对于 user_id = 1 的用户,意向订单总数就等于 3 + 2 + 20 + 1 + 15 = 41。
要实现类似的求和,得需要借助 sum Metric 指标聚合实现。
sum Metric 聚合的前提是:针对某一特定用户形成一个结果,所以其外层是基于用户维度(本文使用:user_id)层面的terms聚合。
为了显示出除了聚合结果之外的其他属性列,需要借助 top_hits 的 _source 中的 include 实现。
DSL 写法大致如下:
"aggs": {"user_id_aggs": {"terms": {"field": "user_id"},"aggs": {"top_sales_hits": {"top_hits": {"_source": {"includes": ["user_id","gender"]}}},"resellers": {"nested": {"path": "time_label"},"aggs": {"sum_count": {"sum": {"field": "time_label.intent_order_count"}}}}
如上:
最外层 terms 聚合:是基于 user_id 的分桶聚合,每个 user_id 的结果聚成一桶。
内层的聚合包含两个,两个是平级的。
其一:top_hits 指标聚合,用于显示聚合结果之外的字段。
其二:sum 指标聚合,用于对“time_label.intent_order_count”统计结果求和。
除了上面的两层聚合,又涉及总和结果和 26 进行比较,所以要基于聚合的聚合,也就是子聚合的实现。
DSL 写法如下:
"count_bucket_filter": {"bucket_selector": {"buckets_path": {"totalcount": "resellers.sum_count"},"script": "params.totalcount >= 26"}}
文中给的实际例子没有满足 26 的文档,所以,这里为了直观显示结果,使用了 >= 26 实现。
3.1.3 应该怎么写dsl语句?
铭毅拆解:
基于上面几个步骤整合到一起,即可实现。
查询 DSL ——即用户最终期望。查询 DSL 就类似“图纸”、“导航”或“路径”,给出了达到给定目的的可行性路径,后面无非就是:java 或者 Python 代码的“堆砌”实现。
3.2 最终 DSL
感谢读者【深红色水杯】2021-07-21 16:45提供的优化DSL建议,已参考修复如下(加了一层aggs 时间过滤)
GET index_personal_02/_search
{"size": 0,"query": {"bool": {"must": [{"nested": {"path": "time_label","query": {"bool": {"must": [{"range": {"time_label.time": {"gte": 20210605,"lte": 20210610}}}]}}}},{"term": {"gender": {"value": "male"}}}]}},"aggs": {"user_id_aggs": {"terms": {"field": "user_id"},"aggs": {"top_sales_hits": {"top_hits": {"_source": {"includes": ["user_id","gender"]}}},"time_aggs": {"nested": {"path": "time_label"},"aggs": {"filter_aggs": {"filter": {"range": {"time_label.time": {"gte": 20210605,"lte": 20210610}}},"aggs": {"sum_aggs": {"sum": {"field": "time_label.intent_order_count"}}}}}},"count_bucket_filter": {"bucket_selector": {"buckets_path": {"totalcount": "time_aggs>filter_aggs.sum_aggs"},"script": "params.totalcount >= 20"}}}}}
}
要强调的点是:
第一:涉及 Nested 的 query 检索 以及 aggs 聚合,都需要明确指定 Nested Path。
第二:复杂检索和聚合出错多数是:子聚合的位置放的不对、后括号和前括弧不匹配等,需要多在 Kibana 测试验证。
第三:Kibana 的一键 DSL 美化快捷键:“ctrl + i” 要掌握和灵活使用。
相信经过上面的拆解,这个相对“复杂”的 DSL 会变得非但不那么“复杂”,反而非常容易读懂。
3.3 查询后结果
"aggregations" : {"user_id_aggs" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 0,"buckets" : [{"key" : "1","doc_count" : 1,"time_aggs" : {"doc_count" : 5,"filter_aggs" : {"doc_count" : 2,"sum_aggs" : {"value" : 21.0}}},"top_sales_hits" : {"hits" : {"total" : {"value" : 1,"relation" : "eq"},"max_score" : 1.4700036,"hits" : [{"_index" : "index_personal_02","_type" : "_doc","_id" : "1","_score" : 1.4700036,"_source" : {"gender" : "male","user_id" : "1"}}]}}}]}}
}
由于检索 size = 0,所以,只返回了聚合结果,没有返回检索结果。
由于二层聚合设置了 top_hits,所以返回结果里除了sum_count的聚合结果,还包含的其下钻数据字段:“gender”、“user_id” 信息,如果实际业务还有更多需要召回字段,可以一并 include 包含后返回即可。
4、有没有更简单的方案?
第 3 小节的实现是基于聚合,但实际文档是 Nested 类型的,基于 userr_id 聚合显得非常的多余。
这里自然想到,用检索能否实现?
如果简单检索不行,那么脚本检索呢?
4.1 扩展方案 1:脚本检索实战
搞一把试试。
GET index_personal_02/_search
{"query": {"bool": {"must": [{"nested": {"path": "time_label","query": {"bool": {"must": [{"range": {"time_label.time": {"gte": 20210510,"lte": 20210613}}},{"script": {"script": """int sum = 0;for (obj in doc['time_label.intent_order_count']) {sum += obj;} sum >= 10;"""}}]}}}},{"term": {"gender": {"value": "male"}}}]}}
}
如上逻辑看似非常严谨的脚本,实际是行不通的。
sum += obj; 本质上只求了一个值。
Elastic 官方工程师给出了详细的解释:“无法在查询时访问脚本中所有嵌套对象的值。脚本查询一次仅适用于一个嵌套对象。”
详细讨论参见:
https://stackoverflow.com/questions/64140179/elasticsearch-sum-up-nested-object-field
https://discuss.elastic.co/t/help-for-painless-iterate-nested-fields/162394
结论:脚本检索不适用 Nested 嵌套对象求和。
官方推荐用 Ingest pipeline 预处理方式实现,那就再搞一把。
4.2 扩展方案 2:Ingest pipeline 方式实战
4.2.1 步骤 1——设置求和的 pipeline。
sum_pipeline 用途:将 nested 嵌套的 intent_order_count 字段进行求和。
# 设定pipeline,统计计数总和PUT _ingest/pipeline/sum_pipeline
{"processors": [{"script": {"source": """ctx.sum_count = ctx.time_label.stream().mapToInt(thing -> thing.intent_order_count).sum()"""}}]
}
4.2.2 步骤 2——结合 pipeline 更新数据
注意一下:nested 添加数据需要借助 script 实现,不能直接指定 id 插入。
若指定 id 插入数据会覆盖掉之前的数据。
# 新插入数据
POST index_personal_02/_update_by_query?pipeline=sum_pipeline
{"query":{"term": {"user_id": {"value": "1"}}},"script": {"source": "ctx._source.time_label.add(params.newlabel)","params": {"newlabel": {"time": 20210702,"intent_order_count": 88}}}
}
4.2.3 步骤 3——结合文章开头要求进行检索
借助 pipeline 新增的字段 sum_count 可以检索条件之一。
# 检索结果
GET index_personal_02/_search
{"query": {"bool": {"must": [{"nested": {"path": "time_label","query": {"bool": {"must": [{"range": {"time_label.time": {"gte": 20210510,"lte": 20210601}}}]}}}},{"term": {"gender": {"value": "male"}}},{"range": {"sum_count": {"gte": 26}}}]}}
}
Ingest pipeline 方案小结:
通过预处理管道新增字段,以空间换时间。
新增的字段作为检索的条件之一,不再需要聚合。
5、小结
分解是计算思维的核心思想之一,“大事化小,逐个击破”。本文的拆解思路也是基于分解的思想一步步拆解。
本文针对线上问题,抛转引玉,给出了方案拆解和完整的步骤实现。
共探索出两种可行的方案:
方案一:聚合实现。
方案一本质:两重嵌套聚合(terms分桶 + 分桶内 sum 指标聚合)+ 子聚合(基于聚合的聚合 bucket_selector)实现。
方案二:预处理管道 pipeline 实现。
方案二本质:新增求和字段,以空间换时间。
实战环境类似本文问题,铭毅推荐使用方案二。
细节问题待进一步结合线上需求进行扩展修改 DSL。
欢迎就问题及方案进行留言,说一下您的思考和思路反馈。
https://discuss.elastic.co/t/script-processor-ingest-pipelines-on-nested-fields/172092/2
推荐
如何系统的学习 Elasticsearch ?
全网首发!《 Elasticsearch 最少必要知识教程 V1.0 》低调发布
从实战中来,到实战中去——Elasticsearch 技能更快提升方法论
刻意练习 Elasticsearch 10000 个小时,鬼知道经历了什么?!
干货 | Elasticsearch Nested类型深入详解
基于儿童积木玩具图解 Elasticsearch 聚合
干货 | 通透理解Elasticsearch聚合
Elasticsearch 如何实现查询/聚合不区分大小写?
更短时间更快习得更多干货!
中国50%+Elastic认证工程师出自于此!
比同事抢先一步!
干货 | 拆解一个 Elasticsearch Nested 类型复杂查询问题相关推荐
- 转:elasticsearch nested嵌套查询
转自: [弄nèng - Elasticsearch]DSL入门篇(七)-- Nested类型查询,聚合_司马缸砸缸了-CSDN博客文章目录1. nested query2. nested 对象聚合项 ...
- Elasticsearch(es) 查询语句语法详解
Elasticsearch 查询语句采用基于 RESTful 风格的接口封装成 JSON 格式的对象,称之为 Query DSL.Elasticsearch 查询分类大致分为全文查询.词项查询.复合查 ...
- ElasticSearch使用(嵌套查询、嵌套高亮)
ElasticSearch使用(嵌套查询.嵌套高亮) 嵌套查询 bool 查询 must.should关系 1.只有must 2.只有should 3.must和should同时存在 4.怎样设置sh ...
- 如何快速部署一个Elasticsearch集群?
作者:无敌码农 来源:无敌码农 今天的文章给大家介绍下Elasticsearch这一目前在"搜索"和"分析"领域使用十分广泛的技术组件.并演示如何快速构建一个E ...
- 【Elasticsearch】索引和查询性能调优的21条建议-以及调优参数
文章目录 1.概述 1.Elasticsearch部署建议 1.1. 选择合理的硬件配置:尽可能使用 SSD 1.2. 给JVM配置机器一半的内存,但是不建议超过32G 1.3. 规模较大的集群配置专 ...
- 初识ElasticSearch(2) -文档查询之match查询 | 分词器
1. 分词器: 2. match查询: 2.1. 数据准备 - 创建带分词器的索引映射 2.2. 数据准备 - 添加文档 2.3. 数据准备 - 查看文本分词 2.4. 查询 - 映射有分词器的字段查 ...
- ElasticSearch DSL语言高级查询+SpringBoot
1 环境准备 1.1 Es数据准备 https://gitee.com/zhurongsheng/elasticsearch-data/blob/master/es.data 描述: 执行后查看结果. ...
- ElasticSearch——手写一个ElasticSearch分词器(附源码)
1. 分词器插件 ElasticSearch提供了对文本内容进行分词的插件系统,对于不同的语言的文字分词器,规则一般是不一样的,而ElasticSearch提供的插件机制可以很好的集成各语种的分词器. ...
- 跟乐乐学ES!(三)ElasticSearch 批量操作与高级查询
上一篇文章:跟乐乐学ES!(二)ElasticSearch基础. 下一篇文章:跟乐乐学ES!(四) java中ElasticSearch客户端的使用. 批量操作 有些增删改查操作是可以进行批量操作的, ...
- 数据库与elasticSearch,大数据查询性能PK
每天早上七点三十,准时推送干货 一.介绍 在这篇文章中 利用springboot+elasticSearch,实现数据高效搜索,实战开发,我们介绍了 SpringBoot 整合 ElasticSear ...
最新文章
- 关于windowsx.h的介绍
- python利用opencv自带的颜色查找表(LUT)进行色彩风格变换
- 数据挖掘之聚类k-means
- Day 4 - PB级规模数据的Elasticsearch分库分表实践
- [Ljava.lang.String和java.lang.String区别
- requests库详解
- Codeforces Round #419 (Div. 2)
- BZOJ 1898: [Zjoi2005]Swamp 沼泽鳄鱼 [矩阵乘法]
- Android常见命令
- Lettuce替换Jedis操作Redis缓存
- 腾讯微博开放平台授权教程(一)
- 通信技术专业技术人员考试 动力与环境_建筑信息模型专业技术人员等级认定培训考试项目介绍...
- 一只测试喵的重新启航
- 使用opencv进行车牌提取及识别
- 推荐系统三十六式:矩阵分解 总结
- 从零开始学习oracle(2) oracle11g的远程链接和数据库调试
- PLC程序的调试方法及步骤
- MBO目标管理与SMART原则
- 百度云上迎新春,开心过大年
- 互联网、因特网、万维网、局域网、广域网的区别
热门文章
- Learning Convolutional Neural Network for Graphs
- 4核处理器_苹果电脑便宜卖!4核i5处理器,480G固态硬盘,带刻录,13.4寸,双系统...
- 《日瓦戈医生》读后感
- iov_iter结构体
- C++实现二叉树同构
- Dx11--用dx11绘制棱台,并用键盘和鼠标进行旋转缩放操作
- 关于hping打流测试工具
- win10系统oracle删除用户,win10 清除个人数据库
- 赏析角度有哪些_几种分析的角度
- 去哪儿2015校园招聘产品经理面试题