前言
本篇内容是es的最后一篇,主要讲解聚合技术,以及与其相关的算法和原理,最后结合实际应用,简单说明了一些常用的数据建模。
一 聚合分析之 bucket(分组)&meteric(统计)
这一节内容主要是介绍下 bucket(分组)的概念 以及 meteric(聚合统计)概念,其实我们做过开发写过sql的就很容易理解了。然后我们结合案例进行练习和体会不同的bucket,以及不同的meteric,强化我们对分组和聚合统计的理解和记忆。
1.1 原理 bucket(分组)与metric(聚合统计)概念理解
bucket 它是指对一组数据进行分组
假设一组数据为:
city name
北京 小李
北京 小王
上海 小张
上海 小丽
上海 小陈
那么基于city划分buckets,划分出来两个bucket,一个是北京bucket,一个是上海bucket
则:
北京bucket:包含了2个人,小李,小王
上海bucket:包含了3个人,小张,小丽,小陈
其实 sql中的分组,就是我们这里的bucket。
metric:对一个数据分组执行的统计
当我们有了一堆bucket之后,就可以对每个bucket中的数据进行聚合分词了,比如说计算一个bucket内所有数据的数量,或者计算一个bucket内所有数据的平均值,最大值,最小值
metric,就是对一个bucket执行的某种聚合分析的操作,比如说求平均值,求最大值,求最小值
1.2 实战 各种bucket(分组)与各种metric(聚合统计)
以一个家电卖场中的电视销售数据为背景,来对各种品牌,各种颜色的电视的销量和销售额,进行各种各样角度的分析
1.2.1 准备初始化数据
PUT /tvs
{
"mappings": {
"properties": {
"price": {
"type": "long"
},
"color": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"sold_date": {
"type": "date"
}
}
}
}
POST /tvs/_bulk
{"index":{}}
{"price":1000,"color":"红色","brand":"长虹","sold_date":"2016-10-28"}
{"index":{}}
{"price":2000,"color":"红色","brand":"长虹","sold_date":"2016-11-05"}
{"index":{}}
{"price":3000,"color":"绿色","brand":"小米","sold_date":"2016-05-18"}
{"index":{}}
{"price":1500,"color":"蓝色","brand":"TCL","sold_date":"2016-07-02"}
{"index":{}}
{"price":1200,"color":"绿色","brand":"TCL","sold_date":"2016-08-19"}
{"index":{}}
{"price":2000,"color":"红色","brand":"长虹","sold_date":"2016-11-05"}
{"index":{}}
{"price":8000,"color":"红色","brand":"三星","sold_date":"2017-01-01"}
{"index":{}}
{"price":2500,"color":"蓝色","brand":"小米","sold_date":"2017-02-12"}
1.2.2 按颜色分组统计电视销量
size:=0只获取聚合结果,而不要执行聚合的原始数据
aggs:固定语法,要对一份数据执行分组聚合操作
popular_colors:就是对每个aggs,都要起一个名字,这个名字是随机的,你随便取什么都ok
terms:根据字段的值进行分组
field:根据指定的字段的值进行分组
GET /tvs/_search
{
"size": 0,
"aggs": {
"popular_colors": {
"terms": {
"field": "color"
}
}
}
}
获取结果
{
"took" : 142,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"popular_colors" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "红色",
"doc_count" : 4
},
{
"key" : "绿色",
"doc_count" : 2
},
{
"key" : "蓝色",
"doc_count" : 2
}
]
}
}
}
- hits.hits:指定size=0,所以hits.hits就是空的,否则会把执行聚合的那些原始数据返回回来
- aggregations:聚合结果
- popular_color:指定的某个聚合的名称
- buckets:指定的field划分出的buckets
- key:每个bucket对应的那个值
- doc_count:每个bucket分组内,有多少个数据 ,每种颜色对应的bucket中的数据的
- 默认的排序规则:按照doc_count降序排序
1.2.3 按颜色分组metric(统计)平均(avg)价格
除了bucket操作,分组,还要对每个bucket执行一个metric聚合统计操作
在一个aggs执行的bucket操作(terms),平级的json结构下,再加一个aggs,这个第二个aggs内部,同样取个名字,执行一个metric操作,avg,对之前的每个bucket中的数据的指定的field,price field,求一个平均值
GET /tvs/_search
{
"size": 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
buckets,除了key和doc_count
avg_price:我们自己取的metric aggs的名字
value:我们的metric计算的结果,每个bucket中的数据的price字段求平均值后的结果
1.2.4 按颜色分组metric(统计)最大(max) 最小(min)价格
max:求一个bucket内,指定field值最大的那个数据
min:求一个bucket内,指定field值最小的那个数据
sum:求一个bucket内,指定field值的总和
一般来说,90%的常见的数据分析的操作,metric,无非就是count,avg,max,min,sum
求总和,就可以拿到一个颜色下的所有电视的销售总额
GET /tvs/_search
{
"size": 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"max_price": {
"max": {
"field": "price"
}
},
"sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
1.2.5 按价格区间(histogram&interval) 统计销售量和销售额
histogram 也是bucket ,他按照某个值指定的interval(步长),划分一个一个的bucket
interval:2000,划分范围,0~2000,2000~4000,4000~6000,6000~8000,8000~10000,buckets
去根据price的值,比如2500,看落在哪个区间内,比如2000~4000,此时就会将这条数据放入2000~4000对应的那个bucket中。
GET /tvs/_search
{
"size": 0,
"aggs": {
"price": {
"histogram": {
"field": "price",
"interval": 2000
},
"aggs": {
"revenue": {
"sum": {
"field": "price"
}
}
}
}
}
}
1.2.6 按日期区间(histogram&calendar_interval) 统计电视销量
date histogram,按照我们指定的某个date类型的日期field,以及日期calendar_interval,按照一定的日期间隔,去划分bucket。
假设:
calendar_interval = 1m,
则:
2017-01-01~2017-01-31,就是一个bucket
2017-02-01~2017-02-28,就是一个bucket
然后会去扫描每个数据的date field,判断date落在哪个bucket中,就将其放入那个bucket,2017-01-05,就将其放入2017-01-01~2017-01-31,就是一个bucket。
min_doc_count:即使某个日期interval,2017-01-01~2017-01-31中,一条数据都没有,那么这个区间也是要返回的,不然默认是会过滤掉这个区间的
extended_bounds,min,max:划分bucket的时候,会限定在这个起始日期,和截止日期内
GET /tvs/_search
{
"size": 0,
"aggs": {
"price": {
"histogram": {
"field": "price",
"interval": 2000
},
"aggs": {
"revenue": {
"sum": {
"field": "price"
}
}
}
}
}
}
1.2.7 按颜色+生产商多层分组(bucket)嵌套下钻分析
下钻分析,就要对bucket进行多层嵌套,多次分组。
举例理解:
比如说,现在红色的电视有4台,同时这4台电视中,有3台是属于长虹的,1台是属于小米的
红色电视中的3台长虹的平均价格是多少?
红色电视中的1台小米的平均价格是多少?
下钻的意思是,已经分了一个组,比如说颜色的分组,然后还要继续对这个分组内的数据,再分组,比如一 个颜色内,还可以分成多个不同的品牌的组,最后对每个最小粒度的分组执行聚合分析操作,这就叫做下钻 分析
按照多个维度(颜色+品牌)多层下钻分析,都可以对每个维度分别执行一次metric聚合操作
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"color_avg_price": {
"avg": {
"field": "price"
}
},
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"brand_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
1.2.8 按季度+品牌多层分组下钻分析售额
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_sold_date": {
"date_histogram": {
"field": "sold_date",
"calendar_interval": "quarter",
"format": "yyyy-MM-dd",
"min_doc_count": 0,
"extended_bounds": {
"min": "2016-01-01",
"max": "2017-12-31"
}
},
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"sum_price": {
"sum": {
"field": "price"
}
}
}
},
"total_sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
二 聚合分析之缩小数据范围&数据排序
2.1 先缩小数据范围再聚合分析
2.1.1 按小米品牌搜索 统计销售额
先查询 品牌为小米的数据 后聚合
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "小米"
}
}
},
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
}
}
}
}
2.1.2 单品牌长虹与所有品牌(global bucket)销量对比
global就是global bucket,就是将所有数据纳入聚合的scope,他不关心过滤的范围,他是统计所有的数据
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "长虹"
}
}
},
"aggs": {
"single_brand_avg_price": {
"avg": {
"field": "price"
}
},
"all": {
"global": {},
"aggs": {
"all_brand_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
single_brand_avg_price:就是针对query搜索结果,执行的,拿到的,就是长虹品牌的平均价格
all.all_brand_avg_price:拿到所有品牌的平均价格
2.1.3 按价格大于1200过滤(filter) 计算平均价格
先搜索过滤出价格大于1200 的数据,然后再计算avg价格
GET /tvs/_search
{
"size": 0,
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 1200
}
}
}
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
2.1.4 按时间段+品牌过滤统计销售额
GET /tvs/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "长虹"
}
}
},
"aggs": {
"recent_150d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-150d"
}
}
},
"aggs": {
"recent_150d_avg_price": {
"avg": {
"field": "price"
}
}
}
},
"recent_140d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-140d"
}
}
},
"aggs": {
"recent_140d_avg_price": {
"avg": {
"field": "price"
}
}
}
},
"recent_130d": {
"filter": {
"range": {
"sold_date": {
"gte": "now-130d"
}
}
},
"aggs": {
"recent_130d_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
bucket filter:对不同的bucket下的aggs,进行filter
2.2 对分组数据进行排序
2.2.1 按颜色分组对销售额排序
每个颜色的电视的销售额,需要按照销售额降序排序
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"avg_price": "asc"
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
2.2.2 按颜色+品牌多层分组下钻排序
就是先颜色分组 然后品牌里分组且排序(平均价格降序)
GET /tvs/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand",
"order": {
"avg_price": "desc"
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
三 聚合分析之百分比统计
3.1 percentiles百分比统计
需求:比如有一个网站,记录下了每次请求的访问的耗时,需要统计tp50,tp90,tp99
tp50:50%的请求的耗时最长在多长时间
tp90:90%的请求的耗时最长在多长时间
tp99:99%的请求的耗时最长在多长时间
3.1.1 初始化数据
DELETE website
PUT /website
{
"mappings": {
"properties": {
"latency": {
"type": "long"
},
"province": {
"type": "keyword"
},
"timestamp": {
"type": "date"
}
}
}
}
POST /website/_bulk
{"index":{}}
{"latency":105,"province":"江苏","timestamp":"2016-10-28"}
{"index":{}}
{"latency":83,"province":"江苏","timestamp":"2016-10-29"}
{"index":{}}
{"latency":92,"province":"江苏","timestamp":"2016-10-29"}
{"index":{}}
{"latency":112,"province":"江苏","timestamp":"2016-10-28"}
{"index":{}}
{"latency":68,"province":"江苏","timestamp":"2016-10-28"}
{"index":{}}
{"latency":76,"province":"江苏","timestamp":"2016-10-29"}
{"index":{}}
{"latency":101,"province":"新疆","timestamp":"2016-10-28"}
{"index":{}}
{"latency":275,"province":"新疆","timestamp":"2016-10-29"}
{"index":{}}
{"latency":166,"province":"新疆","timestamp":"2016-10-29"}
{"index":{}}
{"latency":654,"province":"新疆","timestamp":"2016-10-28"}
{"index":{}}
{"latency":389,"province":"新疆","timestamp":"2016-10-28"}
{"index":{}}
{"latency":302,"province":"新疆","timestamp":"2016-10-29"}
3.1.2 百分比统计
GET /website/_search
{
"size": 0,
"aggs": {
"latency_percentiles": {
"percentiles": {
"field": "latency",
"percents": [
50,
95,
99
]
}
},
"latency_avg": {
"avg": {
"field": "latency"
}
}
}
}
50%的请求,数值的最大的值是多少,不是完全准确的
3.1.3 按照省份分组 算百分比
GET /website/_search
{
"size": 0,
"aggs": {
"group_by_province": {
"terms": {
"field": "province"
},
"aggs": {
"latency_percentiles": {
"percentiles": {
"field": "latency",
"percents": [
50,
95,
99
]
}
},
"latency_avg": {
"avg": {
"field": "latency"
}
}
}
}
}
}
3.2 percentile rank&SLA统计
SLA:就是你提供的服务的标准
我们的网站的提供的访问延时的SLA,确保所有的请求100%,都必须在200ms以内,大公司内,一般都是要求100%在200ms以内,如果超过1s,则需要升级到A级故障,代表网站的访问性能和用户体验急剧下降。
需求:在200ms以内的,有百分之多少,在1000毫秒以内的有百分之多少,percentile ranks metric
GET /website/_search
{
"size": 0,
"aggs": {
"group_by_province": {
"terms": {
"field": "province"
},
"aggs": {
"latency_percentile_ranks": {
"percentile_ranks": {
"field": "latency",
"values": [
200,
1000
]
}
}
}
}
}
}
percentile的优化
如果你想要percentile算法越精准,compression可以设置的越大
四 聚合分析相关算法原理及优化
4.1 易并行&不易并行算法
易并行:max
不易并行:count(distinct),并不是说,在每个node上,直接就出一些count(distinct) value,就可以的,因为数据可能会很多。
es会采取近似聚合算法,就是采用在每个node上进行近估计的方式,得到最终的结论。
如果采取近似估计的算法:延时在100ms左右(一般会达到完全精准的算法的性能的数十倍),0.5%错误
如果采取100%精准的算法:延时一般在5s~几十s,甚至几十分钟,几小时, 0%错误
4.2 精准+实时+大数据三角选择原则
- 精准+实时: 没有大数据,数据量很小,那么一般就是单机跑,随便你则么玩儿就可以
- 精准+大数据:hadoop,批处理,非实时,可以处理海量数据,保证精准,可能会跑几个小时
- 大数据+实时:es,不精准,近似估计,可能会有百分之几的错误率
4.3 cartinality(去重)算法
cartinality metric,对每个bucket中的指定的field进行去重,取去重后的count,类似于count(distcint)
GET /tvs/_search
{
"size": 0,
"aggs": {
"months": {
"date_histogram": {
"field": "sold_date",
"calendar_interval": "month"
},
"aggs": {
"distinct_colors": {
"cardinality": {
"field": "brand"
}
}
}
}
}
}
4.4 cardinality&precision_threshold优化准确率和内存开销
GET /tvs/_search
{
"size": 0,
"aggs": {
"distinct_brand": {
"cardinality": {
"field": "brand",
"precision_threshold": 100
}
}
}
}
brand去重,如果brand的unique value,precision_threshold=100 ,即 在100个以内,小米,长虹,三星,TCL,HTL......则cardinality,几乎保证100%准确。
precision_threshold:
- 会占用precision_threshold * 8 byte 内存消耗,100 * 8 = 800个字节(占用内存很小)
- 而且unique value如果的确在值以内,那么可以确保100%准确
precision_threshold,值设置的越大,占用内存越大,1000 * 8 = 8000 / 1000 = 8KB,可以确保更多unique value的场景下,100%的准确。
4.5 HyperLogLog++ (HLL)算法index-time性能优化
cardinality底层算法:HLL算法会对所有的uqniue value取hash值,通过hash值近似去求distcint count。
默认情况下,发送一个cardinality请求的时候,会动态地对所有的field value 取hash值;
优化方法:将取hash值的操作,前移到建立索引的时候,即我们灌入数据的时候建好hash值,但是提升性能不大,了解即可
PUT /tvs
{
"mappings": {
"properties": {
"brand": {
"type": "text",
"fields": {
"hash": {
"type": "murmur3"
}
}
}
}
}
}
GET /tvs/_search
{
"size": 0,
"aggs": {
"distinct_brand": {
"cardinality": {
"field": "brand.hash",
"precision_threshold": 100
}
}
}
}
五 聚合分析的内部原理
5.1 doc value 正排原理
聚合分析的内部原理是什么?aggs,term,metric avg max,执行一个聚合操作的时候,内部原理是怎样的呢?用了什么样的数据结构去执行聚合?是不是用的倒排索引?
GET /test_index/_search
{
"query": {
"match": {
"search_field": "test"
}
},
"aggs": {
"group_by_agg_field": {
"terms": {
"field": "agg_field"
}
}
}
}
模拟解释
查询操作
doc1: hello world test1, test2 、doc2: hello test、doc3: world test
创建倒排索引
word | doc1 | doc2 | doc3 |
hello | * | * |
|
world | * |
| * |
test1 | * |
|
|
test2 | * |
|
|
test |
| * | * |
执行全文检索
"query": {
"match": {
"search_field": "test"
}
}
结果为 doc2,doc3
聚合操作
doc2: agg1 hello world
doc3: agg2 test hello
正排索引
5.2 doc value 核心原理
正排索引,也会写入磁盘文件中,然后os cache先进行缓存,以提升访问doc value正排索引的性能
如果os cache内存大小不足够放得下整个正排索引,doc value,就会将doc value的数据写入磁盘文件中。
es官方是建议,es大量是基于os cache来进行缓存和提升性能的,不建议用jvm内存来进行缓存,那样会导致一定的gc开销和oom问题。即给jvm更少的内存,给os cache更大的内存。
64g服务器,给jvm最多16g,几十个g的内存给os cache,os cache可以提升doc value和倒排索引的缓存和查询效率。
提升 doc value 性能之 column压缩 合并相同值
doc1: 550
doc2: 550
doc3: 500
doc1和doc2都保留一个550的标识即可
- 所有值相同,直接保留单值
- 少于256个值,使用table encoding模式:一种压缩方式
- 大于256个值,看有没有最大公约数,有就除以最大公约数,然后保留这个最大公约数
- 如果没有最大公约数,采取offset结合压缩的方式:
disable doc value
如果的确不需要doc value,比如聚合等操作,那么可以禁用,减少磁盘空间占用
PUT my_index12
{
"mappings": {
"properties": {
"my_field": {
"type": "keyword",
"doc_values": false
}
}
}
}
5.3 fielddata 原理
5.3.1 对分词的field 如何聚合操作
对于分词的field执行aggregation(聚合操作),发现报错。。。
GET /test_index/_search
{
"aggs": {
"group_by_test_field": {
"terms": {
"field": "test_field"
}
}
}
}
对分词的field,直接执行聚合操作会报错,提示说必须要打开fielddata,然后将正排索引数据加载到内存中,才可以对分词的field执行聚合操作,而且会消耗很大的内存。
给分词的field,设置fielddata=true
POST /test_index/_mapping
{
"properties": {
"test_field": {
"type": "text",
"fielddata": true
}
}
}
测试聚合操作
GET /test_index/_search
{
"size": 0,
"aggs": {
"group_by_test_field": {
"terms": {
"field": "test_field"
}
}
}
}
如果要对分词的field执行聚合操作,必须将fielddata设置为true
5.3.2 使用内置field不分词进行聚合
GET /test_index/_search
{
"size": 0,
"aggs": {
"group_by_test_field": {
"terms": {
"field": "test_field.keyword"
}
}
}
}
如果对不分词的field执行聚合操作,直接就可以执行,不需要设置fieldata=true
5.3.3 分词field+fielddata的工作原理
不分词的field,可以执行聚合操作 , 如果你的某个field不分词,那么在index-time,就会自动生成doc value ,针对这些不分词的field执行聚合操作的时候,自动就会用doc value来执行。
分词field,是没有doc value的。在index-time 是不会给它建立doc value正排索引的,因为分词后,占用的空间过于大,所以默认是不支持分词field进行聚合的。所以直接对分词field执行聚合操作,是会报错的。
如果一定要对分词的field执行聚合,那么必须将fielddata=true,然后es就会在执行聚合操作的时候,现场将field对应的数据,建立一份fielddata正排索引,fielddata正排索引的结构跟doc value是类似的,但是只会将fielddata正排索引加载到内存中来,然后基于内存中的fielddata正排索引执行分词field的聚合操作。
5.4 fielddata 内存控制 & circuit breajer 断路器
fielddata加载到内存的过程是lazy加载的,对一个analzyed field执行聚合时,才会加载,而且是field-level加载的。它不是index-time创建,是query-time创建。
5.4.1 fielddata内存限制
在配置文件中配置
indices.fielddata.cache.size: 20%,超出限制,清除内存已有fielddata数据
fielddata占用的内存超出了这个比例的限制,那么就清除掉内存中已有的fielddata数据
默认无限制,限制内存使用,但是会导致频繁evict和reload,大量IO性能损耗,以及内存碎片和gc
5.4.2 监控fielddata内存使用
GET /_stats/fielddata?fields=*
GET /_nodes/stats/indices/fielddata?fields=*
GET /_nodes/stats/indices/fielddata?level=indices&fields=*
5.4.3 circuit breaker
如果一次query load的feilddata超过总内存,就会oom内存溢出
circuit breaker会估算query要加载的fielddata大小,如果超出总内存,就短路,query直接失败
indices.breaker.fielddata.limit:fielddata的内存限制,默认60%
indices.breaker.request.limit:执行聚合的内存限制,默认40%
indices.breaker.total.limit:综合上面两个,限制在70%以内
5.5 原理 fielddata预加载 全局标记
如果真的要对分词的field执行聚合,那么每次都在query-time现场生产fielddata并加载到内存中来,速度可能会比较慢,我们是不是可以预先生成加载fielddata到内存中来???
global ordinal
PUT my_index/_mapping
{
"properties": {
"tags": {
"type": "keyword",
"eager_global_ordinals": false
}
}
}
原理解释
假设:
doc1: status1
doc2: status2
doc3: status2
doc4: status1
有很多重复值的情况,会进行global ordinal标记
status1 --> 0
status2 --> 1
doc1: 0
doc2: 1
doc3: 1
doc4: 0
建立的fielddata也会是这个样子的,这样的好处就是减少重复字符串的出现的次数,减少内存的消耗
5.6 原理 bucket 深度优先到广度优先
我们的数据:
根据演员分桶: 每个演员的评论的数量
根据每个演员电影分桶: 每个演员的每个电影的评论的数量
评论数量排名前10个的演员,每个演员的电影取到评论数量排名前5的电影
{
"aggs" : {
"actors" : {
"terms" : {
"field" : "actors",
"size" : 10,
},
"aggs" : {
"costars" : {
"terms" : {
"field" : "films",
"size" : 5
}
}
}
}
}
}
默认是 深度优先的方式去执行聚合操作的。它是把所有人的所有电影都查询出来数据量很大。因此我们要考虑广度优先,即我们先过滤出评论前10的演员,然后再去查询他下面的电影,这样数据少很多。我们要使用一个参数
collect_mode=breadth_first
{
"aggs" : {
"actors" : {
"terms" : {
"field" : "actors",
"size" : 10,
"collect_mode" : "breadth_first"
},
"aggs" : {
"costars" : {
"terms" : {
"field" : "films",
"size" : 5
}
}
}
}
}
}
六 数据建模实战
6.1 用户+博客数据建模 应用层join关联
我们在构造数据模型的时候,还是将有关联关系的数据,然后分割为不同的实体,类似于关系型数据库中的模型。
6.1.1 用户+博客建模
案例背景:博客网站,我们会模拟各种用户发表各种博客,然后针对用户和博客之间的关系进行数据建模,同时针对建模好的数据执行各种搜索/聚合的操作
一个用户对应多个博客,一对多的关系,做了建模
POST website_users/_doc/1
{
"name": "小鱼儿",
"email": "xiaoyuer@sina.com",
"birthday": "1980-01-01"
}
POST website_blogs/_doc/1
{
"title": "我的第一篇博客",
"content": "这是我的第一篇博客,开通啦!!!",
"userId": 1
}
6.1.2 搜索小鱼儿发表的所有博客
GET /website_users/_search
{
"query": {
"term": {
"name.keyword": {
"value": "小鱼儿"
}
}
}
}
我们一般在java程序里查询出 userIds 集合 然后再去查询blog
GET /website_blogs/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"userId": [
1
]
}
}
}
}
}
上面的操作,就属于应用层的join,在应用层先查出一份数据,然后再查出一份数据,进行关联
优点:数据不冗余,维护方便
缺点:应用层join,如果关联数据过多,导致查询过大,性能很差
6.2 用户+博客数据建模 冗余数据
用冗余数据,采用文档数据模型,进行数据建模,实现用户和博客的关联
6.2.1 准备数据
冗余数据,就是说,将可能会进行搜索的条件和要搜索的数据,放在一个doc中
POST /website_users/_doc/1
{
"name": "小鱼儿",
"email": "xiaoyuer@sina.com",
"birthday": "1980-01-01"
}
POST /website_blogs/_doc/1
{
"title": "小鱼儿的第一篇博客",
"content": "大家好,我是小鱼儿。。。",
"userInfo": {
"userId": 1,
"username": "小鱼儿"
}
}
6.2.2 冗余用户数据搜索博客
不需要走应用层的join,先搜一个数据,找到id,再去搜另一份数据
GET /website_blogs/_search
{
"query": {
"term": {
"userInfo.username.keyword": {
"value": "小鱼儿"
}
}
}
}
优点:性能高,不需要执行两次搜索
缺点:数据冗余,维护成本高 --> 每次如果你的username变化了,同时要更新user type和blog type
一般来说,对于es这种NoSQL类型的数据存储来讲,都是冗余模式....
6.3 nested object 博客+评论嵌套
冗余数据方式的来建模,其实用的就是object类型,我们这里又要引入一种新的object类型,nested object类型
博客,评论,做的这种数据模型
6.3.1 准备数据
POST website_blogs/_doc/6
{
"title": "花无缺发表的一篇帖子",
"content": "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
"tags": [
"投资",
"理财"
],
"comments": [
{
"name": "小鱼儿",
"comment": "什么股票啊?推荐一下呗",
"age": 28,
"stars": 4,
"date": "2016-09-01"
},
{
"name": "黄药师",
"comment": "我喜欢投资房产,风,险大收益也大",
"age": 31,
"stars": 5,
"date": "2016-10-22"
}
]
}
年龄是28岁的黄药师评论过的博客,搜索
GET website_blogs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "黄药师"
}
},
{
"match": {
"comments.age": 28
}
}
]
}
}
}
结果显然是不对的,应该不能查询到数据才对。
分析 object类型数据结构的底层存储
{
"title": [ "花无缺", "发表", "一篇", "帖子" ],
"content": [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
"tags": [ "投资", "理财" ],
"comments.name": [ "小鱼儿", "黄药师" ],
"comments.comment": [ "什么", "股票", "推荐", "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
"comments.age": [ 28, 31 ],
"comments.stars": [ 4, 5 ],
"comments.date": [ 2016-09-01, 2016-10-22 ]
}
object类型底层数据结构,会将一个json数组中的数据,进行扁平化
所以,直接命中了这个document,name=黄药师,age=28,正好符合
6.3.2 nested object 按对象拆分扁平化数据
修改mapping,将comments的类型从object设置为nested
DELETE website_blogs
PUT /website_blogs
{
"mappings": {
"properties": {
"comments": {
"type": "nested",
"properties": {
"name": {
"type": "text"
},
"comment": {
"type": "text"
},
"age": {
"type": "short"
},
"stars": {
"type": "short"
},
"date": {
"type": "date"
}
}
}
}
}
}
插入数据
POST website_blogs/_doc/6
{
"title": "花无缺发表的一篇帖子",
"content": "我是花无缺,大家要不要考虑一下投资房产和买股票的事情啊。。。",
"tags": [
"投资",
"理财"
],
"comments": [
{
"name": "小鱼儿",
"comment": "什么股票啊?推荐一下呗",
"age": 28,
"stars": 4,
"date": "2016-09-01"
},
{
"name": "黄药师",
"comment": "我喜欢投资房产,风,险大收益也大",
"age": 31,
"stars": 5,
"date": "2016-10-22"
}
]
}
他的数据结构,就不是那么扁平化了
{
"comments.name": [ "小鱼儿" ],
"comments.comment": [ "什么", "股票", "推荐" ],
"comments.age": [ 28 ],
"comments.stars": [ 4 ],
"comments.date": [ 2014-09-01 ]
}
{
"comments.name": [ "黄药师" ],
"comments.comment": [ "我", "喜欢", "投资", "房产", "风险", "收益", "大" ],
"comments.age": [ 31 ],
"comments.stars": [ 5 ],
"comments.date": [ 2014-10-22 ]
}
{
"title": [ "花无缺", "发表", "一篇", "帖子" ],
"body": [ "我", "是", "花无缺", "大家", "要不要", "考虑", "一下", "投资", "房产", "买", "股票", "事情" ],
"tags": [ "投资", "理财" ]
}
再次搜索,成功了
GET website_blogs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "花无缺"
}
},
{
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{
"match": {
"comments.name": "黄药师"
}
},
{
"match": {
"comments.age": 31
}
}
]
}
}
}
}
]
}
}
}
6.3.3 nested object的聚合分析
聚合数据分析的需求1:按照评论日期进行bucket划分,然后拿到每个月的评论的评分的平均值
GET website_blogs/_search
{
"size": 0,
"aggs": {
"comments_path": {
"nested": {
"path": "comments"
},
"aggs": {
"group_by_comments_date": {
"date_histogram": {
"field": "comments.date",
"calendar_interval": "month",
"format": "yyyy-MM"
},
"aggs": {
"avg_stars": {
"avg": {
"field": "comments.stars"
}
}
}
}
}
}
}
}
根据年龄和tag 划分
GET website_blogs/_search
{
"size": 0,
"aggs": {
"comments_path": {
"nested": {
"path": "comments"
},
"aggs": {
"group_by_comments_age": {
"histogram": {
"field": "comments.age",
"interval": 10
},
"aggs": {
"reverse_path": {
"reverse_nested": {},
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags.keyword"
}
}
}
}
}
}
}
}
}
}
6.4 parent child join
nested object的建模,有个不好的地方,就是采取的是类似冗余数据的方式,将多个数据都放在一起了,维护成本就比较高
parent child建模方式,采取的是类似于关系型数据库的三范式类的建模,多个实体都分割开来,每个实体之间都通过一些关联方式,进行了父子关系的关联,各种数据不需要都放在一起,父doc和子doc分别在进行更新的时候,都不会影响对方。
一对多关系的建模,维护起来比较方便,而且我们之前说过,类似关系型数据库的建模方式,应用层join的方式,会导致性能比较差,因为做多次搜索。父子关系的数据模型,不会,性能很好。因为虽然数据实体之间分割开来,但是我们在搜索的时候,由es自动为我们处理底层的关联关系,并且通过一些手段保证搜索性能。
父子关系数据模型,相对于nested数据模型来说,优点是父doc和子doc互相之间不会影响
要点:父子关系元数据映射,用于确保查询时候的高性能,但是有一个限制,就是父子数据必须存在于一个shard中
父子关系数据存在一个shard中,而且还有映射其关联关系的元数据,那么搜索父子关系数据的时候,不用跨分片,一个分片本地自己就搞定了,性能当然高咯
案例背景:研发中心员工管理案例,一个IT公司有多个研发中心,每个研发中心有多个员工
6.5 类似文件系统多层级关系数据建模
path_hierarchy tokenizer 也就是:/a/b/c/d --> path_hierarchy -> /a/b/c/d, /a/b/c, /a/b, /a
6.5.1 准备数据
创建分词器
PUT /fs
{
"settings": {
"analysis": {
"analyzer": {
"paths": {
"tokenizer": "path_hierarchy"
}
}
}
}
}
设置mapping
PUT /fs/_mapping
{
"properties": {
"name": {
"type": "keyword"
},
"path": {
"type": "keyword",
"fields": {
"tree": {
"type": "text",
"analyzer": "paths"
}
}
}
}
}
插入数据
POST /fs/_doc/1
{
"name": "README.txt",
"path": "/workspace/projects/helloworld",
"contents": "这是我的第一个elasticsearch程序"
}
6.5.2 对文件系统执行搜索
文件搜索需求:查找一份,内容包括elasticsearch,在/workspace/projects/hellworld这个目录下的文件
GET /fs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"contents": "elasticsearch"
}
},
{
"constant_score": {
"filter": {
"term": {
"path": "/workspace/projects/helloworld"
}
}
}
}
]
}
}
}
搜索需求2:搜索/workspace目录下,内容包含elasticsearch的所有的文件
GET /fs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"contents": "elasticsearch"
}
},
{
"constant_score": {
"filter": {
"term": {
"path.tree": "/workspace"
}
}
}
}
]
}
}
}
6.6 全局锁+悲观锁 并发控制
第一种锁:全局锁,直接锁掉整个fs index,此时就只有你能执行各种各样的操作了,其他人不能执行操作
PUT /fs/_doc/global/_create
{}
fs: 你要上锁的那个index
lock: 就是你指定的一个对这个index上全局锁的一个type
global: 就是你上的全局锁对应的这个doc的id
_create:强制必须是创建,如果/fs/lock/global这个doc已经存在,那么创建失败,报错
POST fs/_update/1
{
"doc": {
"name": "README1.txt"
}
}
删除锁 DELETE /fs/_doc/global
优点:操作非常简单,非常容易使用,成本低
缺点:你直接就把整个index给上锁了,这个时候对index中所有的doc的操作,都会被block住,导致整个系统的并发能力很低
6.7 document+ 悲观锁
document锁,顾名思义,每次就锁你要操作的,你要执行增删改的那些doc,doc锁了,其他线程就不能对这些doc执行增删改操作了,但是你只是锁了部分doc,其他线程对其他的doc还是可以上锁和执行增删改操作的。
document锁,是用脚本进行上锁
POST _scripts/document-lock
{
"script": {
"lang": "painless",
"source": "if ( ctx._source.process_id != params.process_id ) { Debug.explain('already locked by other thread'); } ctx.op = 'noop';"
}
}
POST /fs/_update/1
{
"upsert": {
"process_id": "123"
},
"script": {
"id": "document-lock",
"params": {
"process_id": "123"
}
}
}
DELETE /fs/_doc/1
PUT /fs/_doc/_bulk
{ "delete": { "_id": 1}}
update+upsert操作,如果该记录没加锁(此时document为空),执行upsert操作,设置process_id,如果已加锁,执行script
script内的逻辑是:判断传入参数与当前doc的process_id,如果不相等,说明有别的线程尝试对有锁的doc进行加锁操作,Debug.explain表示抛出一个异常。
process_id可以由Java应用系统里生成,如UUID。
如果两个process_id相同,说明当前执行的线程与加锁的线程是同一个,ctx.op = 'noop'表示什么都不做,返回成功的响应,Java客户端拿到成功响应的报文,就可以继续下一步的操作,一般这里的下一步就是执行事务方法。
POST /fs/_update/1
{
"doc": {
"name": "README1.txt"
}
}
6.8 共享锁&排它锁 并发控制
共享锁:这份数据是共享的,然后多个线程过来,都可以获取同一个数据的共享锁,然后对这个数据执行读操作
排他锁:是排他的操作,只能一个线程获取排他锁,然后执行增删改操作
如果只是要读取数据的话,那么任意个线程都可以同时进来然后读取数据,每个线程都可以上一个共享锁
但是这个时候,如果有线程要过来修改数据,那么会尝试加上排他锁,排他锁会跟共享锁互斥,也就是说,如果有人已经上了共享锁了,那么排他锁就不能上。即 如果有人在读数据,就不允许别人来修改数据。反之,也是一样的。
6.8.1 共享锁
POST _scripts/shared-lock
{
"script": {
"lang": "painless",
"source": "if (ctx._source.lock_type == 'exclusive') { Debug.explain('already locked'); } ctx._source.lock_count++"
}
}
POST /fs/_update/1
{
"upsert": {
"lock_type": "shared",
"lock_count": 1
},
"script": {
"id": "shared-lock"
}
}
GET /fs/_doc/1
上共享锁,你还是要上共享锁,直接上就行了,没问题,只是lock_count加1。
6.8.2 排他锁
排他锁用的不是upsert语法,create语法,要求lock必须不能存在,直接自己是第一个上锁的人,上的是排他锁
PUT /fs/_create/1
{ "lock_type": "exclusive" }
如果已经有人上了共享锁,create语法去上排他锁,肯定会报错
6.9.3 对共享锁进行解锁
POST _scripts/unlock-shared
{
"script": {
"lang": "painless",
"source": "if (--ctx._source.lock_count == 0) {ctx.op='delete'}"
}
}
POST /fs/_update/1
{
"script": {
"id": "unlock-shared"
}
}
每次解锁一个共享锁,就对lock_count先减1,如果减了1之后,是0,那么说明所有的共享锁都解锁完了,此时就就将/fs/_doc/1删除,就彻底解锁所有的共享锁