天天看点

(六)ElasticSearch 6.1.1聚合查询1 普通类型2 nested类型参考资料

1 普通类型

1.1 基本操作

1.1.1 导入实战数据

数据字段如下:

字段 类型 作用
price long 汽车售价
color text 汽车颜色
make text 汽车品牌
sold date 销售日期
# 创建索引
PUT /cars
{
  "mappings" : {
      "transactions" : {
        "properties" : {
          "color" : {
            "type" : "keyword"
          },
          "make" : {
            "type" : "keyword"
          },
          "price" : {
            "type" : "long"
          },
          "sold" : {
            "type" : "date"
          }
        }
      }
    }
}

# 导入数据
POST /cars/transactions/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }
           

1.1.2 最简单的聚合:terms桶

# 第一个聚合命令是terms桶,相当于SQL中的group by,将所有记录按照颜色聚合
GET /cars/transactions/_search
{
  "size":0,
  "aggs":{
   "popular_colors":{
     "terms": {
       "field": "color"
     }
   } 
  }
}
           
(六)ElasticSearch 6.1.1聚合查询1 普通类型2 nested类型参考资料

1.1.3 添加度量指标

  • 上面的示例返回的是每个桶中的文档数量,接下es支持丰富的指标,例如平均值(Avg)、最大值(Max)、最小值(Min)、累加和(Sum)等,接下来试试累加和的用法;
  • 下面请求的作用是统计每种颜色汽车的销售总额:
GET /cars/transactions/_search
{
  "size":0,
  "aggs":{
   "colors":{
     "terms": {
       "field": "color"
     },
     "aggs":{
       "sales":{
         "sum":{
           "field":"price"
         }
       }
     }
   } 
  }
}

