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 类型复杂查询问题相关推荐

  1. 转:elasticsearch nested嵌套查询

    转自: [弄nèng - Elasticsearch]DSL入门篇(七)-- Nested类型查询,聚合_司马缸砸缸了-CSDN博客文章目录1. nested query2. nested 对象聚合项 ...

  2. Elasticsearch(es) 查询语句语法详解

    Elasticsearch 查询语句采用基于 RESTful 风格的接口封装成 JSON 格式的对象,称之为 Query DSL.Elasticsearch 查询分类大致分为全文查询.词项查询.复合查 ...

  3. ElasticSearch使用(嵌套查询、嵌套高亮)

    ElasticSearch使用(嵌套查询.嵌套高亮) 嵌套查询 bool 查询 must.should关系 1.只有must 2.只有should 3.must和should同时存在 4.怎样设置sh ...

  4. 如何快速部署一个Elasticsearch集群?

    作者:无敌码农 来源:无敌码农 今天的文章给大家介绍下Elasticsearch这一目前在"搜索"和"分析"领域使用十分广泛的技术组件.并演示如何快速构建一个E ...

  5. 【Elasticsearch】索引和查询性能调优的21条建议-以及调优参数

    文章目录 1.概述 1.Elasticsearch部署建议 1.1. 选择合理的硬件配置:尽可能使用 SSD 1.2. 给JVM配置机器一半的内存,但是不建议超过32G 1.3. 规模较大的集群配置专 ...

  6. 初识ElasticSearch(2) -文档查询之match查询 | 分词器

    1. 分词器: 2. match查询: 2.1. 数据准备 - 创建带分词器的索引映射 2.2. 数据准备 - 添加文档 2.3. 数据准备 - 查看文本分词 2.4. 查询 - 映射有分词器的字段查 ...

  7. ElasticSearch DSL语言高级查询+SpringBoot

    1 环境准备 1.1 Es数据准备 https://gitee.com/zhurongsheng/elasticsearch-data/blob/master/es.data 描述: 执行后查看结果. ...

  8. ElasticSearch——手写一个ElasticSearch分词器(附源码)

    1. 分词器插件 ElasticSearch提供了对文本内容进行分词的插件系统,对于不同的语言的文字分词器,规则一般是不一样的,而ElasticSearch提供的插件机制可以很好的集成各语种的分词器. ...

  9. 跟乐乐学ES!(三)ElasticSearch 批量操作与高级查询

    上一篇文章:跟乐乐学ES!(二)ElasticSearch基础. 下一篇文章:跟乐乐学ES!(四) java中ElasticSearch客户端的使用. 批量操作 有些增删改查操作是可以进行批量操作的, ...

  10. 数据库与elasticSearch,大数据查询性能PK

    每天早上七点三十,准时推送干货 一.介绍 在这篇文章中 利用springboot+elasticSearch,实现数据高效搜索,实战开发,我们介绍了 SpringBoot 整合 ElasticSear ...

最新文章

  1. 关于windowsx.h的介绍
  2. python利用opencv自带的颜色查找表(LUT)进行色彩风格变换
  3. 数据挖掘之聚类k-means
  4. Day 4 - PB级规模数据的Elasticsearch分库分表实践
  5. [Ljava.lang.String和java.lang.String区别
  6. requests库详解
  7. Codeforces Round #419 (Div. 2)
  8. BZOJ 1898: [Zjoi2005]Swamp 沼泽鳄鱼 [矩阵乘法]
  9. Android常见命令
  10. Lettuce替换Jedis操作Redis缓存
  11. 腾讯微博开放平台授权教程(一)
  12. 通信技术专业技术人员考试 动力与环境_建筑信息模型专业技术人员等级认定培训考试项目介绍...
  13. 一只测试喵的重新启航
  14. 使用opencv进行车牌提取及识别
  15. 推荐系统三十六式:矩阵分解 总结
  16. 从零开始学习oracle(2) oracle11g的远程链接和数据库调试
  17. PLC程序的调试方法及步骤
  18. MBO目标管理与SMART原则
  19. 百度云上迎新春,开心过大年
  20. 互联网、因特网、万维网、局域网、广域网的区别

热门文章

  1. Learning Convolutional Neural Network for Graphs
  2. 4核处理器_苹果电脑便宜卖!4核i5处理器,480G固态硬盘,带刻录,13.4寸,双系统...
  3. 《日瓦戈医生》读后感
  4. iov_iter结构体
  5. C++实现二叉树同构
  6. Dx11--用dx11绘制棱台,并用键盘和鼠标进行旋转缩放操作
  7. 关于hping打流测试工具
  8. win10系统oracle删除用户,win10 清除个人数据库
  9. 赏析角度有哪些_几种分析的角度
  10. 去哪儿2015校园招聘产品经理面试题