Elasticsearch 组合聚集(Composite aggregation)实现交叉分析

实际分析应用中经常遇到需要交叉表的情况,它能够从不同维度组合分析业务。关系型数据库一般通过 group byPivot实现,很幸运,Elasticsearch也有类似功能。本文带你了解强大的组合分析,其中示例大多数来自官网。

1. 认识组合聚集

组合聚集(Composite aggregation) —— 从不同来源创建组合分组,属于多组聚集分析。与其他的多组聚集分析不同,它可以实现多聚集结果进行分页,其提供了对结果进行流化处理,类似于对文档实现滚动操作。

组合分组是从每个文档中抽取特定值进行组合,每个组合作为一个分组。举例:

{"keyword": ["foo", "bar"],"number": [23, 65, 76]
}

如果把文档中的 keywordnumber 作为值来源进行分组,结果为:

{ "keyword": "foo", "number": 23 }
{ "keyword": "foo", "number": 65 }
{ "keyword": "foo", "number": 76 }
{ "keyword": "bar", "number": 23 }
{ "keyword": "bar", "number": 65 }
{ "keyword": "bar", "number": 76 }

2. 值来源

sources 参数控制分组的数据来源,sources中定义的顺序很重要,因为它影响结果的返回顺序。每个来源的名称必须唯一,主要有三中类型的值来源。

2.1 关键词Terms

terms值来源相当于简单的 terms聚集,和聚集类似它的值可以从字段或脚本中抽取。举例:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "product": { "terms": { "field": "product" } } }]}}}
}

当然也可以使用脚本创建组合聚集的值来源:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{"product": {"terms": {"script": {"source": "doc['product'].value","lang": "painless"}}}}]}}}
}

2.2 直方图 Histogram

直方图可以为日期直方图和数值直方图。

2.2.1 数值直方图

直方图值来源可应用与数值类型构建固定大小的间隔,interval参数定义数据如何分组转换。举例,如果 interval设置为 5 ,则任何数值将转换至其最近的间隔分组,101 会 被转为 100,因为其在 100105之间的分组。举例:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "histo": { "histogram": { "field": "price", "interval": 5 } } }]}}}
}

同样也可以使用脚本实现:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{"histo": {"histogram": {"interval": 5,"script": {"source": "doc['price'].value","lang": "painless"}}}}]}}}
}

2.2.2 日期直方图

date_histogram 与上面 histogram类似,处理值来源为日期类型:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } }]}}}
}

上面示例使用每天作为间隔转换字段 timestamp的值至最近的间隔。其他间隔参数还有 year, quarter, month, week, day, hour, minute, second。当然可以通过缩写进行表示,但不支持小数(我们可以使用90m代替 1.5h)。

日期格式

日期数据是使用64位数值进行表示,即从epoch时间以来的毫秒数。默认该数值返回作为分组的 key。我们可以通过 format参数制定日期格式:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{"date": {"date_histogram": {"field": "timestamp","calendar_interval": "1d","format": "yyyy-MM-dd"         }}}]}}}
}

时区

Elasticsearch 使用UTC格式存储日期时间,缺省所有的分组和凑整也按照UTC中完成。使用time_zone参数指示分组应该使用不同的时区。可以通过 ISO 8601 UTC (如:+01:00or-08:00),也可以使用时区ID, 如:America/Los_Angeles

偏移量

使用 offset 参数可以改变每个分组的初始值,通过制定整数 (+) 或负数 (-) 偏移量,如 1h 表示1小时, 1d 表示1天。举例,使用 day作为时间间隔,每个分组时间从午夜到午夜。设置 offset 为 +6h则改变每个分组跨度为早晨6点到下一个早晨6点:

PUT my-index-000001/_doc/1?refresh
{"date": "2015-10-01T05:30:00Z"
}PUT my-index-000001/_doc/2?refresh
{"date": "2015-10-01T06:30:00Z"
}GET my-index-000001/_search?size=0
{"aggs": {"my_buckets": {"composite" : {"sources" : [{"date": {"date_histogram" : {"field": "date","calendar_interval": "day","offset": "+6h","format": "iso8601"}}}]}}}
}

如果采用默认偏移量,则返回一个分组结果;上面的示例返回结果从6am开始,有两个分组结果:

