Elasticsearch 聚合搜索

Elasticsearch 聚合搜索

当用户使用搜索引擎完成搜索后,在展示结果中需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。

环境准备

  • Elasticsearch 服务(单机或集群)
  • Kibana 服务

如果对ES不了解或没有上述环境,可以看下我之前的博客。

Elasticsearch入门基础和集群部署

Elasticsearch查看集群信息,设置ES密码,Kibana部署

数据准备

json 复制代码
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}}

基础聚合

json 复制代码
# 聚合指令
"aggs":{
    // 指定聚合内容
    "value_count_price":{
      // 指定方法
      "value_count": {
        //指定聚合字段
        "field": "price"
      }
    }
  }

基础聚合方法

  • 平均值 avg
  • 最大 max
  • 最小 min
  • 计数 value_count
  • 求和 sum
  • 统计聚合 stats

注意: 上述基础聚合,非空值不会参与计算

例如:十个文档数据,其中有一个文档中字段为空,对其求平均值,结果应该是: 字段不为空的9个文档的平均值。

对于空值:可以使用missing 字段,指定 将空值替换为某个值 来参与计算

桶聚合(分组聚合)

在ES中,MYSQL 中的 Group by 分组 被称为 桶聚合

单维度桶聚合

单维度桶聚合 就是 按照一个维度对文档 进行分组聚合

在桶聚合时匹配方式

  • terms

terms聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keyword、bool、keyword数组等适合精确匹配的数据类型,因此不能对text字段直接使用terms聚合,如果对text字段有terms聚合的需求,则需要在创建索引时为该字段增加多字段功能。

  • ranges

ranges聚合也是经常使用的一种聚合。它匹配的是数值字段,表示按照数值范围进行分组。用户可以在ranges中添加分组,每个分组用from和to表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。

  • filter

filter聚合使用和搜索时使用方法一样,用来过滤出一批数据,一般用于 不影响查询条件的前过滤器

json 复制代码
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 字段。

如果桶字段类型不是keyword类型,ES在聚合时会将桶字段转换为Lucene存储的实际值进行识别。true在Lucene中存储为1,false在Lucene中存储为0。而key_as_string 则用来表示原始值的字符串形式。


在默认情况下,进行桶聚合时如果不指定指标,则ES默认聚合的是文档计数,该值以doc_count为key存储在每一个bucket子句中。

返回的doc_count 是近似值,并不是一个准确数,

因此在聚合外围,ES给出了两个参考值doc_count_error_upper_boundsum_other_doc_count

doc_count error_upper表示被遗漏的文档数量可能存在的最大值。
sum other doc count表示除了返回给用户的文档外剩下的文档总数。

多维度桶聚合

通常,在一些复杂业务中,单维度的桶聚合无法满足需求。

往往需要引入多维度的嵌套桶聚合。

例如:分别获取发布和未发布状态 价格 在 (-∞,80),[80,100),[100,+∞) 的 价格平均值

分析上述 需求,按照关系数据库的思路,其实就是 按照 发布状态 和 这三个价格区间进行分组 求每个组的平均价格

对应ES 中的桶聚合,可以转化为 求两个维度的桶聚合,

  1. 按照 发布状态 进行分组聚合
  2. 在聚合的结果中,按照价格区间进行分组聚合
  3. 在最新的聚合桶中 计算平均价格

转化为DSL如下:

json 复制代码
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查询。

对应的 就是 SQL 语言中的 Where语句

例如:查询发布状态的 课程 在 价格区间 (-∞,80),[80,100),[100,+∞) 中的平均价格

json 复制代码
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数据

SQL 中没有 前过滤器 关键字,但可以通过 case when end 实现相似的效果

对应DSL语句为:

json 复制代码
GET /course/_search
{
  "size": 0,
  "aggs": {
    "my_aggs": {
      "filter": {
        "term": {
          "publish": "true"
        }
      },
      "aggs": {
        "ans_avg": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

后过滤器

在有些场景中,需要根据条件进行数据查询,但是聚合结果不受影响。

例如:求全部数据的平均值,但只需输出下架的课程

这时 需要使用 post_filter 关键字 进行 聚合后的 后置过滤,但不影响aggs聚合

可以类比 SQL语句中的 having 关键字,分组后进行数据筛选

json 复制代码
GET /course/_search
{
  "size": 10,
  "post_filter": {
    "term": {
      "publish": "false"
    }
  }, 
  "aggs": {
    "ans_avg": {
      "avg": {
        "field": "price"
      }
    }
  }
}

注意:

ans_avg 还是计算的 query 查询出来的数据的平均值

而 post_filter 作用于 聚合计算后,再过滤数据,所以 只输出了三个 publish 为false 的数据

聚合排序

按照文档计数排序

可以使用_count 来进行文档计数排序

json 复制代码
GET /course/_search
{
  "size": 0,
  "aggs": {
    "agg_terms_publish1": {
      "terms": {
        "field": "publish",
        "order": {
          "_count": "asc"
        }
      }
    }
  }
}

按照聚合指标排序

可以使用具体的聚合指标名称 来进行排序

json 复制代码
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的自然顺序升序排列

json 复制代码
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个文档进行展示。

例如: 搜索 "开发"时,按照上下架分组,每组按照价格升序,展示最便宜的数据

json 复制代码
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聚合之后,即此时的分页是作用在分组之后的。

json 复制代码
GET /course/_search
{
  "query": {
    "match": {
      "title": "开发"
    }
  },
  "from": 0,
  "size": 2,
  "collapse": {
    "field": "price"
  }
}
相关推荐
天冬忘忧4 分钟前
Kafka 生产者全面解析:从基础原理到高级实践
大数据·分布式·kafka
hummhumm12 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
青云交27 分钟前
大数据新视界 -- Hive 数据仓库:构建高效数据存储的基石(下)(2/ 30)
大数据·数据仓库·hive·数据安全·数据分区·数据桶·大数据存储
zmd-zk36 分钟前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
电子手信39 分钟前
知识中台在多语言客户中的应用
大数据·人工智能·自然语言处理·数据挖掘·知识图谱
隔着天花板看星星1 小时前
Kafka-Consumer理论知识
大数据·分布式·中间件·kafka
holywangle1 小时前
解决Flink读取kafka主题数据无报错无数据打印的重大发现(问题已解决)
大数据·flink·kafka
隔着天花板看星星1 小时前
Kafka-副本分配策略
大数据·分布式·中间件·kafka
Lorin 洛林1 小时前
Hadoop 系列 MapReduce:Map、Shuffle、Reduce
大数据·hadoop·mapreduce
DolphinScheduler社区2 小时前
大数据调度组件之Apache DolphinScheduler
大数据