Elasticsearch 聚合搜索
当用户使用搜索引擎完成搜索后,在展示结果中需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。
环境准备
- Elasticsearch 服务(单机或集群)
- Kibana 服务
如果对ES不了解或没有上述环境,可以看下我之前的博客。
Elasticsearch入门基础和集群部署
Elasticsearch查看集群信息,设置ES密码,Kibana部署
数据准备
PUT /course
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "teacher_name": {
        "type": "text"
      },
      "price": {
        "type": "double"
      },
      "create_time": {
        "type": "date",
        "format": [
          "yyyy-MM-dd HH:mm:ss"
        ]
      },
      "tags": {
        "type": "keyword"
      },
      "publish": {
        "type":"boolean"
      },
      "comment_info":{
        "properties": {
          "favourable_num":{
            "type":"integer"
          },
          "negative_num":{
            "type":"integer"
          }
        }
      }
    }
  }
}
PUT /_bulk
{"index":{"_index":"course","_id":"1"}}
{"title":"Python编程基础","teacher_name":"张三","price":99.99,"create_time":"2023-04-01 10:00:00","tags":["编程","Python"],"publish":true,"comment_info":{"favourable_num":150,"negative_num":3}}
{"index":{"_index":"course","_id":"2"}}
{"title":"Java高级开发","teacher_name":"李四","price":129.5,"create_time":"2023-06-08 15:30:00","tags":["Java","后端开发"],"publish":true,"comment_info":{"favourable_num":200,"negative_num":7}}
{"index":{"_index":"course","_id":"3"}}
{"title":"数据结构与算法","teacher_name":"王五","price":88.88,"create_time":"2023-03-15 14:45:00","tags":["算法","数据结构"],"publish":false,"comment_info":{"favourable_num":50,"negative_num":0}}
{"index":{"_index":"course","_id":"4"}}
{"title":"Web前端开发入门","teacher_name":"赵六","price":79.9,"create_time":"2023-05-20 09:15:00","tags":["HTML","CSS","JavaScript"],"publish":true,"comment_info":{"favourable_num":88,"negative_num":2}}
{"index":{"_index":"course","_id":"5"}}
{"title":"机器学习实战","teacher_name":"孙七","price":159,"create_time":"2023-02-25 11:00:00","tags":["机器学习","人工智能"],"publish":true,"comment_info":{"favourable_num":120,"negative_num":5}}
{"index":{"_index":"course","_id":"6"}}
{"title":"数据库原理与设计","teacher_name":"周八","price":66.6,"create_time":"2023-01-10 13:30:00","tags":["数据库","SQL"],"publish":false,"comment_info":{"favourable_num":30,"negative_num":1}}
{"index":{"_index":"course","_id":"7"}}
{"title":"Android应用开发","teacher_name":"吴九","price":119.88,"create_time":"2023-04-20 17:45:00","tags":["Android","移动开发"],"publish":true,"comment_info":{"favourable_num":105,"negative_num":4}}
{"index":{"_index":"course","_id":"8"}}
{"title":"深度学习探索","teacher_name":"郑十","price":299,"create_time":"2023-05-05 16:00:00","tags":["深度学习","神经网络"],"publish":true,"comment_info":{"favourable_num":180,"negative_num":6}}
{"index":{"_index":"course","_id":"9"}}
{"title":"UI/UX设计精髓","teacher_name":"钱十一","price":55.55,"create_time":"2023-03-20 12:15:00","tags":["UI设计","用户体验"],"publish":true,"comment_info":{"favourable_num":75,"negative_num":2}}
{"index":{"_index":"course","_id":"10"}}
{"title":"云计算技术基础","teacher_name":"孙十二","create_time":"2023-07-01 18:30:00","tags":["云计算","AWS"],"publish":false,"comment_info":{"favourable_num":45,"negative_num":0}}
基础聚合
# 聚合指令
"aggs":{
    // 指定聚合内容
    "value_count_price":{
      // 指定方法
      "value_count": {
        //指定聚合字段
        "field": "price"
      }
    }
  }
