白日梦的Elasticsearch实战笔记,ES账号免费借用、32个查询案例、15个聚合案例、7个查询优化技巧(二)

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 白日梦的Elasticsearch实战笔记,ES账号免费借用、32个查询案例、15个聚合案例、7个查询优化技巧(二)

四、聚合分析#


4.1、什么是聚合分析?#


聚合分析有点类似于SQL语句中的那种group by、where age > 20 and age < 30、这种操作。常见的聚合分析就是根据某一个字段进行分组分析,要求这个字段是不能被分词的,如果被聚合的字段被分词,按照倒排索引的方式去索引的话,就不得不去扫描整个倒排索引(才可能将被聚合的字段找全,效率很低)。


聚合分析是基于doc value的数据结果集进行操作的,这个doc value 其实就是正排索引(现在了解就好,下一篇文章统一扫盲),


关于聚合分析有三个重要的概念:

  • bucket
    特别是你去使用一下java、golang中的es相关的api,就会看到这个bucket关键字,bucket就是聚合操作得到的结果集。
  • metric
    metric就是对bucket进行分析,比如取最大值、最小值、平均值。
  • 下钻
    下钻就是在现有的分好组的bucket继续分组,比如可以先按性别分组、下钻再按年龄分组。


4.2、干货!15个聚合分析案例#


1、比如我们公司人很多,其中不泛有很多重名的人,现在我的需求是:我想知道我们公司中有多个人叫tom、多少个人叫jerry,也就是说,我想知道:重名的人分别有多少个。于是我们需要像下面这样根据名字聚合。

聚合的结果中天然存在一个metric,它就是当前bucket的count,也就是我们想要的结果:


GET /your_index/your_type/_search
{ 
  # 表示只要聚合的结果,而不要参与聚合的原始数据
  “size”:0,
  # 使用聚合时,天然存在一个metric,就是当前bucket的count
  "aggs": {
    "group_by_name": { # 自定义的名字
      "term": {
        "field": "name" # 指定聚合的字段, 意思是 group by name
      }
    }
  }
} 
GET /your_index/your_type/_search
{ 
  “size”:0,
   # 使用聚合时,天然存在一个metric,就是当前bucket的count
  "aggs": {
    "group_by_xxx": { # 自定义的名字
     # 除了使用term还可以使用terms
     # trems允许你指定多个字段
     "terms": {
         # 指定聚合的字段, 意思是 group by v1、v2、v3
        "field": {"value1","value2","value3"} 
      }
    }
  }
}


2、先搜索,再对搜索结果聚合。比如我想知道在所有的男生中的重名情况


GET /your_index/your_type/_search
{ 
  # 先查询
  “query”:{
      "term":{
        "gender":"man"
      }
  },
   # 再聚合
  "aggs": {
    "group_by_name": { 
      "term": {
        "field": "name" # 指定聚合的字段, 意思是 group by name
      }
    }
  }
}


3、我想把重名的人分成一组,然后我想了解每组人的平均年龄。可以像下面这样干


GET /your_index/your_type/_search
{ 
  "size":0,
   # 聚合中嵌套聚合,意思是先 group by avg age,再 group by field1。
  "aggs": {
    "group_by_name": {
      "terms": {
        "field": "name"
      },
       # 在上面的name分组的结果之上再按照age聚合
      "aggs": { 
        "average_age": {
          # 指定聚合函数为avg
          "avg": {
            "field": "age"
          }
        }
      }
    }
  }
}


4、我想了解我们公司不同年龄段:20岁~25岁有多少人、25岁~30岁有多少人、30岁~35岁、35岁~40岁有多少人,以及每个年龄段有多少女生,多少男生。


