Elasticsearch 7.7.0 高阶篇-聚合技术

前言

本篇内容是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/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删除,就彻底解锁所有的共享锁

Elasticsearch中,聚合(aggregation)是一种强大的数据分析工具,可以对文档进行统计分析并返回计算结果。其中,Metric聚合是一种聚合类型,它会对文档中的某些数值型字段进行统计计算,例如:平均值、最大值、最小值、总和等。 下面我们来看一下如何在Elasticsearch中使用Metric聚合进行数据分析。 假设我们有一个存储了销售数据的索引,其中每个文档都包含了产品的名称、价格、销售量等信息。我们想要统计该索引中所有产品的平均价格、最高价格、最低价格以及销售总量,可以使用以下的聚合查询DSL: ``` GET /sales/_search { "size": 0, "aggs": { "avg_price": { "avg": { "field": "price" } }, "max_price": { "max": { "field": "price" } }, "min_price": { "min": { "field": "price" } }, "total_sales": { "sum": { "field": "sales" } } } } ``` 在上述查询中,我们使用了四个不同的Metric聚合:avg(平均值)、max(最大值)、min(最小值)和sum(总和)。每个聚合都针对文档中的price和sales字段进行了计算,最终返回了平均价格、最高价格、最低价格以及销售总量的计算结果。 在聚合查询中,我们还可以使用多个Metric聚合组合起来进行更加复杂的数据分析。例如,我们可以计算不同销售区域的平均价格和销售总量,可以使用以下的聚合查询DSL: ``` GET /sales/_search { "size": 0, "aggs": { "by_region": { "terms": { "field": "region" }, "aggs": { "avg_price": { "avg": { "field": "price" } }, "total_sales": { "sum": { "field": "sales" } } } } } } ``` 在上述查询中,我们首先使用了terms聚合将文档按照region字段进行了分组,然后在每个分组中使用了两个不同的Metric聚合:avg(平均值)和sum(总和)。最终返回了不同销售区域的平均价格和销售总量的计算结果。 总之,Metric聚合Elasticsearch中非常有用的数据分析工具,可以帮助我们对文档中的数值型字段进行统计分析并返回计算结果。在实际应用中,我们可以根据具体的业务需求来选择不同的Metric聚合进行数据分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值