{..."aggregations": {"my_buckets": {"after_key": { "date": "2015-10-01T06:00:00.000Z" },"buckets": [{"key": { "date": "2015-09-30T06:00:00.000Z" },"doc_count": 1},{"key": { "date": "2015-10-01T06:00:00.000Z" },"doc_count": 1}]}}
}

注:每个桶的开始偏移量是在time_zone调整之后计算的。

2.3 地理块

实际应用中暂时用不到,为了文档完整性列出标题,读者有兴趣可以参阅

2.4 组合不同来源

sources参数接收值来源数组。其可以混合不同的值来源创建组合分组,举例:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } },{ "product": { "terms": { "field": "product" } } }]}}}
}

上面组合分组创建了两个值来源,date_histogramterms ,每个分组有两个值组成,分别来自不同聚集值。任何类型的组合都可以,并且数组顺序在组合分组中保持不变。

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "shop": { "terms": { "field": "shop" } } },{ "product": { "terms": { "field": "product" } } },{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } }]}}}
}

3.6 对应Java Api

通过API增加不同来源:

List<CompositeValuesSourceBuilder<?>> sources = new ArrayList<>();
sources.add(new TermsValuesSourceBuilder("field1").field("field1"));
sources.add(new TermsValuesSourceBuilder("field2").field("field2"));
sources.add(new TermsValuesSourceBuilder("field3").field("field3"));CompositeAggregationBuilder compositeAggregationBuilder =new CompositeAggregationBuilder("byProductAttributes", sources).size(10000);

设置子聚集:

compositeAggregationBuilder.subAggregation(AggregationBuilders.min("sub-field1").field("sub-field1")).subAggregation(AggregationBuilders.max("sub-field2").field("sub-field2"))
searchSourceBuilder.aggregation(compositeAggregationBuilder);

3. 其他选项

3.1 排序 (sort)

缺省分组时按照自然顺序排序,按照值的升序排列。当有多个值来源时,排序是按照每个值来源排序的,一个分组的第一个值与另一个分组的第一个进行比较,如果两者相等,则根据分组中的下一个值进行比较。这意味着 [foo, 100] 小于 [foobar, 0],因为 foo 小于 foobar。我们可以通过给每个值来源定义排序方向,设置 orderasc 或 desc,举例:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },{ "product": { "terms": { "field": "product", "order": "asc" } } }]}}}
}

上面示例日期直方图采用降序,而term来源分组按照升序排序。

3.2 缺失分组 (missing bucket)

给给定源中文档没有值缺省被忽略,我们也可以通过设置 missing_bucket参数为 true(默认为false),使得响应中包括缺省值文档。

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "product_name": { "terms": { "field": "product", "missing_bucket": true } } }]}}}
}

在上面的示例中,源product_name将为product字段没有值的文档显式设置为nullsort参数指示null值应该排在第一(升序,asc)还是最后(降序,desc)。

3.3 大小 (size)

size 参数可以定义返回多个组合分组。每个组合分组作为一个分组,所以设置为 10则返回前十个分组。缺省值为10。

3.4 分页

如果组合分组数量太大(或未知)而无法在单个响应中返回,则可将检索拆分为多个请求。由于组合分组实际上是平的,所以请求的大小正好是响应中返回的分组数量(假设它们就是要返回分组的大小)。如果要检索所有组合分组,那么最好使用较小的大小(例如100或1000),然后使用after参数检索下一个结果。例如:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"size": 2,"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } },{ "product": { "terms": { "field": "product" } } }]}}}
}

返回结果:

{..."aggregations": {"my_buckets": {"after_key": {"date": 1494288000000,"product": "mad max"},"buckets": [{"key": {"date": 1494201600000,"product": "rocky"},"doc_count": 1},{"key": {"date": 1494288000000,"product": "mad max"},"doc_count": 2}]}}
}

需要达到下一组分组,重新发送请求,使用 after参数,设置为上次返回结果中 after_key的值。举例:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"size": 2,"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },{ "product": { "terms": { "field": "product", "order": "asc" } } }],"after": { "date": 1494288000000, "product": "mad max" } }}}
}

after_key通常是响应中返回的最后一个分组的键,但这并不能保证。始终使用返回的after_key,而不是从结果中删除它。

3.5 子聚集