GET /your_index/your_type/_search
{ 
   "size":0,
   # 先按照年龄分组,在按照性别分组
   "aggs": {
    "group_by_age": {
      "range": {
        "field": "age",
        "ranges": [
          {
            "from": 20,
            "to": 25
          },{
            "from": 25,
            "to": 30
          },{
            "from": 30,
            "to": 35
          },{
            "from": 35,
            "to": 40
          }
        ]
      },
      "aggs": {
        "group_by_gender": {
          "terms": {
            # gender.keyword一般是ES自动为我们创建的类型
            # keyword类型的field不会分词、默认长度256字符
            # 这里大家初步了解有这个东西,知道怎么回事就行,下一篇文章扫盲
            "field": "gender.keyword"
          }
        }
       }
    }
}


5、我想知道我们公司每个年龄段,每个性别的平均账户余额。


GET /your_index/your_type/_search
{   
  "size":0,
   # 先按照年龄分组,在按照性别分组,再按照平均工资聚合
   # 最终的结果就得到了每个年龄段,每个性别的平均账户余额
   "aggs": {
    "group_by_age": {
      "range": {
        "field": "age",
        "ranges": [
          {
            "from": 20,
            "to": 30
          }
        ]
      },
      "aggs": {
        "group_by_gender": {
          "term": {
            "field": "gender.keyword"
          },
          # 在上一层根据gender聚合的基础上再基于avg balance聚合
          "aggs": {
            "average_balance": {
              "avg": {
                "field": "balance"
              }
            }
          }
        }
      }
    }
}


6、嵌套聚合,并且使用内部聚合的结果集


GET /your_index/your_type/_search
{   
  "size":0,    
   # 嵌套聚合,并且使用内部聚合的结果集
   "aggs": { 
    "group_by_state": {
      "term": {
        "field": "state.keyword",
        "order": {
          # average_balance是下面内部聚合的结果集合,在此基础上做desc
          "average_balance": "desc" 
        }
      },
      # 如下的agg会产出多个bucket如:
      # bucket1 => {state=1,acg=xxx、min=xxx、max=xxx、sum=xxx}
      # bucket2 => {state=2,acg=xxx、min=xxx、max=xxx、sum=xxx}
      "aggs": {
        "average_balance": {
          "avg": {  # avg 求平均值  metric
            "field": "balance"
          }
        },
         "min_price": {
          "min": {  # metric 求最小值
            "field": "price"
          }
        },
         "max_price": {
          "max": {  # metric 求最大值
            "field": "price"
          }
        },
         "sum_price": {
          "sum": {  # metric 计算总和
            "field": "price"
          }
        },
      }
    }
  }
}


8、除了前面说的按照值分组聚合,比如男、女,还可以使用histogram按区间聚合分析。


GET /your_index/your_type/_search
{
  "size":0,   
  # histogram,类似于terms,同样会进行bucket分组操作。
  # 使用histogram需要执行一个field,比如下例中的age,表示按照age的范围进行分组聚合
  "aggs": { # 聚合中嵌套聚合
      "group_by_price": {
            "histogram": {
                 "field": "age",
                  # interval为10,它会划分成这样 0-10  10-20  20-30 ...
                  # 那age为21的记录就会被分进20-30的区间中
                 "interval":10
             },
       "aggs": { # 聚合中嵌套聚合
            "average_price": {
               "avg": {
                  "field": "price"
               }
            }
        }
     }
  }
}


9、根据日期进行聚合


GET /your_index/your_type/_search
{   
  "size":0, 
  "aggs": {
     "agg_by_time" : { 
          # 关键字
          "date_histogram" : {
                "field" : "age",
                # 间隔,一个月为一个跨度
                "interval" : "1M",
                "format" : "yyyy-MM-dd",
                # 即使这个区间中一条数据都没有,这个区间也要返回
                "min_doc_count":0 
                # 指定区间
                “extended_bounds”:{
                  "min":"2021-01-01",
                  "max":"2021-01-01",
                }
            } 
        }
    }
}
# 补充
"interval":“quarter”按照季度划分


10、filter aggregate 过滤、聚合。


# Case1
# 如下例子:我想先过滤出年龄大于20的人,然后聚合他们的平均工资
GET /your_index/your_type/_search
{
  "size":0,
  "query":{
    "consitant_score":{
      # 这个filter会针对ES中全局的数据进行filter
      "filter":{
        "range":{"age":{"gte":20}}
      }
    }
  },
  "aggs":{
    "avg_salary":{
      "avg":{
        "field":"salary"
      }
    }
  }
}
# Case2
# bucket filter
POST /sales/_search
{
    "aggs" : {
        # T恤bucket的agg
        "agg_t_shirts" : {
            "filter" : { 
              "term": {
                "type": "t-shirt" 
              }
            },
            "aggs" : {
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        },
      # 毛衣bucket的agg
      "agg_sweater" : {
            "filter" : { 
              "term": {
                "type": "sweater" 
              }
            },
            "aggs" : {
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        }
    }
}


11、嵌套聚合-广度优先


说一个应用于场景: 我们检索电影的评论, 但是我们先按照演员分组聚合,再按照评论的数量进行聚合。且我们假设每个演员都出演了10部电影。


分析: 如果我们选择深度优先的话, ES在构建演员电影相关信息时,会顺道计算出电影下面评论数的信息,假如说有10万个演员的filter aggregate话, 10万*10=100万个电影 每个电影下又有很多影评,接着处理影评, 就这样内存中可能会存在几百万条数据,但是我们最终就需要50条,这种开销是很大的。


广度优先的话,是我们先处理电影数,而不管电影的评论数的聚合情况,先从10万演员中干掉99990条数据,剩下10个演员再聚合。


"aggs":{
            "target_actors":{
                "terms":{
                    "field":"actors",
                    "size":10,
                    "collect_mode":"breadth_first" # 广度优先
                }
            }
    }


12、global aggregation


全局聚合,下面先使用query进行全文检索,然后进行聚合, 下面的聚合实际上是针对两个不同的结果进行聚合。


  • 第一个聚合添加了global关键字,意思是ES中存在的所有doc进行聚合计算得出t-shirt的平均价格
  • 第二个聚合针对全文检索的结果进行聚合


POST /sales/_search?size=0
{
    "query" : {
        # 全文检索 type = t-shirt的商品
        "match" : { "type" : "t-shirt" }
    },
    "aggs" : {
        "all_products" : {
            "global" : {}, # 表示让 all_products 对ES中所有数据进行聚合
            "aggs" : {
                # 没有global关键字,表示针对全文检索的结果进行聚合
                "avg_price" : { "avg" : { "field" : "price" } }
            }
        },
        "t_shirts": { "avg" : { "field" : "price" } }
    }
}


13、Cardinality Aggregate 基数聚合


在ES中聚合时去重一般选用cardinality metric,它可以实现对每一个bucket中指定的field进行去重,最终得到去重后的count值。


虽然她会存在5%左右的错误率,但是性能特别好


POST /sales/_search?size=0
{
    "aggs" : {
        # 先按照月份聚合得到不同月的bucket
        "agg_by_month" : {
            "date_histogram":{
              "field" : "my_month",
              "internal":"month"
            }
        },
        # 在上一步得到的以月份为维护划分的bucket基础上,再按照品牌求基数去重。
        # 于是最终我们就得到了每个月、每种品牌的销售量。
        "aggs" : {
          "dis_by_brand" : {
              "cardinality" : { 
                 "field" : "brand"
            }
        }
    }
}


对Cardinality Aggregate的性能优化, 添加 precision_threshold 优化准确率和内存的开销。


还是下面的例子,如果将precision_threshold的值调整到100意思是:当品牌的总数量小于100时,去重的精准度为100%, 此时内存的占用情况为 100*8=800字节。

加入我们将这个值调整为1000,意思是当品台的种类在1000个以内时,去重的精准度100%,内存的占用率为1000*8=80KB。


官方给出的指标是:将precision_threshold设置为5时,错误率会被控制在5%以内。


POST /sales/_search?size=0
{
    "aggs" : {
        "type_count" : {
            "cardinality" : { # 关键字
                "field" : "brand"
                "precision_threshold":100
            }
        }
    }
}


进一步优化,Cardinality底层使用的算法是 HyperLogLog++。


因为这个算法的底层会对所有的 unique value取hash值,利用这个hash值去近似的求distcint count, 因此我们可以在创建mapping时,将这个hash的求法设置好,添加doc时,一并计算出这个hash值,这样 HyperLogLog++ 就无需再计算hash值,而是直接使用。从而达到优化速度的效果。


PUT /index/
{
    "mappings":{
        "my_type":{
            "properties":{
                "my_field":{
                    "type":"text",
                    "fields":{
                        "hash":{
                            "type":"murmu3"
                        }
                    }
                }
            }
        }
    }
}


14、控制聚合的升降序


比如我想知道每种颜色item的平均价格,并且我希望按照价格的从小到大升序展示给我看。


于是就像下面这样,先按照颜色聚合可以将相同颜色的item聚合成1组,在聚合的结果上再根据价格进行聚合。期望在最终的结果中,通过order控制按照价格聚合的分组中升序排序, 这算是个在下钻分析时的排序技巧。


GET /index/type/_search
{
     "size":0,
     "aggs":{
         "group_by_color":{
             "term":{
                 "field":"color",
                 "order":{ #
                     "avg_price":"asc"
                 }
             }
         },
         "aggs":{
             # 在上一层按color聚合的基础上,再针对price进行聚合
             "avg_price":{
                 "avg":{
                     "field":"price"
                 }
             }
         }
     }
}


15、Percentiles Aggregation


计算百分比, 常用它计算:在200ms内成功访问网站的比率、在500ms内成功访问网站的比例、在1000ms内成功访问网站的比例,或者是销售价为1000元的商品占总销售量的比例、销售价为2000元的商品占总销售量的比例等等。

示例: 针对doc中的 load_time字段, 计算出在不同百分比下面的 load_time_outliner情况。


GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            # 关键字
            "percentiles" : {
                "field" : "load_time" 
            }
        }
    }
}


响应解读:在百分之50的加载请求中,平均load_time的时间是在445.0。 在99%的请求中,平均加载时间980.1。


{
    ...
   "aggregations": {
      "load_time_outlier": {
         "values" : {
            "1.0": 9.9,
            "5.0": 29.500000000000004,
            "25.0": 167.5,
            "50.0": 445.0,
            "75.0": 722.5,
            "95.0": 940.5,
            "99.0": 980.1000000000001
         }
      }
   }
}


还可以自己指定百分比跨度间隔。


GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95,99,99.9] 
            }
        }
    }
}


