复合(composite)聚合

一个多桶(multi-bucket)聚合,从不同来源创建复合桶。

与其他多桶(multi-bucket)聚合不同,composite 聚合可用于高效地对多级聚合中的所有桶进行分页。 这种聚合提供了一种方法,类似于 scroll 对文档所做的那样,对特定聚合的所有桶进行流式处理。

复合桶是从为每个文档提取/创建的值的组合中构建的,并且每个组合被认为是一个复合桶。

比如下面的文档:

{
    "keyword": ["foo", "bar"],
    "number": [23, 65, 76]
}

... 当keyword 和 number用作聚合的值的来源时,创建以下复合桶:

{ "keyword": "foo", "number": 23 }
{ "keyword": "foo", "number": 65 }
{ "keyword": "foo", "number": 76 }
{ "keyword": "bar", "number": 23 }
{ "keyword": "bar", "number": 65 }
{ "keyword": "bar", "number": 76 }

值的来源(value source)

参数 sources 控制应该用于构建复合桶的源。 定义 sources 的顺序很重要,因为它也控制着 key 的返回顺序。

每个源的名称必须是唯一的。

值的来源有三种不同类型:

terms (词项)

terms 值来源相当于一个简单的 terms 集合。 这些值是从字段或脚本中提取的,就像 terms 聚合一样。

例如:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "product": { "terms" : { "field": "product" } } }
                ]
            }
        }
     }
}

与 terms 聚合一样,也可以使用脚本来创建复合桶的值:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    {
                        "product": {
                            "terms" : {
                                "script" : {
                                    "source": "doc['product'].value",
                                    "lang": "painless"
                                }
                            }
                        }
                    }
                ]
            }
        }
    }
}

histogram (直方图)

histogram 值来源可应用于数值,以在这些值上构建固定大小的间隔。 参数 interval 定义应该如何转换数值。 例如, interval 设置为 5 会将任何数值转换为最接近 5 的间隔,值 101 会转换为 100,因为 101 在间隔 100 和 105 之间。

例如:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "histo": { "histogram" : { "field": "price", "interval": 5 } } }
                ]
            }
        }
    }
}

这些值由 numeric(数值)字段或返回数值的脚本构建而成:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    {
                        "histo": {
                            "histogram" : {
                                "interval": 5,
                                "script" : {
                                    "source": "doc['price'].value",
                                    "lang": "painless"
                                }
                            }
                        }
                    }
                ]
            }
        }
    }
}

date histogram (日期直方图)

date_histogram值源类似于 histogram,只是它的时间间隔是由日期/时间表达式指定的:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "date": { "date_histogram" : { "field": "timestamp", "calendar_interval": "1d" } } }
                ]
            }
        }
    }
}

上面的示例创建了一个每天的时间间隔,并将所有 timestamp 值转换为最接近的时间间隔的开始。 间隔的可用表达式有:year、quarter, month、week、day、hour、minute、second。

时间值也可以通过时间单位(time units)解析支持的缩写来指定。 请注意,不支持带小数点的时间值,但是你可以通过转换到另一个时间单位来解决这个问题(例如,可以将1.5h指定为90m)。

 

format

在内部,一个日期被表示为一个64位的数字-一个时间戳,以毫秒为单位。 这些时间戳作为桶的 key 返回。 可以使用参数format指定的格式返回格式化的日期字符串:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    {
                        "date": {
                            "date_histogram" : {
                                "field": "timestamp",
                                "calendar_interval": "1d",
                                "format": "yyyy-MM-dd" 
                            }
                        }
                    }
                ]
            }
        }
    }
}

支持日期格式/模式表达式

时区

日期时间以 UTC 存储在 Elasticsearch 中。 默认情况下,所有的分桶和舍入也在 UTC 中完成。 time_zone 参数可用于指示分桶时应该使用不同的时区。

时区可以指定为 ISO 8601 UTC 时差(例如+01:00 或 -08:00),也可以指定为时区id(在TZ数据库中使用的标识符),比如America/Los_Angeles。

Offset

使用 offset 参数按指定的正(+)或负(-)偏移量持续时间来更改每个桶的起始值,例如1h表示一个小时,1d表示一天。 有关更多可能的持续时间选项,请参见时间单位(time units)。

例如,当使用day作为时间间隔时,每个桶的时间区间从午夜到午夜。 将参数 offset 设置为 +6h 会将每个桶的时间区间更改为从早上6点到早上6点:

#添加并索引两个文档
PUT my_index/_doc/1?refresh
{
  "date": "2015-10-01T05:30:00Z"
}

PUT my_index/_doc/2?refresh
{
  "date": "2015-10-01T06:30:00Z"
}

