之前项目增加了一个新需求,用户商品搜索,在搜索引擎完成搜索后,在展示结果中需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总。由于之前看过es的书,基于之前学习的内容,在项目实战中做了总结。
前文如上:
9.工具使用:Elasticsearch从入门到放弃(1)-Elasticsearch概念篇
10.工具使用:Elasticsearch从入门到放弃(2)-相关性算法
11.工具使用:Elasticsearch从入门到放弃(3)-权重及打分 (qq.com)
文档摘自《Elasticsearch搜索引擎构建入门与实战》
1.背景
当用户使用搜索引擎完成搜索后,在展示结果中需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。聚合的需求在很多应用程序中都有所体现,例如在京东App中搜索“咸鸭蛋”,然后单击搜索界面中的“筛选”按钮,在弹出的界面中可以对当前的搜索结果进行进一步的过滤。例如,可以从价格区间、品牌、分类、枚数等维度分别进行筛选,(具体筛选条件是从第一步搜索条件中得到的)如图所示。
为方便介绍,下面重新定义酒店的索引。
2. 聚合指标
2.1 常见的统计指标
在搜索聚合时,用户可能会关注字段的相关统计信息,例如平均值、最大值、最小值及加和值等。例如,用户在使用一个二手房交易搜索引擎进行搜索时,可能会关注当前城市各个区域的房产平均价格。再例如,用户在搜索酒店时,也可能会关注附近各个区域酒店的最低价格。如图7.2所示,左图为在链家App的地图模式,可以搜索到当前位置附近的二手房平均交易价格;右图为携程App的地图模式,可以搜索到当前位置附近最低价格的酒店。
ES聚合请求的地址也是索引的搜索地址,可以使用aggs子句封装聚合请求。当使用avg子句进行平均值的聚合时,可以在avg子句中指定聚合的字段。在默认情况下,查询将匹配所有文档,如果不需要返回匹配的文档信息,最好将返回的文档个数设置为0。这样既可以让结果看起来更整洁,又可以提高查询速度。下面的DSL将查询所有酒店的平均价格并且不返回匹配的文档信息。
在上面的搜索结果中,索引中的5个文档全部命中,由于DSL设置size为0,所以命中文档的信息没有显示。在搜索结果的aggregations子句中存储着聚合结果,其中my_agg是聚合的名称,其对应的value值就是具体聚合结果,即酒店的平均价格。如果聚合的指标字段不是ES的基本类型,例如object类型,则可以使用点运算符进行引用。下面的DSL演示了
以下代码演示了在Java中使用stats聚合的逻辑。
2.2 空值处理
在索引中的一部分文档很可能其某些字段是缺失的,在介绍空值处理之前,首先介绍ES聚合查询提供的value_count聚合,该聚合用于统计字段非空值的个数。以下示例使用value_count聚合统计了price字段中非空值的个数。
需要指出的是,如果判断的字段是数组类型,则value_count统计的是符合条件的所有文档中该字段数组中非空元素个数的总和,而不是数组的个数总和。下面的DSL用于统计tags字段数组中非空元素个数的总和。
如果需要以空值字段的数据作为聚合指标对其进行聚合,可以在指标统计中通过missing参数指定填充值对空值进行填充。以下示例演示了对price字段进行聚合,并设定了当字段值为空值时使用100进行替代的查询请求。
3. 桶聚合
前面介绍的聚合指标是指符合条件的文档字段的聚合,有时还需要根据某些维度进行聚合。例如在搜索酒店时,按照城市、是否满房、标签和创建时间等维度统计酒店的平均价格。这些字段统称为“桶”,在同一维度内有一个或者多个桶。例如城市桶,有“北京”“天津”等,是否满房桶,有“满房”“非满房”。
3.1 单维度桶聚合
最简单的桶聚合是单维度桶聚合,指的是按照一个维度对文档进行分组聚合。在桶聚合时,聚合的桶也需要匹配,匹配的方式有terms、filter和ranges等。本节只介绍比较有代表性的terms查询和ranges查询,对其他匹配方式感兴趣读者可以阅读相关文档进行学习,这里不再赘述。
terms聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keyword、bool、keyword数组等适合精确匹配的数据类型,因此不能对text字段直接使用terms聚合,如果对text字段有terms聚合的需求,则需要在创建索引时为该字段增加多字段功能。
在默认情况下,进行桶聚合时如果不指定指标,则ES默认聚合的是文档计数,该值以doc_count为key存储在每一个bucket子句中。在聚合结果的buckets的两个bucket中,key字段的值分别为“北京”“天津”,表示两个bucket的唯一标识;doccount字段的值分别为3和2,表示两个bucket的文档计数。返回的doccount是近似值,并不是一个准确数,因此在聚合外围,ES给出了两个参考值doc_count_error_upper_bound和sum_other__doc_count,doc_count_error_upper表示被遗漏的文档数量可能存在的最大值,sum_other_doc_count表示除了返回给用户的文档外剩下的文档总数。
从上述结果中可以看到,在满房和非满房的bucket结果中多出了一个字段,名称为key_as_string,其值分别是true和false。另外,这两个bucket的key值分别为1和0。这是因为,如果桶字段类型不是keyword类型,ES在聚合时会将桶字段转换为Lucene存储的实际值进行识别。true在Lucene中存储为1,false在Lucene中存储为0,这就是为什么满房和非满房的key字段分别为1和0的原因。 这种情况给用户的使用带来了一些困惑,因为和原始值的差别比较大。针对这个问题,我们可以使用ES提供的key_as_string桶识别字段,它是原始值的字符串形式,和原始值的差别比较小。
以下代码演示了在Java中使用terms聚合进行单维度桶聚合的逻辑:
除了terms聚合,ranges聚合也是经常使用的一种聚合。它匹配的是数值字段,表示按照数值范围进行分组。用户可以在ranges中添加分组,每个分组用from和to表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。以下DSL演示了使用ranges进行聚合的方法:
有时还需要对单维度桶指定聚合指标,聚合指标单独使用子aggs进行封装,该aggs子句的使用方式和上一节介绍的聚合指标相同。 以下请求表示按照城市维度进行聚合,统计各个城市的平均酒店价格:
在上面的结果中,聚合桶的维度是城市,当前索引中城市为“北京”的文档个数为3,城市为“天津”的文档个数为2。将这两组文档的聚合结果在buckets子句中进行了封装,可以根据key字段进行聚合桶的识别,每个聚合的组中既有文档个数又有价格的加和值。
以下代码演示了在Java中使用桶聚合和指标聚合的逻辑:
3.2 多维度桶嵌套聚合
在某些业务需求中,不仅需要一个维度的桶聚合,而且还可能有多维度桶嵌套聚合的需求。例如在搜索酒店时,可能需要统计各个城市的满房和非满房状态下的酒店平均价格。ES支持嵌套桶聚合,进行嵌套时,可以使用aggs子句进行子桶的继续嵌套,指标放在最里面的子桶内。以下DSL演示了多维度桶的使用方法:
以下代码演示了在Java中使用多维度桶进行聚合的逻辑:
3.3 地理距离聚合
按照地理距离进行聚合是一个非常实用的功能,例如在搜索酒店时,可能需要对附近的酒店个数先预览一下 :查看距离当前位置2km范围内、2~3km范围内、5km范围内的酒店个数。 再如,在与电动汽车相关的充电需求中,车主需要搜寻附近充电桩的数量,以便能快速地完成充电。 如图7.3所示为国家电网“e充电”手机App的地图搜索模式,用户附近的汽车充电桩数量将随着地图的缩放而逐渐变化。
用户可以使用geo_distance聚合进行地理距离聚合,通过field参数来设置距离计算的字段,可以在origin子句中设定距离的原点,通过unit参数来设置距离的单位,可以选择mi和km,分别表示米和千米。ranges子句用来对距离进行阶段性的分组,该子句的使用方式和前面介绍的range聚合类似。以下DSL演示了使用geo_distance聚合进行地理距离聚合的方法:
也可以指定聚合指标进行地理距离聚合,下面的DSL将按照bucket分桶聚合酒店的最低价格: