词项(terms)聚合

一种基于多桶值源的聚合,其中桶是用一个个的唯一值动态构建的。

示例:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : { "field" : "genre" } 
        }
    }
}

terms聚合应该是keyword类型的字段或任何其他适合于桶聚合的数据类型。 为了与text一起使用,需要启用fielddata

响应:

{
    ...
    "aggregations" : {
        "genres" : {
            "doc_count_error_upper_bound": 0, 
            "sum_other_doc_count": 0, 
            "buckets" : [ 
                {
                    "key" : "electronic",
                    "doc_count" : 6
                },
                {
                    "key" : "rock",
                    "doc_count" : 3
                },
                {
                    "key" : "jazz",
                    "doc_count" : 2
                }
            ]
        }
    }
}

每个词项的文档计数的误差上限,见下文

当有很多唯一的词时,Elasticsearch只返回前面几个词;此数字是不属于响应的所有桶的文档计数总和

前几个(top)桶的列表,top是由order定义的

默认情况下,terms聚合将返回按doc_count排序的前10个词的桶。 可以通过设置参数size来改变这种默认行为。

size

参数size可以用来定义应该从整个词项列表中返回多少个词项桶。 默认情况下,协调搜索过程的节点(被称为协调节点)将请求每个分片提供其自己的前size个词项桶,并且一旦所有分片做出响应,它将把结果缩小到最终列表,然后将该列表返回给客户端。 这意味着,如果唯一词的数量大于size,则返回的列表会稍有偏差且不准确(可能是词项计数稍有偏差,甚至可能是本应该在前 size 个桶中的项没有返回)。

如果你想要检索嵌套terms聚合中的所有词项或所有词项组合,应该使用composite聚合,它允许对所有可能的词项进行分页,而不是将size设置为大于terms聚合中字段的基数。 terms聚合旨在返回top(前几个)词项,不允许分页。

文档计数是近似值

terms 聚合中的文档计数(以及任何子聚合的结果)并不总是准确的。 每一个分片都提供了自己的 terms 排序列表视图。 这些视图被组合起来以给出最终视图。

分片大小(shard_size)

请求的size越高,结果就越准确,但是计算最终结果的成本也就越高(这是因为在分片级别上管理的优先级队列越大,以及节点和客户端之间传输的数据越多)。

参数shard_size可用于最小化因请求的size较大而带来的额外工作。 一旦定义了,它将决定协调节点将从每个分片请求多少个词项。 一旦所有的分片都作出响应,协调节点就会将它们缩小到基于参数size的最终结果——通过这种方式,可以提高返回项的准确性,并避免将一个大的桶列表流回客户端的开销。

shard_size 不能小于size(因为没有太大意义)。当它存在时,Elasticsearch将覆盖它并将其重置为与size相等。

shard_size 的默认值为 (size * 1.5 + 10)

当计算文档数量有误差时

在 terms 聚合中可以显示两个误差值。 第一个给出了聚合的整体值,该值表示没有进入最终词项列表的词项的最大潜在文档数。 它被计算为从每个分片返回的最后一个词项的文档计数的总和。

单个桶的文档数量计算有误差

通过将show_term_doc_count_error参数设置为true,可以启用第二个误差值:

GET /_search
{
    "aggs" : {
        "products" : {
            "terms" : {
                "field" : "product",
                "size" : 5,
                "show_term_doc_count_error": true
            }
        }
    }
}

这显示了聚合返回的每个词项的误差值,它代表了文档计数中最差情况的误差,在决定shard_size参数的值时非常有用。 这是通过对没有返回词项的所有分片返回的最后一个词项的文档计数求和来计算的。

只有当词项按文档计数降序排列时,才能以这种方式计算这些误差。 当聚合按词项值本身排序(升序或降序)时,在文档计数中没有误差,因为如果一个分片没有返回出现在另一个分片的结果中的特定词项,则它的索引中一定没有该词项。 当聚合按子聚合或按文档计数升序排序时,无法确定文档计数中的误差,并给定一个值 -1 来表示这一点。

排序(order)

可以通过设置参数order来指定桶的顺序。 默认情况下,桶按照doc_count降序排列。 有可能改变这种行为,如下所示:

不鼓励使用_count升序或子查询进行排序,因为这会增加文档计数的误差。 当查询单个分片时,或者当被聚集的字段在索引时被用作路由键时,这是没有问题的:在这些情况下,结果将是准确的,因为分片具有不相交的值。 然而,除此之外,误差是没有上限的。 仍然有用的一种特殊情况是按minmax聚合排序:计数不准确,但至少会正确选择最前面的几个桶。

将桶按文档的_count升序排序:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "field" : "genre",
                "order" : { "_count" : "asc" }
            }
        }
    }
}

将桶按按词项的字母顺序的升序排序:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "field" : "genre",
                "order" : { "_key" : "asc" }
            }
        }
    }
}

在6.0.0版本中废弃

使用_key代替_term,将桶按词项排序

按单值度量子聚合对桶进行排序(由聚合名称标识):

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "field" : "genre",
                "order" : { "max_play_count" : "desc" }
            },
            "aggs" : {
                "max_play_count" : { "max" : { "field" : "play_count" } }
            }
        }
    }
}

按多值度量子聚集对桶进行排序(由聚合名称标识):

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "field" : "genre",
                "order" : { "playback_stats.max" : "desc" }
            },
            "aggs" : {
                "playback_stats" : { "stats" : { "field" : "play_count" } }
            }
        }
    }
}

管道(pipeline)聚合不能用于排序

管道(pipeline)聚合在所有其他聚合完成后的压缩阶段运行。 因此,它们不能用于排序。

还可以根据层次结构中“更深”的聚合对桶进行排序。 只要聚合路径是单桶类型,就支持这一点,其中路径中的最后一个聚合可以是单桶聚合,也可以是度量聚合。 如果是单桶类型,则顺序将由桶中的文档数(即doc_count)来定义,如果是度量类型,则应用与上述相同的规则(如果是多值度量聚合,则路径必须指示要排序的度量名称,如果是单值度量聚合,则排序将应用于该值)。

路径必须按以下形式定义:

AGG_SEPARATOR       =  '>' ;
METRIC_SEPARATOR    =  '.' ;
AGG_NAME            =  <the name of the aggregation> ;
METRIC              =  <the name of the metric (in case of multi-value metrics aggregation)> ;
PATH                =  <AGG_NAME> [ <AGG_SEPARATOR>, <AGG_NAME> ]* [ <METRIC_SEPARATOR>, <METRIC> ] ;
GET /_search
{
    "aggs" : {
        "countries" : {
            "terms" : {
                "field" : "artist.country",
                "order" : { "rock>playback_stats.avg" : "desc" }
            },
            "aggs" : {
                "rock" : {
                    "filter" : { "term" : { "genre" :  "rock" }},
                    "aggs" : {
                        "playback_stats" : { "stats" : { "field" : "play_count" }}
                    }
                }
            }
        }
    }
}

上面的查询将根据摇滚歌曲的平均播放次数对 艺术家的国家(artist.country) 进行分类。

通过提供一组排序标准,可以使用多个标准来对桶进行排序,如下所示:

GET /_search
{
    "aggs" : {
        "countries" : {
            "terms" : {
                "field" : "artist.country",
                "order" : [ { "rock>playback_stats.avg" : "desc" }, { "_count" : "desc" } ]
            },
            "aggs" : {
                "rock" : {
                    "filter" : { "term" : { "genre" : "rock" }},
                    "aggs" : {
                        "playback_stats" : { "stats" : { "field" : "play_count" }}
                    }
                }
            }
        }
    }
}

上面的查询将根据摇滚歌曲的 平均播放次数(playback_stats.avg),然后按照doc_count降序排列 艺术家的国家(artist.country) 类别。

如果两个桶在所有排序标准中共享相同的值,则将再按桶的词项值升序排序,以防止桶的不确定性排序。

最小文档数量

使用min_doc_count选项可以只返回匹配超过配置的命中数的词项:

GET /_search
{
    "aggs" : {
        "tags" : {
            "terms" : {
                "field" : "tags",
                "min_doc_count": 10
            }
        }
    }
}

上述聚集将仅返回有10次或更多次命中出现的 tags。默认值为1

词项在分片级别上被收集和排序,并在第二步中与从其他分片收集的词项合并。 然而,分片没有关于可用的全局词项计数的信息。 是否将一个词项添加到候选列表的决定只取决于使用局部分片频率在分片上计算的排序。 min_doc_count标准仅在合并所有分片的本地词项统计之后应用。 在某种程度上,在没有非常确定该词项是否将实际达到所需的min_doc_count的情况下,就做出了将该词项添加为候选项的决定。 如果低频的词项填充了候选列表,这可能导致许多(全局)高频的词项在最终结果中丢失。 为了避免这种情况,可以增加shard_size参数,以允许分片上有更多的候选项。 但是,这会增加内存消耗和网络流量。

参数shard_min_doc_count

参数shard_min_doc_count规定了一个分片相对于min_doc_count是否应该被实际添加到候选列表中的确定性。 只有当词项在集合中的本地分片频率高于shard_min_doc_count时,才会考虑这些词项。 如果你的字典包含许多低频的单词,而你对这些单词不感兴趣(例如拼写错误),那么你可以设置shard_min_doc_count参数来过滤掉分级别上的候选词,即使在合并本地频率之后,这些候选词也肯定不会达到所需的min_doc_count。 默认情况下,shard_min_doc_count设置为0,除非你显式设置它,否则它不起作用。

设置min_doc_count=0也还是会返回不匹配任何匹配项的桶。 但是,某些返回的文档计数为零的词项可能只属于已删除的文档或其他类型的文档,因此不能保证match_all查询会为这些词项找到正的文档计数。

当不对doc_count进行降序排序时,min_doc_count的高值可能会返回小于size的桶数,因为没有从分片中收集足够的数据。 将shard_min_doc_count设置得太高会导致词项在分片级别被过滤掉。 该值应设置为远低于min_doc_count/#shards

脚本

使用脚本生成一个词:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "script" : {
                    "source": "doc['genre'].value",
                    "lang": "painless"
                }
            }
        }
    }
}

这将把script参数解释为使用默认脚本语言且没有脚本参数的 内联(inline)脚本。要使用存储的脚本,请使用以下语法:

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "script" : {
                    "id": "my_script",
                    "params": {
                        "field": "genre"
                    }
                }
            }
        }
    }
}

值脚本(value script)

GET /_search
{
    "aggs" : {
        "genres" : {
            "terms" : {
                "field" : "genre",
                "script" : {
                    "source" : "'Genre: ' +_value",
                    "lang" : "painless"
                }
            }
        }
    }
}

对值进行过滤

可以过滤将用于创建桶的值。 这可以使用基于正则表达式字符串或精确值数组的includeexclude参数来完成。 此功能反映了 terms 聚合文档中描述的功能。 此外,include子句可以使用partition表达式进行过滤。

使用正则表达式对值进行过滤

GET /_search
{
    "aggs" : {
        "tags" : {
            "terms" : {
                "field" : "tags",
                "include" : ".*sport.*",
                "exclude" : "water_.*"
            }
        }
    }
}

在上面的例子中,将为所有包含单词sport的 tags 创建桶,除了那些以water_开头的标签(因此值为water_sports的 tag 不会被聚合)。 include正则表达式将决定“允许”聚合哪些值,而exclude将决定不应该聚合的值。 当二者都被定义时,exclude优先,这意味着首先计算include,然后才计算exclude