和其他多分组聚集一样,组合聚集也支持子聚集。可以通过子聚集技术其他分组或分析每个子分组。举例,下面示例技术每个分组的平均值:

GET /_search
{"size": 0,"aggs": {"my_buckets": {"composite": {"sources": [{ "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },{ "product": { "terms": { "field": "product" } } }]},"aggregations": {"the_avg": {"avg": { "field": "price" }}}}}
}

返回结果:

{..."aggregations": {"my_buckets": {"after_key": {"date": 1494201600000,"product": "rocky"},"buckets": [{"key": {"date": 1494460800000,"product": "apocalypse now"},"doc_count": 1,"the_avg": {"value": 10.0}},{"key": {"date": 1494374400000,"product": "mad max"},"doc_count": 1,"the_avg": {"value": 27.0}},{"key": {"date": 1494288000000,"product": "mad max"},"doc_count": 2,"the_avg": {"value": 22.5}},{"key": {"date": 1494201600000,"product": "rocky"},"doc_count": 1,"the_avg": {"value": 10.0}}]}}
}

注意:当前组合分组不支持管道聚集 (pipeline聚集)。

4. 组合分组应用示例

通过上面学习,我了解了组合分组是Elasticsearch中非常强大分析特性及分页能力,下面通过示例进行实战。

需求:一家在线匹萨公司的业务数据分析。主要实现三个方面功能:对结果进行分页展示,对与每个地址创建日期直方图分析,每天匹萨的平均数量。

4.1 数据准备

首先定义mapping,为了简化,这里直接把地址设置为一个字段:

PUT /pizza
{"mappings" : {"properties": {"full_address": {"type": "keyword"},"order" : {"type" : "text"},"num_pizzas" : {"type" : "integer"},"timestamp" : {"type" : "date"}}}
}

插入示例数据:

POST /pizza/_bulk
{ "index": { "_id": 1 }}
{"full_address" : "355 First St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 2, "timestamp": "2018-04-10T12:25" }
{ "index": { "_id": 2 }}
{"full_address" : "961 Union St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 3, "timestamp": "2018-04-11T12:25" }
{ "index": { "_id": 3 }}
{"full_address" : "123 Baker St, San Francisco, CA", "order" : "vegan", "num_pizzas" : 1, "timestamp": "2018-04-18T12:25" }
{ "index": { "_id": 4 }}
{"full_address" : "1700 Powell St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 5, "timestamp": "2018-04-18T12:25" }
{ "index": { "_id": 5 }}
{"full_address" : "900 Union St, San Francisco, CA", "order" : "pepperoni", "num_pizzas" : 4, "timestamp": "2018-04-18T12:25" }
{ "index": { "_id": 6 }}
{"full_address" : "355 First St, San Francisco, CA", "order" : "pepperoni", "num_pizzas" : 3, "timestamp": "2018-04-10T12:25" }
{ "index": { "_id": 7 }}
{"full_address" : "961 Union St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 1, "timestamp": "2018-04-12T12:25" }
{ "index": { "_id": 8 }}
{"full_address" : "100 First St, San Francisco, CA", "order" : "pepperoni", "num_pizzas" : 3, "timestamp": "2018-04-11T12:25" }
{ "index": { "_id": 9 }}
{"full_address" : "101 First St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 5, "timestamp": "2018-04-11T12:25" }
{ "index": { "_id": 10 }}
{"full_address" : "355 First St, San Francisco, CA", "order" : "cheese", "num_pizzas" : 10, "timestamp": "2018-04-10T12:25" }
{ "index": { "_id": 11 }}
{"full_address" : "100 First St, San Francisco, CA", "order" : "pepperoni", "num_pizzas" : 4, "timestamp": "2018-04-11T14:25" }

4.2 分页查询

实际应用中数据量非常大,如果需要查看中间页数据,其他聚集不方便实现。

GET /pizza/_search
{"size": 0,"track_total_hits": false,"aggs": {"group_by_deliveries": {"composite": {"size": 3,"sources": [{"by_address": {"terms": {"field": "full_address"}}}]}}}
}

上面结果展示第一页,注意我们设置size参数为3:

Address Deliveries
100 First St, San Francisco, CA 2
101 First St, San Francisco, CA 1
123 Baker St, San Francisco, CA 1

现在我们需要展示下一页结果。既然 123 Baker St, San Francisco, CA 在前面查询中是最后一条结果,因此现在查询中的参数 after设置为该值:

GET /pizza/_search
{"size" : 0,"track_total_hits" : false,"aggs" : {"group_by_deliveries": {"composite" : {"after" : { "by_address" : "123 Baker St, San Francisco, CA" },"size": 3,"sources" : [{ "by_address": { "terms" : { "field": "full_address" } } }]}} }
}

注,我们还可以设置 track_total_hits 参数的值为 false,它让Elasticsearch 在查询到足够的分组后尽快结束查询。

4.3 对每个地址创建日期直方图

这里我们设置多个源,分析每个地址每天的订单量:

GET /pizza/_search
{"size": 0,"track_total_hits": false,"aggs": {"group_by_deliveries": {"composite": {"size": 3,"sources": [{"by_address": {"terms": {"field": "full_address"}}},{"histogram": {"date_histogram": {"field": "timestamp","calendar_interval": "1d","format": "yyyy-MM-dd"}}}]}}}
}

返回结果:

{"took" : 6,"timed_out" : false,"_shards" : {"total" : 1,"successful" : 1,"skipped" : 0,"failed" : 0},"hits" : {"max_score" : null,"hits" : [ ]},"aggregations" : {"group_by_deliveries" : {"after_key" : {"by_address" : "123 Baker St, San Francisco, CA","histogram" : "2018-04-18"},"buckets" : [{"key" : {"by_address" : "100 First St, San Francisco, CA","histogram" : "2018-04-11"},"doc_count" : 2},{"key" : {"by_address" : "101 First St, San Francisco, CA","histogram" : "2018-04-11"},"doc_count" : 1},{"key" : {"by_address" : "123 Baker St, San Francisco, CA","histogram" : "2018-04-18"},"doc_count" : 1}]}}
}

上面示例给每个地址创建了一个直方图分组。我们还可以使用 after参数,查询下一页结果集:

GET /pizza/_search
{"size": 0,"track_total_hits": false,"aggs": {"group_by_deliveries": {"composite": {"size": 3,"after": {"by_address": "123 Baker St, San Francisco, CA","histogram": "2018-04-18"},"sources": [{"by_address": {"terms": {"field": "full_address"}}},{"histogram": {"date_histogram": {"field": "timestamp","calendar_interval": "1d","format": "yyyy-MM-dd"}}}]}}}
}

4.4 每天平均匹萨销售数量

为了分析每天每个地址的销售的匹萨数量,我们可以给组合聚集增加子聚集计算平均值:

GET /pizza/_search?size=0
{"track_total_hits" : false,"aggs" : {"group_by_deliveries": {"composite" : {"size": 3,"sources" : [{ "by_address": { "terms" : { "field": "full_address" } } },{ "histogram": { "date_histogram" : { "field": "timestamp", "calendar_interval": "1d" } } }]},"aggregations": {"avg_pizzas_per_day": {"avg": { "field": "num_pizzas" }}}} }
}

返回结果:

{..."aggregations" : {js"group_by_deliveries" : {"after_key" : {"by_address" : "123 Baker St, San Francisco, CA","histogram" : 1524009600000},"buckets" : [{"key" : {"by_address" : "100 First St, San Francisco, CA","histogram" : 1523404800000},"doc_count" : 2,"avg_pizzas_per_day" : {"value" : 3.5}},{"key" : {"by_address" : "101 First St, San Francisco, CA","histogram" : 1523404800000},"doc_count" : 1,"avg_pizzas_per_day" : {"value" : 5.0}},{"key" : {"by_address" : "123 Baker St, San Francisco, CA","histogram" : 1524009600000},"doc_count" : 1,"avg_pizzas_per_day" : {"value" : 1.0}}]}}
}

5. 总结

组合聚集是Elasticsearch中强大特性,可以实现对大量聚集结果进行分页,使得响应时间变得可确定。通过组合不同来源,实现对数据从不同维度进行组合分析。

Elasticsearch 组合聚集(Composite aggregation)实现交叉分析相关推荐

  1. Elasticsearch 5: 聚集查询

    目录 1. 聚集查询 2. 指标聚集 2.1 平均值聚集 2.1.1 avg 聚集 2.2 计数聚集与极值聚集 2.2.1 计数聚集 2.2.2 极值聚集 2.3 统计聚集 2.3.1 stats 聚 ...

  2. Elasticsearch:用于内容丰富的文本分析

    每个文本搜索解决方案都与其提供的文本分析功能一样强大. Lucene是这样的开源信息检索库,提供了许多文本分析的可能性. 在本文中,我们将介绍ElasticSearch提供的一些主要文本分析功能,这些 ...

  3. 【设计模式】组合模式 Composite Pattern

    树形结构是软件行业很常见的一种结构,几乎随处可见,  比如: HTML 页面中的DOM,产品的分类,通常一些应用或网站的菜单,Windows Form 中的控件继承关系,Android中的View继承 ...

  4. 26、python数据表透视分析、交叉分析、实现透视表功能

    交叉分析:通常用于分析两个或两个以上,分组变量之间的变量关系,以及交叉表形式进行变量间关系的对比分析 定量.定量分组交叉 定量.定性分析交叉 定性.定性分组交叉 1  交叉统计函数 pivot_tab ...

  5. 组合模式-Composite Pattern

    目录 组合模式的定义与特点 组合模式的结构与实现 组合模式的应用实例 组合模式的应用场景 组合模式的扩展 树形结构在软件中随处可见,例如操作系统中的目录结构.应用软件中的菜单.办公系统中的公司组织结构 ...

  6. Java设计模式 —— 组合模式(Composite)

    Java设计模式 -- 组合模式(Composite) 定义 Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性. ...

  7. 设计模式(17):结构型-组合模式(Composite)(2)

    设计模式(Design pattern) 是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计模式 ...

  8. 【设计模式自习室】结构型:组合模式 Composite

    前言 <设计模式自习室>系列,顾名思义,本系列文章带你温习常见的设计模式.主要内容有: 该模式的介绍,包括: 引子.意图(大白话解释) 类图.时序图(理论规范) 该模式的代码示例:熟悉该模 ...

  9. [设计模式] 8 组合模式 Composite

    DP书上给出的定义:将对象组合成树形结构以表示"部分-整体"的层次结构.组合使得用户对单个对象和组合对象的使用具有一致性.注意两个字"树形".这种树形结构在现实 ...

最新文章

  1. 【CF应用开发大赛】制造过程能力Cpk计算器
  2. python基础教程是什么语言-0编程基础,什么语言也没学过,请问学Python怎样入门?...
  3. 《python核心编程第二版》第5章习题
  4. c++STL之vector简易使用
  5. osgi框架和spring区别_最全153道Spring全家桶面试题,你都知道哪些?(含答案解析)...
  6. 1分钟看懂:java 项目中 VO 、DTO、Entity,各自是在什么情况下应用的
  7. 基于Docker的Mysql主从复制搭建_mysql5.7.x
  8. Java FileOutputStream
  9. Android 屏幕实现水龙头事件
  10. 记一次tomcat故障排查(转)
  11. C语言getchar()=='\n'的使用,对输入的任意个字符操作
  12. 试图执行系统不支持的操作,问题
  13. mysql怎么实现表的复制粘贴_如何对MySQL数据表进行复制、表结构复制
  14. 2022-2028全球与中国电热毯市场现状及未来发展趋势
  15. Sqlite3内存数据库
  16. 【工大SCIR】AAAI20 基于反向翻译和元学习的低资源神经语义解析
  17. AI+智能服务机器人应用基础【实践报告】
  18. wx小程序笔记(2)
  19. ZOJ 3380 Patchouli's Spell Cards(DP,大数)
  20. 从面试官的角度,来聊聊培训班对程序员的帮助!

热门文章

  1. 将数字转换成科学计数法
  2. ASCII表完整版(包含16进制对应表)——看看16进制与10进制的转化
  3. win10内置linux读取u盘raw,U盘或磁盘分区RAW格式恢复方案
  4. 谷歌的无痕模式有什么好处_为什么Google的新搜索结果设计是黑暗的模式
  5. 中国没有乔布斯,美国没有史玉柱
  6. matlab矩阵运算中“.”的使用
  7. HTTP状态 500 - 内部服务器错误java.lang.NullPointerException
  8. IPhone免越狱安装IPA软件
  9. 十年Java架构师分享
  10. springCloud笔记——微服务介绍