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. ubuntu 网卡双网口 配置_Ubuntu 18.04 设置多网卡多端口聚合
  2. ediplus 复制编辑一列_vi编辑器的使用详解
  3. 子窗体中组合框联动_一张表实现组合框联动
  4. php变量教学,PHP变量详解
  5. JavaScript浏览器window对象→简介、消息对话框、计时器、history、打开新窗口及模式showModalDialog、location、navigator、screen、窗口位置尺寸
  6. java学生管理系统
  7. ActiveMQ使用教程
  8. java中前加加++和后加加++的详解
  9. JS中判断某个字符串/数组中是否包含某个值
  10. CSS3旋转跳跃的立方体
  11. 163vip邮箱登录,163邮箱怎么登陆?如何登录163vip邮箱?
  12. 路由器交换机设备管理
  13. WordPress网站为什么及如何使用CDN加速访问
  14. 日本价值链促进会(IVI)秘书长西冈靖之:日本工业互联网发展情况
  15. 嵌入式系统开发,不用Linux的理由 !
  16. Android3d结构光,安卓阵营独一份!OPPO完成3D结构光技术研发,某果开始慌了
  17. geemap的详细安装步骤及环境配置
  18. 前端base64发给后端图片太大怎么办
  19. 我的世界android制作教程,我的世界怎么去月球-月球传送门制作方法
  20. 电赛 | 单相用电器分析监测装置(省电赛一等奖作品)

热门文章

  1. 瑞尔森大学的计算机科学专业,瑞尔森大学专业介绍
  2. c语言错误 cout不明确,C++ error: cout 不明确的符号
  3. 事业单位计算机类岗位考试试题,国家机关(事业单位)计算机岗位考试试题
  4. 《弃子长安》第三章 月下魅影
  5. 密立根油滴实验的c语言程序,密立根油滴实验数据处理程序c++
  6. 揭开docker的神秘面纱?镜像制作
  7. 数据预处理(一):无量纲化
  8. 电子邮件注册网站哪个好:四大邮箱客户端的对比
  9. UNI-APP,动态设置view的背景图片
  10. 微分中值定理定义及几何意义