#搜索
GET my_index/_search?size=0
{
  "aggs": {
    "my_buckets": {
      "composite" : {
        "sources" : [
          {
            "date": {
              "date_histogram" : {
                "field": "date",
                "calendar_interval": "day",
                "offset": "+6h",
                "format": "iso8601"
              }
            }
          }
        ]
      }
    }
  }
}

上面的请求的单个桶不是从午夜开始,而是从早上6点开始:

{
  ...
  "aggregations": {
    "my_buckets": {
      "after_key": { "date": "2015-10-01T06:00:00.000Z" },
      "buckets": [
        {
          "key": { "date": "2015-09-30T06:00:00.000Z" },
          "doc_count": 1
        },
        {
          "key": { "date": "2015-10-01T06:00:00.000Z" },
          "doc_count": 1
        }
      ]
    }
  }
}

在进行 time_zone 调整后,计算每个桶的起始 offset 值。

混合不同的值源

参数 sources 接受一个值源数组。可以混合不同的值源来创建复合桶。例如:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } },
                    { "product": { "terms": {"field": "product" } } }
                ]
            }
        }
    }
}

这将从由两个值源创建的值创建复合桶,一个date_histogram和一个terms。 每个桶由两个值组成,聚合中定义的每个值源对应一个值。 允许任何类型的组合,并且在组合桶中保留数组中的顺序。

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "shop": { "terms": {"field": "shop" } } },
                    { "product": { "terms": { "field": "product" } } },
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } }
                ]
            }
        }
    }
}

排序 (order)

默认情况下,复合桶按其自然顺序排序。 值按其值的升序排序。 当请求多个值源时,将对每个值源进行排序,将组合桶的第一个值与另一个组合桶的第一个值进行比较,如果它们相等,则组合桶中的下一个值将用于再次比较。 这意味着复合桶 [foo, 100]被认为比 [foobar, 0] 小,因为 foo 被认为比 foobar 小。 通过直接在值源定义中将 order 设置为asc(升序,默认值)或desc(降序),可以定义每个值源的排序方向。例如:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },
                    { "product": { "terms": {"field": "product", "order": "asc" } } }
                ]
            }
        }
    }
}

当比较 date_histogram 源中的值时,将按降序对复合桶进行排序,当比较 terms 源中的值时,将按升序对复合桶进行排序。

缺失的桶

默认情况下,没有给定来源值的文档将被忽略。 通过将 missing_bucket 设置为 true (默认为 false),可以将它们包含在响应中:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "product_name": { "terms" : { "field": "product", "missing_bucket": true } } }
                ]
            }
        }
     }
}

在上面的示例中,对于字段 product 没有值的文档,源 product_name 将生成一个显式的 null 值。 源中指定的 order 决定了 null 应该排在第一位(升序,asc)还是最后一位(降序,desc)。

size

可以设置参数 size 来定义应该返回多少个复合桶。 每个组合桶都被视为一个桶,因此将大小设置为 10 将返回从值源创建的前 10 个组合桶。 响应的数组中包含每个组合桶的值,该数组包含从每个值源提取的值。

分页(pagination)

如果复合桶的数量太多(或未知)而无法在单个响应中返回,则可以将检索分成多个请求。 因为复合桶本质上是扁平的,所以请求的 size 正好是响应中返回的复合桶的数量(假设它们至少是要返回的 size 个复合桶)。 如果应该检索所有的组合桶,最好使用一个较小的 size 值(例如 100 或 1000 ),然后使用参数 after 检索下一个结果。例如:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "size": 2,
                "sources" : [
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d" } } },
                    { "product": { "terms": {"field": "product" } } }
                ]
            }
        }
    }
}

... 返回:

{
    ...
    "aggregations": {
        "my_buckets": {
            "after_key": {
                "date": 1494288000000,
                "product": "mad max"
            },
            "buckets": [
                {
                    "key": {
                        "date": 1494201600000,
                        "product": "rocky"
                    },
                    "doc_count": 1
                },
                {
                    "key": {
                        "date": 1494288000000,
                        "product": "mad max"
                    },
                    "doc_count": 2
                }
            ]
        }
    }
}

要获得下一组桶,请重新发送相同的聚合,将参数 after 设置为响应中返回的 after_key 的值。 例如,下面的请求使用在之前的响应中提供的 after_key 的值:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "size": 2,
                 "sources" : [
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },
                    { "product": { "terms": {"field": "product", "order": "asc" } } }
                ],
                "after": { "date": 1494288000000, "product": "mad max" } 
            }
        }
    }
}

应该将聚合限制为在提供的值的排序之后(after)的桶。