语法同 正则查询

使用精确值对值进行过滤

对于基于精确值的匹配,includeexclude参数可以简单地接受一个字符串数组,这些字符串表示在索引中找到的词项:

GET /_search
{
    "aggs" : {
        "JapaneseCars" : {
             "terms" : {
                 "field" : "make",
                 "include" : ["mazda", "honda"]
             }
         },
        "ActiveCarManufacturers" : {
             "terms" : {
                 "field" : "make",
                 "exclude" : ["rover", "jensen"]
             }
         }
    }
}

使用partition对值进行过滤

有时在一个请求/响应对中需要处理非常多独特的词,因此将分析分成多个请求会很有用。 这可以通过在查询时将字段值分组到多个 partition(分区) 中,并在每个请求中只处理一个 partition(分区) 来实现。 看一下下面这个请求,它寻找最近没有任何访问记录的帐户:

GET /_search
{
   "size": 0,
   "aggs": {
      "expired_sessions": {
         "terms": {
            "field": "account_id",
            "include": {
               "partition": 0,
               "num_partitions": 20
            },
            "size": 10000,
            "order": {
               "last_access": "asc"
            }
         },
         "aggs": {
            "last_access": {
               "max": {
                  "field": "access_date"
               }
            }
         }
      }
   }
}

此请求是为了查找客户帐户子集记录的最后访问日期,因为我们可能希望终止一些很久没有出现的客户帐户。 num_partitions设置要求将唯一的 account_ids 平均组织成20个分区(0到19)。 并且该请求中的partition设置对仅考虑落入分区0的account_ids进行过滤。 后续请求应该请求分区1、分区2等,以完成堆过期帐户的分析。

注意,返回结果数量的size设置需要和num_partitions一起调整。 对于这个特定的帐户到期的例子,平衡sizenum_partitions值的过程如下:

  1. 使用cardinality聚合来估计唯一 account_id 值的总数
  2. num_partitions选择一个值,将数字从 1) 分成更易于管理的块
  3. 为我们希望从每个分区得到的响应数量选择一个size
  4. 运行一个请求做测试

如果我们试图在一个请求中做太多事情而遇到断路器错误,则必须增加num_partitions。 如果请求成功,但按日期排序的测试响应中的最后一个帐户ID仍然是我们可能想要过期的帐户,那么我们可能会错过感兴趣的帐户,而且我们可能将这个数字设置得太低了。 以下必须二选一:

  • 增加参数size的值以让每个分区返回更多结果(可能会占用大量内存),或者
  • 增加num_partitions以减少每个请求的帐户数量(可能会增加总处理时间,因为我们需要发出更多的请求)

最终,这是在管理处理单个请求所需的Elasticsearch资源和客户端应用程序为完成任务必须发出的请求量之间的一个平衡。

多个字段词项聚合(Multi-field terms aggregation)

terms聚合不支持从同一文档中的多个字段收集词项。 原因是terms聚合本身并不收集字符串词项值,而是使用全局序号(global ordinals)来生成字段中所有唯一值的列表。 全局序号(global ordinals)可以带来重要的性能提升,这在多个字段中是不可能实现的。

有两种方法可用于跨多个字段执行terms聚合:

脚本
使用脚本从多个字段中检索词项。 这将禁用全局序号优化,并且比从单个字段中收集词项要慢,但它为你提供了在搜索时实现此选项的灵活性。
copy_to字段
如果你事先知道要从两个或更多字段中收集词项,那么可以在映射中使用copy_to在编制索引时创建一个新的专用字段,其中包含两个字段的值。 可以在这个单一字段上进行聚合,而这将可以利用全局序号优化带来的好处。

收集模式

推迟子聚合的计算