# 解释
GET /cars/transactions/_search
{
  "size":0,
  "aggs":{         ------和前面一样,指定聚合操作
   "colors":{      ------别名
     "terms": {    ------桶类型是按指定字段聚合
       "field": "color" ------按照color字段聚合
     },
     "aggs":{      ------新增的aggs对象,用于处理聚合在每个桶内的文档
       "sales":{   ------别名
         "sum":{   ------度量指标是指定字段求和
           "field":"price" ---求和的字段是price
         }
       }
     }
   } 
  }
}

 "aggregations" : {               ------聚合结果
    "colors" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [               ------这个json数组的每个对象代表一个桶
        {
          "key" : "red",        ------该桶将所有color等于red的文档聚合进来
          "doc_count" : 4,      ------有4个color等于red的文档
          "sales" : {           ------这里面是sum计算后的结果  
            "value" : 130000.0  ------所有color等于red的汽车销售总额
          }
        },
        {
          "key" : "blue",
          "doc_count" : 2,
          "sales" : {
            "value" : 40000.0  ------所有color等于blue的汽车销售总额
          }
        },

           
(六)ElasticSearch 6.1.1聚合查询1 普通类型2 nested类型参考资料

1.2 区间聚合

1.2.1条形图(histogram桶)

以汽车销售记录为例做一次聚合查询,为售价创建histogram桶,以20000作为间隔,每个桶负责的区间如上图所示,相关的销售记录就会被放入对应的桶中,请求参数和说明如下:

# 可执行查询
GET /cars/transactions/_search
{
  "size":0,                  
  "aggs":{                   
   "price":{                 
     "histogram": {          
       "field": "price",     
       "interval": 20000     
     }
   } 
  }
}
# 解释
GET /cars/transactions/_search
{
  "size":0,                  ---令返回值的hits对象为空
  "aggs":{                   ---聚合命令
   "price":{                 ---聚合字段名称
     "histogram": {          ---桶类型
       "field": "price",     ---指定price字段的值作为判断条件
       "interval": 20000     ---每个桶负责的区间大小为20000
     }
   } 
  }
}


# 返回结果
  "aggregations" : {             ---聚合结果
    "price" : {                  ---请求参数中指定的名称
      "buckets" : [              ---price桶的数据在此数组中
        {
          "key" : 0.0,           ---第一个桶,区间[0-19999],0.0是起始值
          "doc_count" : 3        ---这个区间有三个文档(price值分别是10000、12000、15000)
        },
        {
          "key" : 20000.0,       ---第二个桶,区间[20000-39999],20000.0是起始值
          "doc_count" : 4        ---这个区间有四个文档
        },
        {
          "key" : 40000.0,       ---第三个桶,区间[40000-59999],40000.0是起始值
          "doc_count" : 0        ---这个区间没有文档
        },
       ......
           

1.2.2 控制空桶是否返回

在上面的返回值中,第三个桶中没有文档,在有的业务场景中,我们不需要没有数据的桶,此时可以用min_doc_count参数来控制,如果min_doc_count等于2,表示桶中最少有两条记录才会出现在返回内容中,如下所示,min_doc_count如果等于1,那么空桶就不会被es返回了:

GET /cars/transactions/_search
{
  "size":0,
  "aggs":{
   "price":{
     "histogram": {
       "field": "price",
       "interval": 20000,
       "min_doc_count": 1
     }
   } 
  }
}

           

1.2.3 histogram桶加metrics

上面的例子返回结果只有每个桶内的文档数,也可以加入metrics对桶中的数据进行处理,例如计算每个区间内的最高价、最低价、平均售价,可以加入max、min、avg参数,如下:

# 可执行查询
GET /cars/transactions/_search
{
  "size":0,                  
  "aggs":{                   
   "price":{                 
     "histogram": {          
       "field": "price",     
       "interval": 20000,    
        "min_doc_count": 1
     },
     "aggs": {               
        "max_price": {       
          "max":{            
            "field": "price" 
          }
        },
        "min_price": {       
          "min":{            
            "field": "price" 
          }
        },
        "avg_price": {       
          "avg":{            
            "field": "price" 
          }
        }
      }
   } 
  }
}

# 解释
GET /cars/transactions/_search
{
  "size":0,                  ---令返回值的hits对象为空
  "aggs":{                   ---聚合命令
   "price":{                 ---聚合字段名称
     "histogram": {          ---桶类型
       "field": "price",     ---指定price字段的值作为判断条件
       "interval": 20000     ---每个桶负责的区间大小为20000
     },
     "aggs": {               ---表示对桶内数据做metrics
        "max_price": {       ---指定metrics处理结果的字段名
          "max":{            ---metrics类型为max
            "field": "price" ---指定取price字段的值做最大值比较
          }
        },
        "min_price": {       ---指定metrics处理结果的字段名
          "min":{            ---metrics类型为min
            "field": "price" ---指定取price字段的值做最小值比较
          }
        },
        "avg_price": {       ---指定metrics处理结果的字段名
          "avg":{            ---metrics类型为avg
            "field": "price" ---指定取price字段的值计算平均值
          }
        }
      }
   } 
  }
}

# 返回结果
  "aggregations" : {             ---聚合结果
    "price" : {                  ---请求参数中指定的名称
      "buckets" : [              ---price桶的数据在此数组中
        {
          "key" : 0.0,           ---第一个区间[0-19999],0.0是起始值
          "doc_count" : 3,       ---这个区间有三条记录(price值分别是10000、12000、15000)
          "max_price" : {        ---指定的metrics结果名称
            "value" : 15000.0    ---桶中有三个文档,price字段的最大值是15000
          },
          "min_price" : {
            "value" : 10000.0    ---桶中有三个文档,price字段的最小值是10000
          },
          "avg_price" : {
            "value" : 12333.333333333334    ---桶中有三个文档,price字段的平均值是12333.333333333334
          }
        },
       ......

           

1.2.4 时间区间的桶(date_histogram)

  • histogram桶可以实现按照时间分段么?如果用毫秒数来处理,似乎是可以的,但是对年月日的处理就力不从心了,常见的时间区间处理,用date_histogram桶即可满足要求;
  • 下面就是date_histogram桶的用法:每月销售多少台汽车:
GET /cars/transactions/_search
{
# 可执行查询
GET /cars/transactions/_search
{
  "size": 0,                    
  "aggs": {                     
    "sales": {                  
      "date_histogram": {       
        "field": "sold",        
        "interval": "month",    
        "format": "yyyy-MM-dd"  
      },
      "aggs": {                 
        "max_price": {          
          "max":{               
            "field": "price"    
          }
        },
        "min_price": {          
          "min":{               
            "field": "price"    
          }
        }
      }
    }
  }
}

# 解释
  "size": 0,                    ---令返回值的hits对象为空
  "aggs": {                     ---聚合命令
    "sales": {                  ---聚合字段名称
      "date_histogram": {       ---桶类型
        "field": "sold",        ---用sold字段的值作进行时间区间判断
        "interval": "month",    ---间隔单位是月
        "format": "yyyy-MM-dd"  ---返回的数据中,时间字段格式
      },
      "aggs": {                 ---表示对桶内数据做metrics
        "max_price": {          ---指定metrics处理结果的字段名
          "max":{               ---metrics类型为max
            "field": "price"    ---指定取price字段的值做最大值比较
          }
        },
        "min_price": {          ---指定metrics处理结果的字段名
          "min":{               ---metrics类型为min
            "field": "price"    ---指定取price字段的值做最小值比较
          }
        }
      }
    }
  }
}

# es返回
"aggregations" : {                           ---聚合结果
    "sales" : {                              ---请求参数中指定的名称
      "buckets" : [                          ---sales桶的数据在此数组中
        {
          "key_as_string" : "2014-01-01",    ---请求的format参数指定了key的格式
          "key" : 1388534400000,             ---真正的时间字段
          "doc_count" : 1,                   ---2014年1月份的文档数量
          "max_price" : {                    ---2014年1月的文档做了metrics类型为max的处理后,结果在此
            "value" : 80000.0                ---2014年1月的文档中,price字段的最大值
          },
          "min_price" : {                    ---2014年1月的文档做了metrics类型为min的处理后,结果在此
            "value" : 80000.0                ---2014年1月的文档中,price字段的最大值
          }
        },
        {
          "key_as_string" : "2014-02-01",
          "key" : 1391212800000,
          "doc_count" : 1,
          "max_price" : {
            "value" : 25000.0
          },
          "min_price" : {
            "value" : 25000.0
          }
        },
        ......

           

1.2.5 连续多次聚合

连续分桶,第二次分桶一定是在第一次分桶的基础上,对每一个桶再进行二次分桶,这一点可以在{}范围上看出,global关键字可以突破这个限制,但是两次分桶的结果仍然遵循{}的层次,这一点可以在全局桶体会到。

  • 来做一个略为复杂的聚合操作:按季度展示每个汽车品牌的销售总额;
  • 操作的第一步是按照时间区间做聚合,然后在每个桶中,将文档按照品牌做第二次聚合,第二次聚合的结果也可以理解为多个桶,每个桶中的文档,是某个平台在某个季度的销售总额;
# 可执行
GET /cars/transactions/_search
{
  "size": 0,                      
  "aggs": {                       
    "sales": {                    
      "date_histogram": {         
        "field": "sold",          
        "interval": "1q",         
        "format": "yyyy-MM-dd",   
        "min_doc_count": 1        
      },
      "aggs": {                   
        "per_make_sum": {         
          "terms": {              
            "field": "make"       
          },
          "aggs": {               
            "sum_price": {        
              "sum": {            
                "field": "price"  
              
            }
          }
        }
      }
    }
  }
}
}

# 解释
GET /cars/transactions/_search
{
  "size": 0,                      ---令返回值的hits对象为空
  "aggs": {                       ---聚合命令
    "sales": {                    ---聚合字段名称
      "date_histogram": {         ---桶类型为时间区间
        "field": "sold",          ---指定sold字段的值作为判断条件
        "interval": "1q",         ---区间间隔为1季度
        "format": "yyyy-MM-dd",   ---返回的桶的key,被格式化时的格式
        "min_doc_count": 1        ---空桶不返回
      },
      "aggs": {                   ---第二层桶
        "per_make_sum": {         ---聚合字段名称
          "terms": {              ---桶类型为terms
            "field": "make"       ---按照make字段聚合
          },
          "aggs": {               ---第二层桶的metrics
            "sum_price": {        ---聚合字段名称
              "sum": {            ---metrics处理,累加
                "field": "price"  ---取price字段的值累加
              }
            }
          }
        }
      }
    }
  }
}

# es返回
"aggregations" : {
    "sales" : {
      "buckets" : [                             ---聚合结果
        {
          "key_as_string" : "2014-01-01",       ---当前桶的key的格式化后的值
          "key" : 1388534400000,                ---当前桶的key原值
          "doc_count" : 2,                      ---当前桶中文档数
          "per_make_sum" : {                    ---第二层桶的名称
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [                       ---第二层聚合结果
              {
                "key" : "bmw",                  ---聚合字段的值,这里是汽车品牌
                "doc_count" : 1,                ---桶内的文档数量
                "sum_price" : {                 ---metrics处理结果名称
                  "value" : 80000.0             ---metrics处理结果,这里是销售额累加值
                }
              },
              {
                "key" : "ford",                 ---聚合字段的值,这里是汽车品牌
                "doc_count" : 1,                ---桶内的文档数量
                "sum_price" : {                 ---metrics处理结果名称
                  "value" : 25000.0             ---metrics处理结果,这里是销售额累加值
              
           

1.3 范围限定

1.3.1 最简单的查询范围

福特汽车一共分为几种颜色?这就是最简单的范围限定聚合(限定了汽车品牌),查询DSL如下:

# 可执行
GET /cars/transactions/_search
{
  "size":0,
  "query": {                
    "term": {               
      "make": "ford"        
    }
  }, 
  "aggs":{                  
   "popular_colors":{       
     "terms": {             
       "field": "color"     
     }
   } 
  }
}

# 解释
GET /cars/transactions/_search
{
  "size":0,
  "query": {                ---范围限定的查询
    "term": {               ---查询类型是精确匹配
      "make": "ford"        ---查询条件是品牌为福特
    }
  }, 
  "aggs":{                  ---聚合
   "popular_colors":{       ---聚合字段名
     "terms": {             ---桶类型
       "field": "color"     ---匹配字段是color
     }
   } 
  }
}

# es返回
  "aggregations" : {                       ---聚合结果
    "popular_colors" : {                   ---聚合字段
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [                        ---这个数组的元素是所有的桶
        {
          "key" : "blue",                  ---color为blue的文档
          "doc_count" : 1                  ---文档数为1
        },
        {
          "key" : "green",                 ---color为blue的文档
          "doc_count" : 1                  ---文档数为1
        }
      ]
    }
  }
}

           

1.3.2 全局桶

如果想对比福特汽车的销售额和所有汽车的销售额,可以通过全局桶对所有文档做聚合,关键字是global,全局桶的聚合不受范围限定的影响:

# 可执行
GET /cars/transactions/_search
{
  "size": 0,
  "query": {			    
    "term": {               
      "make": "ford"        
    }
  },
  "aggs": {                  
    "ford_sales": {          
      "sum": {               
        "field": "price"     
      }
    },
    "all": {                 
      "global": {},          
      "aggs": {              
        "all_sales": {       
          "sum": {           
            "field": "price" 
          }
        }
      }
    }
  }
}

# 解释
GET /cars/transactions/_search
{
  "size": 0,
  "query": {			     ---范围限定的查询
    "term": {                ---查询类型是精确匹配
      "make": "ford"         ---查询条件是品牌为福特
    }
  },
  "aggs": {                  ---聚合
    "ford_sales": {          ---聚合字段名
      "sum": {               ---直接对范围内的所有文档执行metrics,类型是累加
        "field": "price"     ---选择price字段的值进行累加
      }
    },
    "all": {                 ---聚合字段名
      "global": {},          ---全局桶关键字,表示忽略前面term查询的范围限定
      "aggs": {              ---聚合
        "all_sales": {       ---聚合字段名
          "sum": {           ---直接对范围内的所有文档执行metrics,类型是累加
            "field": "price" ---选择price字段的值进行累加
          }
        }
      }
    }
  }
}
# es返回
......
  "aggregations" : {         ---聚合结果
    "all" : {                ---全局桶的聚合结果(term查询无效)
      "doc_count" : 8,       ---文档总数
      "all_sales" : {        ---聚合字段名
        "value" : 212000.0   ---总销售额
      }
    },
    "ford_sales" : {         ---聚合字段名(term查询限定了范围,只有福特汽车的销售记录)
      "value" : 55000.0      ---福特汽车销售额
    }
  }
}

           

1.3.3 使用filter提高查询性能

虽然query和filter限定范围的结果是一样的,但是filter会忽略评分,并且有可能缓存结果数据,这些都是性能上的优势。

前面的范围限定用到了query,其实适用于查询的过滤器也能应用在聚合操作中,下面是过滤+聚合的查询,和前面一样,也是统计总销售和和福特汽车的销售额:

# 可执行
GET /cars/transactions/_search
{
  "size": 0,
  "query": {
    "bool": {                 
      "filter": {             
        "term": {             
          "make": "ford"      
        }
      }
    }
  },
  "aggs": {                   
    "ford_sales": {           
      "sum": {                
        "field": "price"      
      }
    },
    "all": {                  
      "global": {},           
      "aggs": {               
        "all_sales": {        
          "sum": {            
            "field": "price"  
          }
        }
      }
    }
  }
}

# 解释
GET /cars/transactions/_search
{
  "size": 0,
  "query": {
    "bool": {                 ---布尔查询,里面可以将query和filter组合使用
      "filter": {             ---本例只用到了filter
        "term": {             ---精确匹配
          "make": "ford"      ---匹配福特品牌 
        }
      }
    }
  },
  "aggs": {                   ---聚合结果
    "ford_sales": {           ---聚合字段名
      "sum": {                ---metrics操作,累加
        "field": "price"      ---累加字段是price
      }
    },
    "all": {                  ---聚合字段名                  
      "global": {},           ---全局桶关键字,表示忽略范围限定
      "aggs": {               ---聚合
        "all_sales": {        ---聚合字段名
          "sum": {            ---metrics操作,累加
            "field": "price"  ---累加字段是price
          }
        }
      }
    }
  }
}

           

1.3.4 桶内filter

学习桶内filter之前,先看看官方的布尔查询DSL,如下所示,查询JSON对象的内部可以加入filter,对查询结果做过滤:

GET /_search
{
  "query": { 
    "bool": { 
      "must": [                                     ---布尔查询
        { "match": { "title":   "Search"        }}, 
        { "match": { "content": "Elasticsearch" }}  
      ],
      "filter": [                                   ---对查询结果做过滤
        { "term":  { "status": "published" }}, 
        { "range": { "publish_date": { "gte": "2015-01-01" }}} 
      ]
    }
  }
}

           
  • 桶内filter和布尔查询中的filter类似,对进入桶中的数据可以加入filter,这样桶内的数据就是此filter过滤后的数据了;
  • 举个例子,统计蓝色的福特汽车销售额,首先限定品牌范围,这个可以直接用之前的限定方式,然后在桶内加入一个filter,只保留颜色为蓝色的文档:
GET /cars/transactions/_search
{
  "size": 0,
  "query": {
    "bool": {                 ---布尔查询,里面可以将query和filter组合使用
      "filter": {             ---本例只用到了filter
        "term": {             ---精确匹配
          "make": "ford"      ---匹配福特品牌 
        }
      }
    }
  },
  "aggs": {
    "sales": {
      "filter": {             ---桶内filter
        "term": {             ---精确匹配
          "color": "blue"     ---匹配蓝色
        }
      },
      "aggs": {
        "blue_sales": {
          "sum": {            ---metrics操作,累加
            "field": "price"
          }
        }
      }
    }
  }
}

# es返回
  "hits" : {
    "total" : 2,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "sales" : {
      "doc_count" : 1,
      "green_sales" : {
        "value" : 25000.0
      }
    }
  }
}
           

1.4 结果排序

1.4.1 默认排序

之前文章中的聚合查询,我们都没有做排序设置,此时es会用每个桶的doc_count字段做降序,下图是个terms桶聚合的示例,可见返回了三个bucket对象,是按照doc_count字段降序排列的:

1.4.2 内置排序

除了自定义排序,es自身也内置了两种排序参数,可以直接拿来使用:

# _count:这个参数对应的就是doc_count,以下请求的排序效果和默认的排序效果是一致
GET /cars/transactions/_search
{
  "size":0,
  "aggs":{
   "popular_colors":{
     "terms": {
       "field": "color",
       "order": {             ---表示要对聚合结果做排序
         "_count": "desc"     ---排序字段是doc_count,顺序是降序
       }
     }
   } 
  }
}

# _key:在区间聚合的时候(histogram或者date_histogram),可以根据桶的key做排序:
GET /cars/transactions/_search
{
  "size": 0,
  "aggs": {
    "price": {
      "histogram": {           ---区间聚合
        "field": "price",      ---取price字段的值
        "interval": 20000,     ---每个区间的大小是20000
        "order": {             ---表示要对聚合结果做排序
          "_key": "desc"       ---排序字段是桶的key值,这里是每个区间的起始值,顺序是降序
        }
      }
    }
  }
}
# es返回
  ......
  "aggregations" : {
    "price" : {
      "buckets" : [
        {
          "key" : 80000.0,
          "doc_count" : 1
        },
        {
          "key" : 60000.0,
          "doc_count" : 0
        },
        {
          "key" : 40000.0,
          "doc_count" : 0
        },
        {
          "key" : 20000.0,
          "doc_count" : 4
        },
        {
          "key" : 0.0,
          "doc_count" : 3
        }
      ]
    }
  }
}

           

2 nested类型

2.1 一个官网例子

A special single bucket aggregation that enables aggregating nested documents.

For example, lets say we have an index of products, and each product holds the list of resellers - each having its own price for the product. The mapping could look like:

# 可执行
# resellers is an array that holds nested documents.
PUT /products
{
    "mappings": {
      "Apple":{
        "properties" : {
            "resellers" : { 
                "type" : "nested",
                "properties" : {
                    "reseller" : { "type" : "text" },
                    "price" : { "type" : "double" }
                }
            }
        }
      }
    }
}

# We are using a dynamic mapping for the name attribute.
PUT /products/Apple/0
{
  "name": "LED TV", 
  "resellers": [
    {
      "reseller": "companyA",
      "price": 350
    },
    {
      "reseller": "companyB",
      "price": 500
    }
  ]
}

# The following request returns the minimum price a product can be purchased for
GET /products/_search
{
  "size": 0,
    "query" : {
        "match" : { "name" : "led tv" }
    },
    "aggs" : {
        "resellers" : {
            "nested" : {
                "path" : "resellers"
            },
            "aggs" : {
                "min_price" : { "min" : { "field" : "resellers.price" } }
            }
        }
    }
}
           

As you can see above, the nested aggregation requires the path of the nested documents within the top level documents. Then one can define any type of aggregation over these nested documents.

{
  ...
  "aggregations": {
    "resellers": {
      "doc_count": 2,
      "min_price": {
        "value": 350
      }
           

参考资料

  1. https://blog.csdn.net/boling_cavalry/article/details/89735952 详细讲解了聚合查询
  2. https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html官网例子