after_key 通常是响应中返回的最后一个桶的key,但这并不能保证。 总是使用返回的 after_key,而不是从桶中取出它。

提前终止 (early termination)

为了获得最佳性能,应该对索引设置索引排序(index sort),以便它匹配复合聚合中的部分或全部源顺序。 例如下面的索引排序:

PUT twitter
{
    "settings" : {
        "index" : {
            "sort.field" : ["username", "timestamp"],   
            "sort.order" : ["asc", "desc"]              
        }
    },
    "mappings": {
        "properties": {
            "username": {
                "type": "keyword",
                "doc_values": true
            },
            "timestamp": {
                "type": "date"
            }
        }
    }
}

该索引首先按 username 排序,然后按timestamp排序。

…​ username 字段按升序排列,timestamp 字段按降序排列。

  1. 可用于优化这些复合聚合:
GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "user_name": { "terms" : { "field": "user_name" } } }     
                ]
            }
        }
     }
}

user_name 是索引排序和顺序匹配(asc)的前缀。

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "user_name": { "terms" : { "field": "user_name" } } }, 
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } } 
                ]
            }
        }
     }
}

user_name 是索引排序和顺序匹配(asc)的前缀。

timestamp 也匹配前缀,并且顺序匹配(desc)。

为了对提前终止(early termination)进行优化,建议将请求中的 track_total_hits 设置为false。 匹配请求的总命中数可以在第一次请求时检索,在每一页上计算这个数值的成本是很高的:

GET /_search
{
    "size": 0,
    "track_total_hits": false,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                "sources" : [
                    { "user_name": { "terms" : { "field": "user_name" } } },
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } }
                ]
            }
        }
     }
}

请注意,源的顺序很重要,在下面的示例中,用 timestamp 调换 user_name 将会禁用排序优化,因为这种配置与索引排序的规范不匹配。 如果源的顺序对你的用例不重要,可以遵循这些简单的准则:

  • 将基数最高的字段放在第一位。(这个与MySQL的优化类似)
  • 确保字段的顺序与索引排序的顺序相匹配。
  • 将多值字段放在最后,因为它们不能用于提前终止。

索引排序(index sort) 会降低索引编排的速度,使用你的特定用例和数据集测试索引排序以确保它符合你的要求是非常重要的。 即使你没有注意到这一点,composite 聚合也会尝试在查询匹配所有文档(match_all查询)的情况下提前终止非排序索引。

子聚合

与任何 multi-bucket(多桶) 聚合一样,composite 聚合可以包含子聚合。 这些子聚合可用于计算其他桶或由此父聚合创建的每个复合桶的统计数据。 例如,下面的示例计算每个复合桶的 price 字段的平均值:

GET /_search
{
    "size": 0,
    "aggs" : {
        "my_buckets": {
            "composite" : {
                 "sources" : [
                    { "date": { "date_histogram": { "field": "timestamp", "calendar_interval": "1d", "order": "desc" } } },
                    { "product": { "terms": {"field": "product" } } }
                ]
            },
            "aggregations": {
                "the_avg": {
                    "avg": { "field": "price" }
                }
            }
        }
    }
}

... 返回:

{
    ...
    "aggregations": {
        "my_buckets": {
            "after_key": {
                "date": 1494201600000,
                "product": "rocky"
            },
            "buckets": [
                {
                    "key": {
                        "date": 1494460800000,
                        "product": "apocalypse now"
                    },
                    "doc_count": 1,
                    "the_avg": {
                        "value": 10.0
                    }
                },
                {
                    "key": {
                        "date": 1494374400000,
                        "product": "mad max"
                    },
                    "doc_count": 1,
                    "the_avg": {
                        "value": 27.0
                    }
                },
                {
                    "key": {
                        "date": 1494288000000,
                        "product" : "mad max"
                    },
                    "doc_count": 2,
                    "the_avg": {
                        "value": 22.5
                    }
                },
                {
                    "key": {
                        "date": 1494201600000,
                        "product": "rocky"
                    },
                    "doc_count": 1,
                    "the_avg": {
                        "value": 10.0
                    }
                }
            ]
        }
    }
}

管道聚合(pipeline aggregations)

目前复合聚合与管道聚合不兼容,在大多数情况下也没有意义。 例如,由于复合聚合的分页特性,单个逻辑分区(例如一天)可能会分布在多个页面上。 由于管道聚合纯粹是对最终桶列表的后处理,在复合页面上运行类似派生的东西可能会导致不准确的结果,因为它只考虑该页面上的“部分”结果。

将来可能会支持自包含到单个桶(如bucket_selector)的管道聚合。