对于具有许多唯一词项和少量必需结果的字段,将子聚合的计算延迟到顶层父级聚合被删除后进行可能会更有效。 通常,聚合树的所有分支都在一次深度优先的遍历中展开,然后才进行任何修剪。 在某些情况下,这可能是非常浪费的,并且会达到内存限制。 一个此类问题场景的例子是在电影数据库中查询10个最受欢迎的演员和他们的5个最常见的联合主演:

GET /_search
{
    "aggs" : {
        "actors" : {
             "terms" : {
                 "field" : "actors",
                 "size" : 10
             },
            "aggs" : {
                "costars" : {
                     "terms" : {
                         "field" : "actors",
                         "size" : 5
                     }
                 }
            }
         }
    }
}

尽管参与者的数量可能相对较少,我们只需要50个结果桶,但是在计算过程中会出现桶的组合爆炸——一个参与者可以产生 n² 个桶(其中n是参与者的数量)。 明智的选择是首先确定10个最受欢迎的演员,然后才检查这10个演员的最佳联合主演。 这种替代策略就是我们所说的广度优先(breadth_first)收集模式,与 深度优先(depth_first)模式相对。

广度优先(breadth_first)是基数大于请求大小的字段或基数未知的字段(例如数值字段或脚本)的默认模式。 可以覆盖默认启发式规则,并在请求中直接指定收集模式:

GET /_search
{
    "aggs" : {
        "actors" : {
             "terms" : {
                 "field" : "actors",
                 "size" : 10,
                 "collect_mode" : "breadth_first" 
             },
            "aggs" : {
                "costars" : {
                     "terms" : {
                         "field" : "actors",
                         "size" : 5
                     }
                 }
            }
         }
    }
}

可能的值是breadth_firstdepth_first

当使用广度优先(breadth_first)模式时,落入最主要的桶中的文档集被缓存以供后续重放,因此这样做存在与匹配文档的数量成线性关系的内存开销。 请注意,在使用breadth_first设置时,参数order仍可用于引用子聚合中的数据——父聚合知道需要在调用任何其他子聚合之前首先调用该子聚合。

诸如top_hits之类的嵌套聚合,需要访问使用breadth_first收集模式的聚合下的得分信息,需要在第二次传递时重放查询,但只针对属于前几个桶的文档。

执行提示(execution hint)

可以通过不同的机制来执行 terms 聚合:

  • 通过直接使用字段值来聚合每个桶的数据 (map)
  • 通过使用字段的全局序号并为每个全局序号分配一个桶 (global_ordinals)

Elasticsearch试图使用合理的默认值,所以这通常不需要配置。

global_ordinalskeyword类型字段的默认选项,它使用全局序号来动态分配桶,因此内存使用量与聚合范围内的文档值的数量成线性关系。

只有当很少文档与查询匹配时,才应该考虑map。默认情况下,map仅在脚本上运行聚合时使用,因为它们没有序号。

GET /_search
{
    "aggs" : {
        "tags" : {
             "terms" : {
                 "field" : "tags",
                 "execution_hint": "map" 
             }
         }
    }
}

可能的值是mapglobal_ordinals

请注意,如果这个执行提示不适用,并且对这些提示没有向后兼容性保证,Elasticsearch将忽略它。

缺失的值

参数missing定义应该如何处理缺少值的文档。默认情况下,它们将被忽略,但也可以将它们视为有一个值。

GET /_search
{
    "aggs" : {
        "tags" : {
             "terms" : {
                 "field" : "tags",
                 "missing": "N/A" 
             }
         }
    }
}

tags字段中没有值的文档将与具有N/A值的文档落入同一个桶中。

混合字段类型

在多个索引上执行聚合时,所有索引中聚合字段的类型可能不相同。 有些类型是相互兼容的(integerlongfloatdouble),但是当类型是十进制数和非十进制数的混合时,terms 聚合会将非十进制数转换为十进制数。 这可能会导致桶值的精度损失。