基础聚合方法
- 平均值 avg
- 最大 max
- 最小 min
- 计数 value_count
- 求和 sum
- 统计聚合 stats

桶聚合(分组聚合)
在ES中,MYSQL 中的 Group by 分组 被称为 桶聚合
单维度桶聚合
单维度桶聚合 就是 按照一个维度对文档 进行分组聚合
 在桶聚合时匹配方式
- terms
- ranges
- filter
GET /course/_search
{
  // 桶聚合默认 计算每个桶对应的文档数
  "size": 0,
  "aggs":{
    "agg_terms_tags":{
      // 默认只返回十个桶,即size为10 
      "terms": {
        "field": "tags"
      }
    },
    "agg_terms_publish":{
      "terms": {
        "field": "publish",
        "size": 10
      }
    },
    "agg_range_price":{
      "range": {
        "field": "price",
        "ranges": [
          //不指定from (-∞,80)
          {
            "to": 80
          },
          // 左闭右开 [80,100)
          {
            "from": 80,
            "to": 100
          },
          //不指定to [100,+∞)
          {
            "from": 100
          }
        ]
      }
    },
    "agg_filter_price": {
      "filter": {
        "match": {
          "publish": "true"
        }
      }
    }
  }
}

 注意: 这里多了一个 key_as_string 字段。


 
在默认情况下,进行桶聚合时如果不指定指标,则ES默认聚合的是文档计数,该值以doc_count为key存储在每一个bucket子句中。
返回的doc_count 是近似值,并不是一个准确数,
 因此在聚合外围,ES给出了两个参考值doc_count_error_upper_bound 和 sum_other_doc_count
doc_count error_upper表示被遗漏的文档数量可能存在的最大值。
 sum other doc count表示除了返回给用户的文档外剩下的文档总数。
多维度桶聚合
通常,在一些复杂业务中,单维度的桶聚合无法满足需求。
 往往需要引入多维度的嵌套桶聚合。