优化: percentile底层使用的是 TDigest算法。用很多个节点执行百分比计算,近似估计,有误差,节点越多,越精准。


可以设置compression的值, 默认是100 , ES限制节点的最多是 compression*20 =2000个node去计算 , 因为节点越多,性能就越差。


一个节点占用 32字节, 1002032 = 64KB。


GET latency/_search
{
    "size": 0,
    "aggs" : {
        "load_time_outlier" : {
            "percentiles" : {
                "field" : "load_time",
                "percents" : [95,99,99.9],
                "compression":100 # 默认值100
            }
        }
    }
}


参考:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations.html


五、7个查询优化技巧#


  • 第一种:多字段检索,巧妙控制权重
  • 第一种: 更换写法,改变占用的权重比例。
  • 第三种: 如果不希望使用相关性得分,使用下面的语法。
  • 第四种: 灵活的查询
  • 第五种: 比如我对title字段进行检索,我希望检索结果中包含"java",并且我允许检索结果中包含:”golang“ ,但是!如果检索结果中包含”golang“,我希望这个title中包含”golang“的doc的排名能靠后一些。
  • 第六种: 重打分机制
  • 第七种: 提高召回率和精准度的技巧:混用match和match_phrase+slop提高召回率。注意下面的嵌套查询层级 bool、must、should



