前言

本篇内容是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}]}}
}
  1. hits.hits:指定size=0,所以hits.hits就是空的,否则会把执行聚合的那些原始数据返回回来
  2. aggregations:聚合结果
  3. popular_color:指定的某个聚合的名称
  4. buckets:指定的field划分出的buckets
  5. key:每个bucket对应的那个值
  6. doc_count:每个bucket分组内,有多少个数据 ,每种颜色对应的bucket中的数据的
  7. 默认的排序规则:按照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 精准+实时+大数据三角选择原则

  1. 精准+实时: 没有大数据,数据量很小,那么一般就是单机跑,随便你则么玩儿就可以
  2. 精准+大数据:hadoop,批处理,非实时,可以处理海量数据,保证精准,可能会跑几个小时
  3. 大数据+实时: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的标识即可

  1. 所有值相同,直接保留单值
  2. 少于256个值,使用table encoding模式:一种压缩方式
  3. 大于256个值,看有没有最大公约数,有就除以最大公约数,然后保留这个最大公约数
  4. 如果没有最大公约数,采取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/1PUT /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删除,就彻底解锁所有的共享锁

Elasticsearch 7.7.0 高阶篇-聚合技术相关推荐

  1. Redis7实战加面试题-高阶篇(案例落地实战bitmap/hyperloglog/GEO)

    案例落地实战bitmap/hyperloglog/GEO 面试题: 抖音电商直播,主播介绍的商品有评论,1个商品对应了1系列的评论,排序+展现+取前10条记录 用户在手机App上的签到打卡信息:1天对 ...

  2. php redis微信发红包,高阶篇二 使用Redis队列发送微信模版消息

    # 高阶篇二 使用Redis队列发送微信模版消息 > 此命令行执行任务的方法类比较复杂 他需要命令行运行才会有效 > 命令行源码以及创建方法 参见上节 https://www.kanclo ...

  3. Go 接口实现原理【高阶篇】: type _interface struct

    Go 接口实现原理[高阶篇]: type _interface struct The Internal Definition Of Interface Types https://www.tapirg ...

  4. ✨三万字制作,关于C语言,你必须知道的这些知识点(高阶篇)✨

    目录 一,写在前面 二,数据的存储 1,数据类型介绍 2,类型的基本归类 3,整形在内存中的存储 4,浮点型在内存中的存储 三,指针的进阶 1,字符指针 2,指针数组 3,数组指针的使用 4,函数指针 ...

  5. 爬虫requests高阶篇详细教程

    文章目录 一.前言 二.SSL验证 三.代理设置 四.超时设置 ​ 五.身份认证 1)基本身份认证 2)摘要式身份认证 六.总结 一.前言 本篇文高阶篇,上一篇为基础篇,希望你一定要学完基础再来看高阶 ...

  6. 高阶篇:4.3)FTA故障树分析法-DFMEA的另外一张脸

    本章目的:明确什么是FTA,及与DFMEA的关系. 1.FTA定义 故障树分析(FTA) 其一:故障树分析(Fault Tree Analysis,简称FTA)又称事故树分析,是安全系统工程中最重要的 ...

  7. 高阶篇:4.2.2)DFMEA层级分明的失效模式、失效后果、失效原因

    本章目的:明确失效模式.失效后果.失效原因的定义,分清楚层次关系,完成DFMEA这部分的填写. 1.失效模式,失效后果,失效原因的定义 这是FEMEA手册第四册中的定义. 1.1 潜在失效模式 (b) ...

  8. Prompt工程师指南[高阶篇]:对抗性Prompting、主动prompt、ReAct、GraphPrompts、Multimodal CoT Prompting等

    Prompt工程师指南[高阶篇]:对抗性Prompting.主动prompt.ReAct.GraphPrompts.Multimodal CoT Prompting等 1.对抗性 Prompting ...

  9. 【檀越剑指大厂--mysql】mysql高阶篇

    文章目录 一.Mysql 基础 1.数据库与实例? 2.mysql 的配置文件 3.mysql 体系结构 4.innodb 的特点? 5.innodb 和 myisam 的区别 6.其他存储引擎? 7 ...

  10. 【檀越剑指大厂—SpringCloudNetflix】SpringCloudNetflix高阶篇

    一.基础概念 1.架构演进 在系统架构与设计的实践中,从宏观上可以总结为三个阶段: 单体架构 :就是把所有的功能.模块都集中到一个项目中,部署在一台服务器上,从而对外提供服务(单体架构.单体服务.单体 ...

最新文章

  1. Python3 list 自定义比较函数
  2. python3安装cocos2d_(3)在Windows7上搭建Cocos2d-x
  3. Git 常用命令清单,掌握这些,轻松驾驭版本管理
  4. Mysql 8.0安装
  5. mysql autoenlist默认_javascript code all (2) (转转)
  6. python写一个ssh工具_用Python写个自动ssh登录远程服务器的小工具
  7. python查看文档的软件_Python __doc__属性:查看文档
  8. 硬件:U盘无法识别的解决方案
  9. 递归优化的这三种方式你知道吗?
  10. petshop4学习_重构DataList实现分页
  11. spring处理循环依赖时序图_Maven依赖管理系统
  12. 【Python】如何在python中执行另一个py文件
  13. c语言下面程序的功能是求圆的周长和面积.请改正程序中带*行中,2012年计算机等级考试二级C语言上机题(5)...
  14. Rand7()实现Rand10()
  15. python中线程里面多线程_Python中的线程和多线程是什么
  16. js深入研究之神奇的匿名函数类生成方式
  17. -mmin find shell 报错_[shell]find用法小结
  18. 下载MySQL安装包
  19. 高级维修电工实训装置
  20. 奥维互动地图谷歌图源,通过自建Vercel反代实现墙内访问 - DaPeng‘s Blog

热门文章

  1. 手把手教你在浏览器(chrom,edge)上安装Tampermonkey(油猴)(附所需所有内容链接)
  2. android 11.0 去掉音量键电源键组合键的屏幕截图功能
  3. 计算机专业博士阶段研究方向,国内计算机专业博士研究方向
  4. 在java中class是什么意思_java 中Class? 中的?代表什么意思?
  5. MyBatis第N+1种分页方式,全新的MyBatis分页
  6. 查看笔记本预装系统的产品密钥
  7. Android使用MediaRecorder的stop方法报stop failed错误的解决方案
  8. 道教门派封神大战九曲黄河迷仙大阵
  9. 浙大陈越老师数据结构 02-线性结构4 Pop Sequence详解
  10. Ubuntu14.04安装微软雅黑字体