例如:分别获取发布和未发布状态 价格 在 (-∞,80),[80,100),[100,+∞) 的 价格平均值
转化为DSL如下:
GET /course/_search
{
  "size": 0,
  "aggs": {
    // 第一层分组桶:按照发布状态分组
    "publish_group": {
      "terms": {
        "field": "publish"
      },
      "aggs": {
        // 第二层分组:按照价格区间分组
        "price_range_group": {
          "range": {
            "field": "price",
            "ranges": [
              {
                "to": 80
              },
              {
                "from": 80,
                "to": 100
              },
              {
                "from": 100
              }
            ]
          },
          "aggs": {
            // 聚合计算平均值,也可以算一层
            "ans_avg": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
  }
}

 第一个桶 publish 为 true 时,内部嵌套了 价格区间的桶,每个价格区间内又有计算的 平均值 ans_avg

 第二个桶 publish 为 false 时,内部嵌套了 价格区间的桶,每个价格区间内又有计算的 平均值 ans_avg
聚合方式
ES支持灵活的聚合方式,它不仅支持聚合和查询相结合,而且还可以使聚合的过滤条件不影响搜索条件,并且还支持在聚合后的结果中进行过滤选。
直接聚合
直接聚合指的是聚合时的DSL没有query子句,是直接对索引内的所有文档进行聚合。
 前面的案例都是使用直接聚合方式
先查询后聚合
与直接聚合相对应,这种查询方式需要增加query子句,query子句和普通的query查询没有区别,参加聚合的文档必须匹配query查询。
例如:查询发布状态的 课程 在 价格区间 (-∞,80),[80,100),[100,+∞) 中的平均价格
GET /course/_search
{
  "query": {
    "match": {
      "publish": "true"
    }
  },
  "size": 0,
  "aggs": {
    "price_range_group": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "to": 80
          },
          {
            "from": 80,
            "to": 100
          },
          {
            "from": 100
          }
        ]
      },
      "aggs": {
        "ans_avg": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

前过滤器
有时需要对聚合条件进一步地过滤,但是又不能影响当前的查询条件。
例如:查询全部课程,并计算已经上架的课程的平均价格
这时 需要用到 filter 关键字,在聚合前进行过滤,但不影响query数据
对应DSL语句为:
GET /course/_search
{
  "size": 0,
  "aggs": {
    "my_aggs": {
      "filter": {
        "term": {
          "publish": "true"
        }
      },
      "aggs": {
        "ans_avg": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

后过滤器
在有些场景中,需要根据条件进行数据查询,但是聚合结果不受影响。
例如:求全部数据的平均值,但只需输出下架的课程
这时 需要使用 post_filter 关键字 进行 聚合后的 后置过滤,但不影响aggs聚合
GET /course/_search
{
  "size": 10,
  "post_filter": {
    "term": {
      "publish": "false"
    }
  }, 
  "aggs": {
    "ans_avg": {
      "avg": {
        "field": "price"
      }
    }
  }
}

 注意:
聚合排序
按照文档计数排序
可以使用_count 来进行文档计数排序
GET /course/_search
{
  "size": 0,
  "aggs": {
    "agg_terms_publish1": {
      "terms": {
        "field": "publish",
        "order": {
          "_count": "asc"
        }
      }
    }
  }
}
按照聚合指标排序
可以使用具体的聚合指标名称 来进行排序
GET /course/_search
{
  "size": 0,
  "aggs": {
    "agg_terms_publish1": {
      "terms": {
        "field": "publish",
        "order": {
          "avg_aggs": "desc"
        }
      },
      "aggs": {
        "avg_aggs": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
按照分组Key排序
在聚合排序时,业务需求可能有按照每个分组的组名称排序的场景。此时可以使用 key来引用分组名称。
 按照分组Key的自然顺序升序排列
GET /course/_search
{
  "size": 0,
  "aggs": {
    "agg_terms_publish1": {
      "terms": {
        "field": "publish",
        "order": {
          "_key": "asc"
        }
      }
    }
  }
}
聚合分页
ES支持同时返回查询结果和聚合结果,前面介绍聚合查询时,查询结果和聚合结果各自封装在不同的子句中。
但有时我们希望聚合的结果按照每组选出前N个文档的方式进行呈现,最常见的一个场景就是电商搜索,如搜索苹果手机6S,搜索结果应该展示苹果手机6S型号中的一款手机即可,而不论该型号手机的颜色有多少种。
另外,当聚合结果和查询结果封装在一起时,还需要考虑对结果分页的问题,此时前面介绍的聚合查询就不能解决这些问题了。
ES提供的Top hits聚合和Collapse聚合可以满足上述需求,但是这两种查询的分页方案是不同的。
Top hits 聚合
Top hits聚合指的是聚合时在每个分组内部,按照某个规则选出前N个文档进行展示。
 例如: 搜索 “开发”时,按照上下架分组,每组按照价格升序,展示最便宜的数据
GET /course/_search
{
  "query": {
    "match": {
      "title": "开发"
    }
  },
  "size": 0,
  "aggs": {
    "group_publish": {
      "terms": {
        "field": "publish"
      },
      "aggs": {
        "top_aggs": {
          "top_hits": {
            "size": 1,
            "sort": {
              "price": {
                "order": "asc"
              }
            }
          }
        }
      }
    }
  }
}

Collapse 聚合
当在索引中有大量数据命中时,Top hits聚合存在效率问题,并且需要用户自行排序。
针对上述问题,ES推出了Collapse聚合,即用户可以在collapse子句中指定分组字段,匹配query的结果按照该字段进行分组,并在每个分组中按照得分高低展示组内的文档。
当用户在query子句外指定from和size时,将作用在Collapse聚合之后,即此时的分页是作用在分组之后的。
GET /course/_search
{
  "query": {
    "match": {
      "title": "开发"
    }
  },
  "from": 0,
  "size": 2,
  "collapse": {
    "field": "price"
  }
}