上面的七种优化相关性得分的方式的具体实现代码,在公众号原文中可以查看到,推荐阅读原文,json的格式会好看很多,ES专题依然在连载中~,欢迎关注。

点击阅读原文,查看7种优化方式的具体实现代码

点击阅读原文,查看7种优化方式的具体实现代码

点击阅读原文,查看7种优化方式的具体实现代码


参考:

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.0

query dsl:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl.html

聚合分析:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations.html



欢迎关注#


点击阅读原文,怎么关注我你懂的~

相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
JSON Ubuntu Java
Elasticsearch聚合学习之四:结果排序
在前面的实战中,聚合的结果以桶(bucket)为单位,放在JSON数组中返回,这些数据是没有排序的,今天来学习如何给这些数据进行排序
452 0
Elasticsearch聚合学习之四:结果排序
|
3月前
|
JSON 自然语言处理 算法
ElasticSearch基础2——DSL查询文档,黑马旅游项目查询功能
DSL查询文档、RestClient查询文档、全文检索查询、精准查询、复合查询、地理坐标查询、分页、排序、高亮、黑马旅游案例
ElasticSearch基础2——DSL查询文档,黑马旅游项目查询功能
|
4月前
|
消息中间件 监控 数据挖掘
Elasticsearch 使用误区之二——频繁更新文档
【8月更文挑战第15天】在大数据与搜索技术日益成熟的今天,Elasticsearch 作为一款分布式、RESTful 风格的搜索与数据分析引擎,凭借其强大的全文搜索能力和可扩展性,成为了众多企业和开发者的首选。然而,在使用 Elasticsearch 的过程中,一些常见的误区可能会导致性能下降或数据不一致等问题,其中“频繁更新文档”便是一个不容忽视的误区。本文将深入探讨这一误区的根源、影响及解决方案,帮助读者更好地利用 Elasticsearch。2
81 0
|
SQL 关系型数据库 索引
Elasticsearch查询时还在百度DSL语句吗?你可能需要这份总结
Elasticsearch查询时还在百度DSL语句吗?你可能需要这份总结
|
JSON 安全 搜索推荐
白日梦的Elasticsearch实战笔记,32个查询案例、15个聚合案例、7个查询优化技巧(一)
白日梦的Elasticsearch实战笔记,32个查询案例、15个聚合案例、7个查询优化技巧(一)
1031 0
|
SQL 关系型数据库 MySQL
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(五)
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(五)
88 0
|
缓存
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(三)
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(三)
84 0
|
自然语言处理
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(二)
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(二)
79 0
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(四)
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(四)
67 0
|
自然语言处理 Java API
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践(一)
【ElasticSearch从入门到放弃系列 十一】Elasticsearch常用查询方式讨论及实践
133 0