vlambda博客
学习文章列表

读书笔记《elasticsearch-server-third-edition》用于数据分析的聚合

Chapter 7. Aggregations for Data Analysis

在上一章中,我们再次讨论了 Elasticsearch 的查询方面。我们了解了 Lucene TF/IDF 算法的工作原理以及如何使用 Elasticsearch 脚本功能。我们处理了多语言数据并通过提升影响了文档分数。我们使用同义词来匹配具有相同含义的单词,并使用 Elasticsearch Explain API 查看文档分数的计算方式。在本章结束时,您将学习以下主题:

  • 什么是聚合

  • Elasticsearch 聚合引擎的​​工作原理

  • 如何使用指标聚合

  • 如何使用桶聚合

  • 如何使用管道聚合

Aggregations


在 Elasticsearch 1.0 中引入的聚合是 Elasticsearch 中数据分析的核心。高度 灵活和高性能的聚合将 Elasticsearch 1.0 作为全功能分析引擎带到了一个新的位置。随着 Elasticsearch 1.x 的生命周期的延长,在 2.x 中它们更强大、内存要求更低、速度更快。有了这个框架,您可以使用 Elasticsearch 作为分析引擎进行数据提取和可视化。让我们看看这个功能是如何工作的,以及我们可以通过使用它来实现什么。

General query structure

要使用聚合,我们需要在查询中添加一个 附加部分。一般来说,我们的聚合查询如下所示:

{
   "query": { … },
   "aggs" : {
     "aggregation_name" : {
       "aggregation_type" : {
         ...
       }
     }
   }
}

aggs 属性中(如果需要,可以使用 aggregationsaggs只是一个缩写),您可以定义任意数量的聚合。每个聚合都由其名称和 Elasticsearch 提供的一种聚合类型定义。不过要记住的一件事是,键定义了聚合的名称(您将需要它来区分服务器响应中的特定 聚合)。让我们使用 library 索引并使用使用聚合创建第一个查询。发送此类查询的命令如下所示:

curl 'localhost:9200/library/_search?search_type=query_then_fetch&size=0&pretty' -d '{
   "aggs": {
      "years": {
         "stats": {
            "field": "year"
         }
      },
      "words": {
         "terms": {
            "field": "copies"
         }
      }
   }
}'

此查询定义了两个聚合。名为 years 的聚合显示 year 字段的统计信息。 words 聚合包含有关给定字段中使用的术语的信息。

Note

在我们的示例中,我们假设除了搜索之外我们还执行聚合。如果我们不需要找到的文档,更好的办法是使用 size 参数并将其设置为 0。这样省去了一些不必要的工作,效率更高。在这种情况下,端点应该是 /library/_search?size=0。您可以在第 3 章了解查询过程

现在让我们看看 Elasticsearch 对上述查询返回的响应:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "words" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : 0,
        "doc_count" : 2
      }, {
        "key" : 1,
        "doc_count" : 1
      }, {
        "key" : 6,
        "doc_count" : 1
      } ]
    },
    "years" : {
      "count" : 4,
      "min" : 1886.0,
      "max" : 1961.0,
      "avg" : 1928.0,
      "sum" : 7712.0
    }
  }
}

如您所见,聚合(yearswords)都返回了。我们在查询中定义的第一个聚合(years)返回给定字段的一般统计信息,该字段收集到与我们的查询匹配的所有文档中。第二个定义的聚合(words)有点不同。它创建了几个名为 buckets 的集合, 是根据返回的文档计算得出的,并且每个聚合值都在这些集合之一中。如您所见,有多种聚合类型可用,它们返回不同的结果。我们将在本节的后面部分看到不同之处。

聚合引擎的​​伟大之处在于它允许您拥有多个聚合,并且聚合 可以嵌套。这意味着您通常可以拥有无​​限级别的嵌套和任意数量的聚合。查询的扩展结构如下所示:

{
   "query": { … },
   "aggs" : {
     "first_aggregation_name" : {
       "aggregation_type" : {
         ...
       },
    "aggregations" : {
         "first_nested_aggregation" : {
         ...
         },
         .
         .
         .
         "nth_nested_aggregation" : {
         ...
         }
       }
     },
     .
     .
     .
     "nth_aggregation_name" : {
     ...
     }
   }
}

Inside the aggregations engine

聚合基于查询返回的结果工作。这非常方便,因为我们从查询和数据分析的角度获得了我们感兴趣的信息。那么当我们在发送给 Elasticsearch 的请求中包含查询的聚合部分时,Elasticsearch 会做什么呢?首先,在每个相关分片上执行聚合,并将结果返回到负责运行该查询的节点。该节点等待计算部分结果;得到所有结果后,它会合并结果,产生最终结果。

这种方法对于分布式系统及其工作和通信方式而言并不是什么新鲜事,但在结果的精度方面可能会引起问题。在大多数情况下,这不是问题,但您应该知道会发生什么。让我们想象下面的例子:

读书笔记《elasticsearch-server-third-edition》用于数据分析的聚合

上图显示了三个分片的简化视图,每个分片包含仅包含 Elasticsearch 和 Solr 术语的文档。现在假设我们对索引的单个术语感兴趣。使用 size=1 运行时的术语聚合将返回单个 term,这将是最常见的(当然仅限于我们运行的查询)。所以我们的聚合器节点会看到部分结果告诉我们 Elasticsearch 存在于 Shard 1 并且 Solr 术语出现在 Shard 2Shard 3,表示最上面的词是Solr,不正确。这是一个极端情况,但在某些用例(例如会计)中,精度是关键,您应该注意这种情况。

Note

与查询相比,Elasticsearch 的聚合在 CPU 周期和内存消耗方面都更加繁重。我们将在本章的缓存聚合部分更详细地讨论这个问题。

Aggregation types


Elasticsearch 2.x 允许我们使用 三种类型的聚合:指标、存储桶和管道。 metrics 聚合返回一个指标,就像我们用于 stats 聚合文字">统计数据 字段。桶聚合 返回桶、键和共享相同值、范围等的文档数,就像 copies 字段的 ">terms 聚合。最后,Elasticsearch 2.0 中引入的管道聚合聚合了其他聚合及其指标的 输出,这使我们能够进行更复杂的数据分析。了解了这一切,现在让我们看看我们可以在 Elasticsearch 2.x 中使用的所有聚合。

Metrics aggregations

我们将从指标聚合开始,它可以将文档中的值聚合成一个指标。指标聚合总是如此——您可以期望它们是基于数据的单个指标。现在让我们看一下 Elasticsearch 2.x 中可用的 指标聚合。

Minimum, maximum, average, and sum

我们要向您展示的第一组指标聚合是从给定文档计算基本值的聚合。这些聚合是:

  • min:计算返回文档中给定数字字段的最小

  • max:计算返回文档中给定数字字段的最大

  • avg:计算返回文档中给定数值字段的平均值

  • sum:计算返回文档中给定数值字段的总和

如您所见,前面提到的聚合是不言自明的。因此,让我们尝试计算数据的平均值。例如,假设我们要计算书籍的平均副本数。执行此操作的查询如下所示:

{
 "aggs" : {
  "avg_copies" : {
   "avg" : {
    "field" : "copies"
   }
  }
 }
}

运行上述查询后,Elasticsearch 返回的结果如下:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "avg_copies" : {
      "value" : 1.75
    }
  }
}

因此,我们有 平均每本书 1.75 份。计算起来很容易 - (6 + 0 + 1 + 0) / 4 相等<一个 id="id928" class="indexterm"> 到 1.75。似乎我们 做对了。

Missing values

前面提到的 聚合的好​​处在于,如果我们指定的字段没有任何值,我们可以控制 Elasticsearch 可以使用的值。例如,如果我们希望 Elasticsearch 在前面的示例中使用 0 作为副本字段的值,我们将在查询中添加 missing 属性并将其设置为 0。例如:

{
 "aggs" : {
  "avg_copies" : {
   "avg" : {
    "field" : "copies",
    "missing" : 0
   }
  }
 }
}
Using scripts

输入值也可以由脚本生成。例如,如果我们想从 year 字段中的所有值中找到最小值,但我们想减去 1000根据这些值,我们将发送如下聚合:

{
 "aggs": {
  "min_year": {
   "min": {
    "script": "doc['year'].value - 1000"
   }
  }
 }
}

Note

请注意,前面的查询需要允许内联脚本。这意味着查询需要在 elasticsearch 中将 script.inline 属性设置为 on。 yml 文件。

在这种情况下,聚合将使用的值将是原始 year 字段值减去 1000

我们还可以使用 Elasticsearch 的 值脚本功能。例如,要实现与前面的脚本相同,我们可以使用以下查询:

{
 "aggs": {
  "min_year": {
   "min": {
    "field" : "year",
    "script" : {
     "inline" : "_value - factor",
     "params" : {
      "factor" : 1000
     }
    }
   }
  }
 }
}

如果您不熟悉 Elasticsearch 脚本功能,可以在 Elasticsearch 的脚本功能 部分了解更多信息href="#" linkend="ch06">第 6 章让您的搜索更好

值得记住的一件事是,使用命令行可能需要正确转义 doc 数组中的值。例如,执行第一个脚本化查询的命令如下所示:

curl -XGET 'localhost:9200/library/_search?size=0&pretty' -d '{
 "aggs": {
  "min_year": {
   "min": {
    "script": "doc[\"year\"].value - 1000"
   }
  }
 }
}'

Field value statistics and extended statistics

我们将 讨论的下一个聚合是为我们提供有关我们正在运行的数字字段的统计信息的聚合 聚合:statsextended_stats 聚合。

例如,以下查询为 year 字段提供扩展统计信息:

{
 "aggs" : {
  "extended_statistics" : {
   "extended_stats" : {
    "field" : "year"
   }
  }
 }
}

对上述查询的响应如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "extended_statistics" : {
      "count" : 4,
      "min" : 1886.0,
      "max" : 1961.0,
      "avg" : 1928.0,
      "sum" : 7712.0,
      "sum_of_squares" : 1.4871654E7,
      "variance" : 729.5,
      "std_deviation" : 27.00925767213901,
      "std_deviation_bounds" : {
        "upper" : 1982.018515344278,
        "lower" : 1873.981484655722
      }
    }
  }
}

如您所见,在响应中,我们获得了有关 year 字段中具有值的文档数量、最小值、最大值、平均值和总和的信息。如果我们运行 stats 聚合而不是 extended_stats,这些是我们将获得的值。 extended_stats 聚合提供附加信息,例如 平方的总和、方差和标准差。 Elasticsearch 提供了两种类型的聚合,因为 extended_stats 在处理能力方面稍微贵一些。

Note

statsextended_stats 聚合,类似于 minmaxavgsum 聚合,支持脚本并允许我们指定哪个值应该用于在指定字段中没有值的字段。

Value count

value_count 聚合是一个简单的聚合,它允许对聚合文档中的值进行计数。当与嵌套聚合一起使用时,这 非常有用。我们现在不关注该主题,但要牢记这一点。例如,要在复制的字段上使用 value_count 聚合,我们将运行以下查询:

{
 "aggs" : {
  "count" : {
   "value_count" : {
    "field" : "copies"
   }
  }
 }
}

Note

value_count 聚合允许我们使用脚本,本章前面我们在描述 minmaxavgsum< /code> 聚合。请参阅本章前面的 Metrics 聚合部分的开头以获取更多参考。

Field cardinality

cardinality 聚合是允许我们通过控制其精度来控制聚合的资源消耗程度的聚合之一,它计算不同 给定字段中的值。但是,需要记住一件事:计算的计数是一个近似值,而不是确切的 值。 Elasticsearch 使用 HyperLogLog++ 算法 (http://static.googleusercontent.com/media/research.google.com/fr//pubs/archive/40671.pdf)来计算值。

此聚合具有多种用例,例如在负责保存索引 Apache 访问日志的状态代码的字段中显示不同值的数量。一个查询,您就知道该字段中不同值的近似计数。

例如,我们可以为 title 字段请求基数:

{
 "aggs" : {
  "card_title" : {
   "cardinality" : {
    "field" : "title"
   }
  }
 }
}

为了控制基数计算的精度,我们可以指定 precision_threshold 属性——值越高,聚合越精确,需要的资源越多。当前最大 precision_threshold 值为 40000,默认取决于父聚合。使用 precision_threshold 属性的示例查询如下所示:

{
 "aggs" : {
  "card_title" : {
   "cardinality" : {
    "field" : "title",
    "precision_threshold" : 1000
   }
  }
 }
}

Percentiles

百分位数聚合是 Elasticsearch 中聚合的另一个示例。它使用算法近似方法为我们提供结果。它使用 T-Digest 算法(https://github.com/tdunning/t-digest/blob/master/docs/t-digest-paper/histo.pdf) 来自 Ted Dunning 和 Otmar Ertl 并允许我们计算百分位数:向我们显示有多少结果高于某个值的指标。例如,第 99 个百分位向我们显示大于其他值 99% 的值。

让我们来看一个例子,看看一个查询,它将计算我们数据中年份字段的百分位数:

{
 "aggs" : {
  "copies_percentiles" : {
   "percentiles" : {
    "field" : "year"
   }
  }
 }
}

Elasticsearch 为上述请求返回的结果如下所示:

{
  "took" : 26,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "copies_percentiles" : {
      "values" : {
        "1.0" : 1887.2899999999997,
        "5.0" : 1892.4499999999998,
        "25.0" : 1918.25,
        "50.0" : 1932.5,
        "75.0" : 1942.25,
        "95.0" : 1957.25,
        "99.0" : 1960.25
      }
    }
  }
}

如您所见,高于 99% 的值是 1960.25

您可能想知道为什么 这样的聚合很重要。它对性能指标非常有用;例如,我们通常会查看一段时间内的平均值。想象一下,我们查询最后一小时的平均响应时间是 50 毫秒,这还不错。但是,如果第 95 个百分位显示 2 秒,那将意味着大约 5% 的用户必须等待两秒或更长时间才能获得搜索结果,这并不好。

默认情况下,percentiles 聚合计算七个百分位数:1、5、25、50、75、95 和 99。我们可以使用 percents 属性并指定我们感兴趣的百分位数。例如,如果我们只想获得第 95 个和第 99 个百分位数,我们将查询更改为以下一个:

{
 "aggs" : {
  "copies_percentiles" : {
   "percentiles" : {
    "field" : "year",
    "percents" : [ "95", "99" ]
   }
  }
 }
}

Note

类似于 minmaxavgsum 聚合,percentiles 聚合支持脚本,并允许我们为指定的字段中没有值的字段指定哪个值场地。

我们之前已经提到,百分位数聚合使用一种算法方法并且是一种近似值。与所有近似值一样,我们可以控制算法的精度和内存使用。我们通过使用 compression 属性来做到这一点,该属性默认为 100。它是 Elasticsearch 的内部属性,其实现细节可能会在版本之间发生变化。值得知道的是,将 compression 值设置为大于 100 的值可以以内存使用为代价来提高算法精度。

Percentile ranks

percentile_ranks 聚合是 类似于 percentiles 之一我们刚刚讨论过的。它允许我们显示给定值的百分位数。例如,为了向我们显示 19321960 的百分位年份,我们运行以下查询:

{
 "aggs" : {
  "copies_percentile_ranks" : {
   "percentile_ranks" : {
    "field" : "year",
    "values" : [ "1932", "1960" ]
   }
  }
 }
}

Elasticsearch 返回的响应如下:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "copies_percentile_ranks" : {
      "values" : {
        "1932.0" : 49.5,
        "1960.0" : 61.5
      }
    }
  }
}

Top hits aggregation

top_hits 聚合跟踪正在聚合的最相关文档。这听起来不是很吸引人,但它允许我们实现 Elasticsearch 中最需要的功能之一,称为文档分组、字段折叠或文档折叠。这样的功能在一些用例中非常有用——例如,当我们想要显示一个图书目录但只有一个出版商的图书目录时。要在没有 top_hits 聚合的情况下做到这一点,我们将不得不运行多个查询。使用 top_hits 聚合,我们只需要一个查询。

top_hits 聚合是在 Elasticsearch 1.3 中引入的。事实上,上面提到的文档折叠或多或少是一种副作用,并且只是 top_hits 聚合的可能使用示例之一。

top_hits 聚合背后的想法很简单。还可以记住分配给特定存储桶的每个文档。默认情况下,每个桶只记住三个文档。

Note

请注意,为了展示 top_hits 聚合的全部潜力,我们决定也使用其中一种桶聚合并将它们嵌套以显示文档分组功能的实现。本章稍后将详细描述分桶聚合。

为了向您展示一个利用 top_hits 聚合的潜在用例,我们决定使用一个简单的示例。我们希望每 100 年出版一次最相关的书。为此,我们使用以下查询:

{
 "aggs": {
  "when": {
   "histogram": {
    "field": "year",
    "interval": 100
   },
   "aggs": {
    "book": {
     "top_hits": {
      "_source": {
       "include": [ "title", "available" ]
      },
      "size": 1
     }
    }
   }
  }
 }
}

在前面的示例中,我们对年份范围进行了 histogram 聚合。每个桶每一百年创建一次。嵌套的 top_hits 聚合会记住每个存储桶中得分最高的单个文档(因为 size 属性设置为 1)。我们添加 include 选项只是为了更简单的结果,因此我们只 返回 titleavailable 字段。 Elasticsearch 返回的响应如下:

{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "when" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1,
        "book" : {
          "hits" : {
            "total" : 1,
            "max_score" : 1.0,
            "hits" : [ {
              "_index" : "library",
              "_type" : "book",
              "_id" : "4",
              "_score" : 1.0,
              "_source" : {
                "available" : true,
                "title" : "Crime and Punishment"
              }
            } ]
          }
        }
      }, {
        "key" : 1900,
        "doc_count" : 3,
        "book" : {
          "hits" : {
            "total" : 3,
            "max_score" : 1.0,
            "hits" : [ {
              "_index" : "library",
              "_type" : "book",
              "_id" : "2",
              "_score" : 1.0,
              "_source" : {
                "available" : false,
                "title" : "Catch-22"
              }
            } ]
          }
        }
      } ]
    }
  }
}

我们可以看到,由于 top_hits 聚合,我们在响应中包含了得分最高的文档(来自每个存储桶)。在我们的特定情况下,查询是 match_all 之一,并且所有文档都有相同的分数,因此每个桶的最高得分文档是 或多或少是随机的。但是,您需要记住这是默认行为。如果我们想要自定义排序,这对 Elasticsearch 来说不是问题。我们只需要为我们的 top_hits 聚合器添加 sort 属性。例如,我们可以返回给定世纪的第一本书:

{
 "aggs": {
  "when": {
   "histogram": {
    "field": "year",
    "interval": 100
   },
   "aggs": {
    "book": {
     "top_hits": {
      "sort": {
       "year": "asc"
      },
      "_source": {
       "include": [ "title", "available" ]
      },
      "size": 1
     }
    }
   }
  }
 }
}

我们在 top_hits 聚合中添加了排序,因此结果将根据 year 字段进行排序。这意味着 第一个文档将是该字段中具有最低值的文档,并且这是将为每个存储桶返回的文档。

Additional parameters

排序和字段包含不是 我们在 top_hits 聚合中可以做的所有事情。由于此聚合返回文档,我们还可以使用以下功能:

  • 突出显示

  • 解释

  • 脚本

  • fielddata 字段(字段的未反转表示)

  • 版本

我们只需要在 top_hits 聚合体中包含一个适当的部分,类似于我们在构造查询时所做的。例如:

{
 "aggs": {
  "when": {
   "histogram": {
    "field": "year",
    "interval": 100
   },
   "aggs": {
    "book": {
     "top_hits": {
      "highlight": {
       "fields": {
       "title": {}
       }
      },
      "explain": true,
      "version": true,
      "_source": {
       "include": [ "title", "available" ]
      },
      "fielddata_fields" : ["title"],
      "script_fields": {
       "century": {
        "script": "(doc[\"year\"].value / 100).intValue()"
       }
      },
      "size": 1
     }
    }
   }
  }
 }
}

Note

请注意,前面的查询要求允许内联脚本。这意味着 查询需要将 script.inline 属性设置为 elasticsearch.yml 文件中的

Geo bounds aggregation

geo_bounds 聚合是一个简单的聚合,它允许我们从聚合中计算包含所有 geo_point 类型字段值的边界框文件。

Note

如果您对 空间搜索感兴趣,专门介绍它的部分称为Geo 并包含在 第 8 章全文搜索之外

我们只需要提供字段(使用field属性;它需要是geo_point类型)。我们还可以提供 wrap_longitude (值 truefalse;它默认为true) 如果允许边界框与国际日期变更线重叠。作为响应,我们得到边界框左上角和右下角的纬度和经度。使用此聚合的示例查询如下所示(使用假设的 location 字段):

{
 "aggs" : {
  "box" : {
   "geo_bounds" : {
    "field" : "location"
   }
  }
 }
}

Scripted metrics aggregation

我们要讨论的最后一个指标聚合是 scripted_metric 聚合,它允许我们定义 我们自己的聚合使用脚本计算。对于这种聚合,我们可以提供以下脚本(map_script 是唯一必需的,其余是可选的):

  • init_script:这个脚本在初始化期间运行,允许我们设置计算的初始状态。

  • map_script:这是唯一 必需的脚本。对于需要将计算存储在名为 _agg 的对象中的每个文档,它都会执行一次。

  • combine_script:此脚本在 Elasticsearch 在该分片上完成文档收集后在每个分片上执行一次。

  • reduce_script:这个 脚本在协调特定查询执行的节点上执行一次。该脚本可以访问 _aggs 变量,该变量是 combine_script 返回的值的数组。

例如,我们可以使用 scripted_metric 聚合来计算我们图书馆中所有书籍的所有副本,方法是运行以下请求(我们显示整个请求以显示名称被转义):

curl -XGET 'localhost:9200/library/_search?size=0&pretty' -d '{
 "aggs" : {
  "all_copies" : {
   "scripted_metric" : {
    "init_script" : "_agg[\"all_copies\"] = 0",
    "map_script" : "_agg.all_copies += doc.copies.value",
    "combine_script" : "return _agg.all_copies",
    "reduce_script" : "sum = 0; for (number in _aggs) { sum += number }; return sum"
   }
  }
 }
}'

当然,前面的脚本只是一个简单的求和,我们可以使用求和聚合,但我们只是想向您展示一个简单的示例,说明您可以使用 scripted_metric 聚合做什么。

Note

请注意,前面的查询需要允许内联脚本。这意味着查询需要在 elasticsearch 中将 script.inline 属性设置为 on。 yml 文件。

如您所见,聚合的 init_script 部分用于初始化 all_copies 变量。接下来,我们有 map_script,它对每个文档执行一次,我们只需将 copies 字段的值添加到前面初始化变量。 combine_script 部分在每个分片上执行一次,告诉 Elasticsearch 返回计算的变量。最后,reduce_script 部分,在聚合器节点上为整个查询执行一次,将运行 for 循环,该循环将执行通过存储在 _aggs 数组中的所有返回值并返回这些值的总和。 Elasticsearch 为上述查询返回的最终结果如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "all_copies" : {
      "value" : 7
    }
  }
}

Buckets aggregations

我们将讨论的第二种聚合是桶聚合。与指标聚合相比,桶聚合返回的数据不是单个指标,而是一个称为桶的键值对列表。例如,terms 聚合返回与给定字段中的每个术语关联的 文档数。桶聚合非常强大的地方在于它们可以有子聚合,这意味着 我们可以在返回桶的聚合中嵌套其他聚合(我们将在桶聚合讨论结束时讨论这个)。现在让我们看一下 Elasticsearch 提供的存储桶聚合。

Filter aggregation

filter 聚合是一个 简单的桶聚合,它允许我们将结果过滤到单个桶中。例如,假设我们要获取所有小说书籍的计数和平均拷贝数,这意味着它们在 novel “文字”>标签字段。将返回此类结果的查询如下所示:

{
 "aggs" : {
  "novels_count" : {
   "filter" : {
    "term": {
     "tags": "novel"
    }
   },
   "aggs" : {
    "avg_copies" : {
     "avg" : {
      "field" : "copies"
     }
    }
   }
  }
 }
}

如您所见,我们在聚合定义的 filter 部分定义了过滤器,并且我们定义了第二个嵌套聚合。嵌套聚合是将在过滤后的文档上运行的聚合。

Elasticsearch 返回的响应如下所示:

{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "novels_count" : {
      "doc_count" : 2,
      "avg_copies" : {
        "value" : 3.5
      }
    }
  }
}

在返回的存储桶中,我们 有关于文档数量的信息(由 doc_count 属性表示)和平均副本数,这就是我们想要的。

Filters aggregation

我们要向您展示的第二个桶聚合是 filters 聚合。虽然 之前讨论过的 filter 聚合产生了一个单独的桶,但 filters 聚合返回多个桶——每个定义的过滤器一个。让我们扩展前面的例子,假设除了小说的平均拷贝数之外,我们还想知道可用书籍的平均拷贝数。将为我们获取此信息的查询将使用 filters 聚合,如下所示:

{
 "aggs" : {
  "count" : {
   "filters" : {
    "filters" : {
     "novels" : {
      "term" : {
       "tags" : "novel"
      }
     },
     "available" : {
      "term" : {
       "available" : true
      }
     }
    }
   },
   "aggs" : {
    "avg_copies" : {
     "avg" : {
      "field" : "copies"
     }
    }
   }
  }
 }
}

让我们在这里停下来看看聚合的定义。如您所见,我们使用 filters 聚合的 filters 部分定义了两个过滤器。每个过滤器都有一个名称和实际的 Elasticsearch 过滤器;第一个称为 novels,第二个称为 available。 Elasticsearch 将在返回的响应中使用这些名称。要记住的是,Elasticsearch 将为每个定义的过滤器创建一个存储桶,并将计算我们定义的嵌套 聚合——在我们的例子中,计算平均份数。

Note

filters 聚合允许我们在定义的存储桶之外再返回一个存储桶——一个包含所有与过滤器不匹配的文档的存储桶。为了计算这样一个桶,我们需要将 other_bucket 属性添加到聚合的主体中,并将​​其设置为 true .

Elasticsearch 返回的结果如下:

{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "count" : {
      "buckets" : {
        "novels" : {
          "doc_count" : 2,
          "avg_copies" : {
            "value" : 3.5
          }
        },
        "available" : {
          "doc_count" : 2,
          "avg_copies" : {
            "value" : 0.5
          }
        }
      }
    }
  }
}

如您所见,我们得到了两个桶,这是我们所期望的

Terms aggregation

最常用的桶聚合之一是 terms 聚合。它使我们能够获取有关条款的信息以及具有这些条款的文档的数量。例如,最简单的用途之一是获取 可用和不可用书籍的计数。我们可以通过运行以下查询来做到这一点:

{
 "aggs" : {
  "counts" : {
   "terms" : {
    "field" : "available"
   }
  }
 }
}

在响应中,我们会得到两个桶(因为 Boolean 字段只能有两个值 - true“假”)。在这里,这将如下所示:

{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "counts" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : 0,
        "key_as_string" : "false",
        "doc_count" : 2
      }, {
        "key" : 1,
        "key_as_string" : "true",
        "doc_count" : 2
      } ]
    }
  }
}

默认情况下,数据是根据文档计数排序的,这意味着最常用的术语将放在聚合结果的顶部。当然,我们可以像往常一样通过指定order属性并提供顺序来控制这种行为按任意字段值排序时执行。 Elasticsearch 允许我们按文档计数(使用 _count 静态值)和术语(使用 _term 静态值)进行排序)。例如,如果我们想按降序对前面的聚合结果进行排序,我们可以运行以下查询:

{
 "aggs" : {
  "counts" : {
   "terms" : {
    "field" : "available",
    "order" : { "_term" : "desc" }   }
  }
 }
}

然而,在排序方面,这还不是全部。我们还可以按查询中包含的嵌套聚合的结果进行排序。

Note

terms 聚合,类似于 min, max, avg,sum 聚合在本文的指标 聚合部分中讨论章,支持脚本并允许我们指定应该为指定字段中没有值的字段使用哪个值。

Counts are approximate

在讨论 terms 聚合时要记住的是计数是近似的。这 是因为每个分片都提供自己的计数并将聚合信息返回给协调节点。协调节点聚合它获得的信息,将最终信息返回给客户端。因此,根据数据及其在分片之间的分布方式,一些有关计数的信息可能会丢失,并且计数将不准确。当然,在处理低基数字段时,近似值会更接近精确数字,但这仍然是使用 terms 聚合时应该考虑的问题。

我们可以控制从每个分片返回到协调节点的信息量。我们可以通过指定 sizeshard_size 属性来做到这一点。 size 属性指定最多返回多少个桶。 size 属性越高,计算就越准确。但是,这将花费我们额外的内存和 CPU 周期,这意味着计算将更加昂贵,并且会给硬件带来更大的压力。这是因为从每个分片返回到协调节点的结果会更大,结果合并过程会更难。

shard_size 属性可用于最小化协调节点需要完成的工作。设置后,协调节点将(从每个分片)获取由 shard_size 属性确定的桶数。这使我们能够提高聚合的精度,同时避免协调节点上的额外开销。请记住,shard_size 属性不能小于 size 属性。

最后,size 属性可以设置为 0,这将告诉 Elasticsearch 不要限制返回的桶数。将 size 属性设置为 0 通常是不明智的,因为它会导致高资源消耗。此外,对于高基数字段,请避免将 size 属性设置为 0,因为这可能会使您的 Elasticsearch 集群爆炸。

Minimum document count

Elasticsearch 为我们提供了两个额外的属性,它们在某些情况下很有用:min_doc_countshard_min_doc_countmin_doc_count 属性默认为 1 并指定有多少个 文档必须与要包含在聚合结果中的术语匹配。要记住的一件事是,将 min_doc_count 属性设置为 0 将导致返回所有术语,无论它们是否有是否匹配文件。这可能会导致聚合结果的结果集非常大。例如,如果我们想要返回与 5 或更多文档匹配的词,我们将运行以下查询:

{
 "aggs" : {
  "counts" : {
   "terms" : {
    "field" : "available",
    "min_doc_count" : 5   }
  }
 }
}

shard_min_doc_count 属性非常相似,它定义了有多少文档必须匹配一个术语才能包含在聚合结果中,但在分片级别。

Range aggregation

range 聚合允许我们定义一个或多个 范围和 Elasticsearch 为它们计算存储桶。例如,如果我们想检查在给定时间段内出版了多少本书,我们创建以下查询:

{
 "aggs": {
  "years": {
   "range": {
    "field": "year",
    "ranges": [
     { "to" : 1850 },
     { "from": 1851, "to": 1900 },
     { "from": 1901, "to": 1950 },
     { "from": 1951, "to": 2000 },
     { "from": 2001 }
    ]
   }
  }
 }
}

我们指定要计算聚合的字段和范围数组。每个范围由一个或两个属性定义: twofrom 类似于我们已经讨论过的范围查询。

Elasticsearch 为我们的数据返回的 结果如下所示:

{
  "took" : 23,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "years" : {
      "buckets" : [ {
        "key" : "*-1850.0",
        "to" : 1850.0,
        "to_as_string" : "1850.0",
        "doc_count" : 0
      }, {
        "key" : "1851.0-1900.0",
        "from" : 1851.0,
        "from_as_string" : "1851.0",
        "to" : 1900.0,
        "to_as_string" : "1900.0",
        "doc_count" : 1
      }, {
        "key" : "1901.0-1950.0",
        "from" : 1901.0,
        "from_as_string" : "1901.0",
        "to" : 1950.0,
        "to_as_string" : "1950.0",
        "doc_count" : 2
      }, {
        "key" : "1951.0-2000.0",
        "from" : 1951.0,
        "from_as_string" : "1951.0",
        "to" : 2000.0,
        "to_as_string" : "2000.0",
        "doc_count" : 1
      }, {
        "key" : "2001.0-*",
        "from" : 2001.0,
        "from_as_string" : "2001.0",
        "doc_count" : 0
      } ]
    }
  }
}

例如,在 19011950 之间,我们发布了两本书。

Note

range 聚合,类似于 min, max, avg,sum 聚合在 metrics 聚合部分讨论本章支持脚本,并允许我们指定应该 用于指定字段中没有值的字段。

Keyed buckets

当涉及到 range 聚合时,应该 提到的一件事是我们可以给定义的范围名称.例如,假设我们要为 1799 之前发行的书籍使用名称 Before 18th century18001900 之间发行的书籍的 "literal">18 世纪,19 世纪 用于 19001999,之后发布的书籍2000 之后出版的书籍的 19 世纪。我们可以通过将 key 属性添加到每个定义的范围,为其命名,并将 keyed 属性设置为。将 keyed 属性设置为 true 会将唯一的字符串值关联到每个存储桶和 key 属性定义将用作唯一名称的存储桶的名称。执行此操作的查询如下所示:

{
 "aggs": {
  "years": {
   "range": {
    "field": "year",
    "keyed": true,
    "ranges": [
     { "key": "Before 18th century", "to": 1799 },
     { "key": "18th century", "from": 1800, "to": 1899 },
     { "key": "19th century", "from": 1900, "to": 1999 },
     { "key": "After 19th century", "from": 2000 }
    ]
   }
  }
 }
}

这样的情况下Elasticsearch返回的响应会如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "years" : {
      "buckets" : {
        "Before 18th century" : {
          "to" : 1799.0,
          "to_as_string" : "1799.0",
          "doc_count" : 0
        },
        "18th century" : {
          "from" : 1800.0,
          "from_as_string" : "1800.0",
          "to" : 1899.0,
          "to_as_string" : "1899.0",
          "doc_count" : 1
        },
        "19th century" : {
          "from" : 1900.0,
          "from_as_string" : "1900.0",
          "to" : 1999.0,
          "to_as_string" : "1999.0",
          "doc_count" : 3
        },
        "After 19th century" : {
          "from" : 2000.0,
          "from_as_string" : "2000.0",
          "doc_count" : 0
        }
      }
    }
  }
}

Note

关于 range 聚合的一个重要且非常有用的点是定义的范围不必是不相交的。在这种情况下,Elasticsearch 将正确计算多个存储桶的文档。

Date range aggregation

date_range 聚合类似于前面讨论的 range 聚合,但它是为使用基于日期的类型的字段设计的。但是,在 library 索引中,文档有年份,但字段是 一个数字,而不是一个日期。为了展示这种聚合是如何工作的,让我们假设我们想要扩展我们的 library 索引来支持报纸。为此,我们将使用以下命令创建一个新索引(称为 library2):

curl -XPOST localhost:9200/_bulk --data-binary '{ "index": {"_index": "library2", "_type": "book", "_id": "1"}}
{ "title": "Fishing news", "published": "2010/12/03 10:00:00", "copies": 3, "available": true }
{ "index": {"_index": "library2", "_type": "book", "_id": "2"}}
{ "title": "Knitting magazine", "published": "2010/11/07 11:32:00", "copies": 1, "available": true }
{ "index": {"_index": "library2", "_type": "book", "_id": "3"}}
{ "title": "The guardian", "published": "2009/07/13 04:33:00", "copies": 0, "available": false }
{ "index": {"_index": "library2", "_type": "book", "_id": "4"}}
{ "title": "Hadoop World", "published": "2012/01/01 04:00:00", "copies": 6, "available": true }
'

出于本示例的目的,我们将保留 Elasticsearch 的映射定义——在这种情况下就足够了。让我们从使用 date_range 聚合的第一个查询开始:

{
 "aggs": {
  "years": {
   "date_range": {
    "field": "published",
    "ranges": [
     { "to" : "2009/12/31" },
     { "from": "2010/01/01", "to": "2010/12/31" },
     { "from": "2011/01/01" }
    ]
   }
  }
 }
}

与普通的range聚合相比,唯一改变的是聚合类型,现在是date_range。日期可以作为字符串以 Elasticsearch 识别的形式传递,也可以作为数字值(自 1970-01-01 以来的毫秒数)传递。 Elasticsearch 针对上述查询返回的 响应如下所示:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "years" : {
      "buckets" : [ {
        "key" : "*-2009/12/31 00:00:00",
        "to" : 1.2622176E12,
        "to_as_string" : "2009/12/31 00:00:00",
        "doc_count" : 1
      }, {
        "key" : "2010/01/01 00:00:00-2010/12/31 00:00:00",
        "from" : 1.262304E12,
        "from_as_string" : "2010/01/01 00:00:00",
        "to" : 1.2937536E12,
        "to_as_string" : "2010/12/31 00:00:00",
        "doc_count" : 2
      }, {
        "key" : "2011/01/01 00:00:00-*",
        "from" : 1.29384E12,
        "from_as_string" : "2011/01/01 00:00:00",
        "doc_count" : 1
      } ]
    }
  }
}

正如您所见,range 聚合返回的响应相比,响应没有什么不同。每个桶都有两个属性 - 名为 fromto,它们表示从 1970 年 1 月 1 日开始的毫秒数。 from_as_stringto_as_string 属性与 from 提供相同的信息class="literal">to, 但以人类可读的形式。当然,日期范围定义中的 keyed 参数和 key 以已经描述的方式工作。

Elasticsearch 还允许我们使用 format 属性定义显示日期的格式。在我们的示例中,我们以年份分辨率呈现日期,因此不需要日期和时间部分。如果我们想显示月份名称,我们可以发送如下查询:

{
 "aggs": {
  "years": {
   "date_range": {
    "field": "published",
    "format": "MMMM YYYY",
    "ranges": [
     { "to" : "December 2009" },
     { "from": "January 2010", "to": "December 2010" },
     { "from": "January 2011" }
    ]
   }
  }
 }
}

请注意,tofrom 参数中的日期也需要以指定格式提供。返回的范围之一如下所示:

{
 "key" : "January 2010-December 2010",
 "from" : 1.262304E12,
 "from_as_string" : "January 2010",
 "to" : 1.2911616E12,
 "to_as_string" : "December 2010",
 "doc_count" : 1
}

Note

我们 可以在 format 中使用的可用格式在 Joda Time 库中定义。完整列表可在 http: //joda-time.sourceforge.net/apidocs/org/joda/time/format/DateTimeFormat.html

关于 date_range 聚合,我们还想提一件事。想象一下,有时我们可能想要构建一个可以随时间变化的聚合。例如,我们可能想查看过去 3、6、9 和 12 个月内出版了多少份报纸。这是可能的,不需要每次都调整查询,因为我们可以使用诸如 now-9M 之类的常量。以下示例显示了这一点:

{
 "aggs": {
  "years": {
   "date_range": {
    "field": "published",
    "format": "dd-MM-YYYY",
    "ranges": [
     { "to" : "now-9M/M"  },
     { "to" : "now-9M"  },
     { "from": "now-6M/M", "to": "now-9M/M" },
     { "from": "now-3M/M" }
    ]
   }
  }
 }
}

这里的关键是表达式,例如now-9M。 Elasticsearch 进行数学运算并生成适当的值。例如,您可以使用 y(年)、M(月)、w< /code>(周),d(天),h(小时),m (分钟)和 s(秒)。例如,表达式 now+3d 表示从现在起三天。我们示例中的 /M 仅采用四舍五入为月的日期。由于这种表示法,我们只计算整月。第二个优点是计算的日期对缓存更友好,不会每毫秒更改舍入日期,这使得基于范围的每个缓存在大多数情况下都无关紧要并且基本上无用。

IPv4 range aggregation

一个非常有趣的聚合ip_range 聚合,因为它适用于 Internet 地址。它适用于使用 ip 类型定义的字段,并允许定义 CIDR 中 IP 范围给定的 范围表示法(http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing< /a>)。 ip_range 聚合的示例用法如下所示:

{
 "aggs": {
  "access": {
   "ip_range": {
    "field": "ip",
    "ranges": [
     { "from": "192.168.0.1", "to": "192.168.0.254" },
     { "mask": "192.168.1.0/24" }
    ]
   }
  }
 }
}

对上述查询的响应如下:

      "access": {
         "buckets": [
            {
               "from": 3232235521,
               "from_as_string": "192.168.0.1",
               "to": 3232235774,
               "to_as_string": "192.168.0.254",
               "doc_count": 0
            },
            {
               "key": "192.168.1.0/24",
               "from": 3232235776,
               "from_as_string": "192.168.1.0",
               "to": 3232236032,
               "to_as_string": "192.168.2.0",
               "doc_count": 4
            }
         ]
      }

类似于 range 聚合,我们定义了括号和掩码的两端。其余的由 Elasticsearch 自己完成。

Missing aggregation

missing 聚合允许我们创建一个bucket并查看有多少文档在指定字段中没有值.例如,我们可以检查 library 索引中有多少书籍没有定义原始标题 - otitle场地。为此,我们运行以下查询:

{
 "aggs": {
  "missing_original_title": {
   "missing": {
    "field": "otitle"
   }
  }
 }
}

在这种情况下,Elasticsearch 返回的响应如下所示:

{
  "took" : 15,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "missing_original_title" : {
      "doc_count" : 2
    }
  }
}

正如我们所见,我们有两个没有 otitle 字段的 文档。

Histogram aggregation

histogram 聚合是 一个有趣的聚合,因为它是自动化的。此聚合定义了存储桶本身。我们只负责定义字段和区间,其余的都是自动完成的。使用此聚合的最简单查询形式如下所示:

{
 "aggs": {
  "years": {
   "histogram": {
    "field" : "year",
    "interval": 100
   }
  }
 }
}

我们需要提供的新信息是 interval,它定义了将用于创建存储桶的每个范围的长度。我们将间隔设置为 100,在我们的例子中,这将产生 100 年宽的桶。发送到我们的 library 索引的对前面查询的响应的聚合部分如下:

{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "years" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1
      }, {
        "key" : 1900,
        "doc_count" : 3
      } ]
    }
  }
}

类似于 range 聚合,histogram 聚合允许我们使用 keyed 属性来定义命名的存储桶。另一个可用选项是 min_doc_count,它允许我们 指定创建一个桶。如果我们将 min_doc_count 属性设置为 zero,Elasticsearch 还将包含文档计数为零的存储桶。我们还可以使用 missing 属性来指定当文档在指定字段中没有值时 Elasticsearch 应该使用的值。

Date histogram aggregation

作为 date_range 聚合range 聚合,date_histogramhistogram的扩展代码>聚合,适用于日期。出于本示例的目的,我们将再次使用我们在讨论 date 聚合时索引的数据。这意味着我们将针对名为 library2 的索引运行查询。使用 date_histogram 聚合的示例查询如下所示:

{
 "aggs": {
  "years": {
   "date_histogram": {
    "field" : "published",
    "format" : "yyyy-MM-dd HH:mm",
    "interval" : "10d", 
    "min_doc_count" : 1   }
  }
 }
}

histogramdate_histogram 聚合之间的区别在于 interval 属性。这个属性的值现在是一个描述时间间隔的字符串,在我们的例子中是 10 天。当然,我们可以将其设置为任何我们想要的。它使用了我们在讨论 date_range 聚合中的格式时讨论过的相同后缀 。值得一提的是,数字可以是 float 值。例如,1.5m 表示桶的长度为一分半钟。 format 属性与 date_range 聚合中的属性相同。多亏了它,Elasticsearch 可以根据定义的格式添加人类可读的日期文本。当然 format 属性不是必需的,但很有用。除此之外,与其他范围聚合类似,keyedmin_doc_count 属性仍然有效。

Time zones

Elasticsearch 将所有 日期存储在 UTC 时区中。您可以使用 time_zone 属性定义 Elasticsearch 使用的时区。通过设置这个属性,我们基本上告诉 Elasticsearch 应该使用哪个时区来执行计算。可以使用三种符号来设置这些属性:

Geo distance aggregations

接下来的两个聚合与地图和空间搜索相关联。我们将在 Elasticsearch 空间功能 部分讨论地理类型和 查询第 8 章超越全文搜索 ,所以现在可以跳过这两个主题,稍后再返回

查看以下查询:

{
 "aggs": {
  "neighborhood": {
   "geo_distance": {
    "field": "location",
    "origin": [-0.1275, 51.507222],
    "ranges": [
     { "to": 1200 },
     { "from": 1201 }
    ]
   }
  }
 }
}

可以看到查询类似于 range 聚合。前面的聚合将计算落入两个存储桶的文档数量:一个距离 origin 属性定义的地理点的距离小于 1200 公里,第二个距离大于 1200 公里(在前一种情况下,起点是伦敦)。 Elasticsearch 返回的响应的聚合部分如下所示:

      "neighborhood": {
         "buckets": [
            {
               "key": "*-1200.0",
               "from": 0,
               "to": 1200,
               "doc_count": 1
            },
            {
               "key": "1201.0-*",
               "from": 1201,
               "doc_count": 4
            }
         ]
      }

keyedkey 属性也适用于 geo_distance 聚合,因此我们可以轻松地根据我们的需求修改响应并创建命名存储桶。

geo_distance 聚合 支持以下查询中显示的一些附加参数:

{
 "aggs": {
  "neighborhood": {
   "geo_distance": {
    "field": "location",
    "origin": { "lon": -0.1275, "lat": 51.507222},
    "unit": "m",
    "distance_type" : "plane",
    "ranges": [
     { "to": 1200 },
     { "from": 1201 }
    ]
   }
  }
 }
}

我们在前面的查询中强调了三件事。第一个变化是我们如何定义 origin 点。这次我们通过明确提供纬度和经度来指定位置。

第二个变化unit 属性。它定义了 ranges 数组中使用的单位。可能的值为:km(默认值,公里)、mi(英里)、 in (英寸),yd(码),m(米),cm(厘米)和 mm(毫米)。

最后一个属性 distance_type, 指定 Elasticsearch 如何计算距离。可能的值是(从最快但最不准确到最慢但最准确):planesloppy_arc(默认) 和 arc

Geohash grid aggregation

第二个聚合与地理相关的分析是基于网格的,称为geohash_grid。它将区域组织成网格,并将每个位置分配给这样的网格中的一个单元格。为了有效地做到这一点,Elasticsearch 使用 Geohash (http://en.wikipedia.org/wiki/Geohash),它将位置编码为细绳。字符串越长,对特定位置的描述就越准确。例如,一个字母足以声明一个大约五千平方公里的盒子,5个字母足以将准确度提高到五平方公里。让我们看一下以下查询:

{
 "aggs": {
  "neighborhood": {
   "geohash_grid": {
    "field": "location",
    "precision": 5
   }
  }
 }
}

我们用精度为 5 平方公里的桶定义了 geohash_grid 聚合(precision 属性描述了在geohash 字符串对象)。表格的长度 geohash 可以在 https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-bucket-geohashgrid-aggregation.html

当然,我们希望聚合越准确,Elasticsearch 将消耗的资源就越多,因为聚合必须计算的桶数。默认情况下,Elasticsearch 不会生成超过 10,000 个桶。您可以使用 size 属性更改此行为,但请记住,对于包含数千个存储桶的非常宽的查询,性能可能会受到影响。

Global aggregation

global 聚合是一个聚合,它定义了一个单独的桶 包含来自给定索引和类型的所有文档,不受查询本身的影响。 global 聚合与所有其他聚合的区别在于 global 聚合的主体为空。例如,查看以下查询:

{
 "query" : {
  "term" : {
   "available" : "true"
  }
 },
 "aggs": {
  "all_books" : {
   "global" : {}
  }
 }
}

在我们的 library 索引中,我们只有两本书可用,但对前面查询的响应看起来像如下:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "all_books" : {
      "doc_count" : 4
    }
  }
}

如您所见,global 聚合不受查询约束。因为 global 聚合的结果是一个 包含所有文档的单个桶(不是由查询本身),它非常适合用作嵌套聚合的顶级父聚合。

Significant terms aggregation

significant_terms 聚合允许我们获取与给定查询相关且可能最重要的术语。好消息是它不仅显示了来自 给定查询结果的最重要的词,而且还显示了似乎最重要的词一。

这种 聚合类型的用例可能会有所不同,从查找在您的应用程序环境中工作的最麻烦的服务器到从文本中建议昵称。每当 Elasticsearch 发现一个词的流行度发生显着变化时,这个词就可以成为重要的候选词。

Note

请记住,当涉及到资源和针对大型索引运行时,significant_terms 聚合非常昂贵。正在开展工作以提供该聚合的轻量级版本;因此,significant_terms 聚合的 API 将来可能会发生变化。

描述 significant_terms 聚合类型的最佳方式是使用示例。让我们从索引 12 个简单文档开始,这些文档代表对实习生所做工作的评论:

curl -XPOST 'localhost:9200/interns/review/1' -d '{"intern" : "Richard", "grade" : "bad", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/2' -d '{"intern" : "Ralf", "grade" : "perfect", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/3' -d '{"intern" : "Richard", "grade" : "bad", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/4' -d '{"intern" : "Richard", "grade" : "bad", "type" : "review"}'
curl -XPOST 'localhost:9200/interns/review/5' -d '{"intern" : "Richard", "grade" : "good", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/6' -d '{"intern" : "Ralf", "grade" : "good", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/7' -d '{"intern" : "Ralf", "grade" : "perfect", "type" : "review"}'
curl -XPOST 'localhost:9200/interns/review/8' -d '{"intern" : "Richard", "grade" : "medium", "type" : "review"}'
curl -XPOST 'localhost:9200/interns/review/9' -d '{"intern" : "Monica", "grade" : "medium", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/10' -d '{"intern" : "Monica", "grade" : "medium", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/11' -d '{"intern" : "Ralf", "grade" : "good", "type" : "grade"}'
curl -XPOST 'localhost:9200/interns/review/12' -d '{"intern" : "Ralf", "grade" : "good", "type" : "grade"}'

当然,为了展示 significant_terms 聚合的真正威力,我们应该使用更大的数据集的方式。但是,出于本书的目的,我们将专注于这个示例,因此更容易说明这种聚合是如何工作的。

现在让我们尝试为 Richard 找出最重要的成绩。为此,我们将使用以下查询:

curl -XGET 'localhost:9200/interns/_search?size=0&pretty' -d '{
 "query" : {
  "match" : {
   "intern" : "Richard"
  }
 },
 "aggregations" : {
  "description" : {
   "significant_terms" : {
    "field" : "grade"
   }
  }
 }
}'

前面的查询结果如下:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 5,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "description" : {
      "doc_count" : 5,
      "buckets" : [ {
        "key" : "bad",
        "doc_count" : 3,
        "score" : 0.84,
        "bg_count" : 3
      } ]
    }
  }
}

如您所见,对于我们的 查询,Elasticsearch 告诉我们 Richard 最重要的成绩是 bad。也许这对他来说不是最好的实习;谁知道。

Choosing significant terms

为了计算重要的术语,Elasticsearch 会查找在两组数据之间报告其受欢迎程度发生显着变化的数据< strong>foreground 集和 background 集。前台集是我们查询返回的数据,而后台集是数据 在我们的索引(或索引,取决于我们如何运行查询)中。如果一个词存在于 100 万个索引的文档中的 10 个中,但出现在 10 个返回的文档中的 5 个中,那么这样的词绝对是重要的,值得关注。

现在让我们回到前面的例子来分析一下。理查德从审稿人那里获得了三个等级 - bad 三次,medium 一次,一次。从这三个中,错误值出现在与查询匹配的五个文档中的三个中。一般来说,在索引中的 12 个文档(这是我们的背景集)中,有 3 个文档(bg_count 属性)出现了不良评分。这为我们提供了 25% 的索引文档。另一方面,与查询匹配的五个文档中有三个(这是我们的前景集)出现了不良评分,这为我们提供了 60% 的文档。如您所见,流行度的变化对于 bad 等级来说意义重大,这就是 Elasticsearch 在 significant_terms 聚合中返回它的原因结果。

Multiple value analysis

significant_terms 聚合可以嵌套,并为我们提供很好的数据分析能力, 连接两个多组数据.例如,让我们尝试为我们掌握的每个实习生找到一个重要的成绩。为此,我们将 significant_terms 聚合嵌套在 terms 聚合中。执行此操作的查询如下所示:

curl -XGET 'localhost:9200/interns/_search?size=0&pretty' -d '{
 "aggregations" : {
  "grades" : {
   "terms" : {
    "field" : "intern"
   },
   "aggregations" : {
    "significantGrades" : {
     "significant_terms" : {
      "field" : "grade"
     }
    }
   }
  }
 }
}'

Elasticsearch 对上述 查询返回的结果如下:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 12,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "grades" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : "ralf",
        "doc_count" : 5,
        "significantGrades" : {
          "doc_count" : 5,
          "buckets" : [ {
            "key" : "good",
            "doc_count" : 3,
            "score" : 0.48,
            "bg_count" : 4
          } ]
        }
      }, {
        "key" : "richard",
        "doc_count" : 5,
        "significantGrades" : {
          "doc_count" : 5,
          "buckets" : [ {
            "key" : "bad",
            "doc_count" : 3,
            "score" : 0.84,
            "bg_count" : 3
          } ]
        }
      }, {
        "key" : "monica",
        "doc_count" : 2,
        "significantGrades" : {
          "doc_count" : 2,
          "buckets" : [ ]
        }
      } ]
    }
  }
}

Sampler aggregation

sampler 聚合是 Elasticsearch 中的实验性聚合之一。它允许我们将子 聚合处理限制为 的文档样本得分最高的。这允许过滤和潜在地删除数据中的垃圾。这是一个非常好的候选者作为顶级聚合来限制 significant_terms 聚合运行的数据量。使用此聚合的最简单示例如下:

{
 "aggs": {
  "sampler_example" : {
   "sampler" : {
    "field" : "tags",
    "max_docs_per_value" : 1,
    "shard_size" : 10
   },
   "aggs" : {
    "best_terms" : {
     "terms" : {
      "field" : "title"
     }
    }
   }
  }
 }
}

要了解采样的真正威力,我们必须在更大的数据集上使用它,但现在我们将讨论前面的示例。 sampler 聚合定义了三个属性:fieldmax_docs_per_value、shard_size。前两个属性允许我们控制采样的多样性。我们告诉 Elasticsearch 最多可以在一个分片上收集多少个文档(max_doc_per_value 属性的值),并且在定义的字段中具有相同的值(字段 属性)。

shard_size 属性 告诉 Elasticsearch(最多)从每个分片收集多少文档。

Children aggregation

children 聚合是一个 单桶聚合,它创建了一个 包含指定类型的所有子项的存储桶。让我们回到中的使用父子关系部分第 5 章扩展您的索引结构,让我们使用创建的商店索引。要在商店索引中创建具有变体类型的所有子文档的存储桶,我们运行以下查询:

{
 "aggs": {
  "variation_children" : {
   "children" : {
    "type" : "variation"
   }
  }
 }
}

Elasticsearch 返回的响应如下:

{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "variation_children" : {
      "doc_count" : 2
    }
  }
}

Note

因为 children 聚合使用父子功能,它依赖于 _parent 字段,其中 需要存在。

Nested aggregation

第5章使用嵌套对象部分, 扩展您的索引结构,我们了解了嵌套文档。让我们使用这些数据来研究下一种聚合类型——嵌套的一个.让我们创建一个最简单的工作查询,如下所示(我们使用在上述章节中创建的 shop_nested 索引):

{
 "aggs": {
  "variations": {
   "nested": {
    "path": "variation"
   }
  }
 }
}

前面的查询在结构上与任何其他聚合类似。但是,它没有提供应该计算聚合的字段名称,而是包含一个指向嵌套文档的参数 path。在响应中,我们得到了一些嵌套文档:

{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "variations" : {
      "doc_count" : 2
    }
  }
}

前面的响应意味着我们在索引中有两个嵌套文档,提供的类型为 variation

Reverse nested aggregation

reverse_nested 聚合是一种特殊的单桶聚合,它允许从嵌套文档聚合父 文档. reverse_nested 聚合没有类似于全局聚合的主体。听起来很复杂,但事实并非如此。让我们看一下我们针对 Chapter 5shop_nested 索引运行的以下查询>,扩展您的索引结构使用嵌套对象部分:

{
 "aggs": {
  "variations": {
   "nested": {
    "path": "variation"
   },
   "aggs" : {
    "sizes" : {
     "terms" : {
      "field" : "variation.size"
     },
     "aggs" : {
      "product_name_terms" : {
       "reverse_nested" : {},
       "aggs" : {
        "product_name_terms_per_size" : {
         "terms" : {
          "field" : "name"
         }
        }
       }
      }
     }
    }
   }
  }
 }
}

我们从顶级聚合开始,它与我们在讨论 nested 聚合时使用的 nested 聚合相同。但是,我们包含一个使用 reverse_nested 的子聚合,以便能够针对顶级嵌套聚合返回的每个大小显示标题中的术语。这是可能的,因为当使用 reverse_nested 聚合时,Elasticsearch 会根据父文档计算数据,而不是使用嵌套的 文档。

Note

请记住,reverse_nested 聚合必须在 nested 聚合内使用。

前面的查询的响应如下所示:

{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "variations" : {
      "doc_count" : 2,
      "sizes" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [ {
          "key" : "XL",
          "doc_count" : 1,
          "product_name_terms" : {
            "doc_count" : 1,
            "product_name_terms_per_size" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [ {
                "key" : "shirt",
                "doc_count" : 1
              }, {
                "key" : "test",
                "doc_count" : 1
              } ]
            }
          }
        }, {
          "key" : "XXL",
          "doc_count" : 1,
          "product_name_terms" : {
            "doc_count" : 1,
            "product_name_terms_per_size" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [ {
                "key" : "shirt",
                "doc_count" : 1
              }, {
                "key" : "test",
                "doc_count" : 1
              } ]
            }
          }
        } ]
      }
    }
  }
}

Nesting aggregations and ordering buckets

当谈到 bucket 聚合时,我们只需要回到嵌套聚合的主题。这是一项非常强大的技术,因为它允许您进一步 处理存储桶中文档的数据。对于 示例,terms 聚合将为每个术语返回一个桶,而 stats 聚合可以向我们显示每个存储桶中文档的统计信息。例如,让我们看一下以下查询:

{
 "aggs": {
  "copies" : {
   "terms" : {
    "field" : "copies"
   },
   "aggs" : {
    "years" : {
     "stats" : {
      "field" : "year"
     }
    }
   }
  }
 }
}

这是 嵌套 聚合的示例。 terms 聚合将从副本字段中返回每个术语的存储桶(在我们的数据中是三个存储桶),以及 stats 聚合将计算落入 顶部聚合返回的每个桶的文档的年份字段的统计信息。 Elasticsearch 对上述查询的响应如下所示:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "copies " : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ {
        "key" : 0,
        "doc_count" : 2,
        "years" : {
          "count" : 2,
          "min" : 1886.0,
          "max" : 1936.0,
          "avg" : 1911.0,
          "sum" : 3822.0
        }
      }, {
        "key" : 1,
        "doc_count" : 1,
        "years" : {
          "count" : 1,
          "min" : 1929.0,
          "max" : 1929.0,
          "avg" : 1929.0,
          "sum" : 1929.0
        }
      }, {
        "key" : 6,
        "doc_count" : 1,
        "years" : {
          "count" : 1,
          "min" : 1961.0,
          "max" : 1961.0,
          "avg" : 1961.0,
          "sum" : 1961.0
        }
      } ]
    }
  }
}

这是一个强大的功能,允许我们构建非常复杂的数据处理管道。当然,我们并不局限于单个嵌套聚合,我们可以嵌套它们中的多个,甚至在嵌套聚合中嵌套聚合。例如:

{
 "aggs": {
  "popular_tags" : {
   "terms" : {
    "field" : "copies"
   },
   "aggs" : {
    "years" : {
     "terms" : {
      "field" : "year"
     },
     "aggs" : {
      "available_by_year" : {
       "stats" : {
        "field" : "available"
       }
      }
     }
    },
    "available" : {
     "stats" : {
      "field" : "available"
     }
    }
   }
  }
 }
}

如您所见,如果您有足够的内存和 CPU 能力来处理非常复杂的聚合,那么可能性几乎是无限的。

Buckets ordering

还有一个关于嵌套聚合和聚合结果排序的特性。 Elasticsearch 可以使用嵌套聚合中的 值对父存储桶进行排序。例如,让我们看一下以下查询:

{
 "aggs": {
  "availability": {
   "terms": {
    "field": "copies",
    "order": { "numbers.avg": "desc" }
   },
   "aggs": {
    "numbers": { "stats" : {} }
   }
  }
 }
}

在前面的示例中,availability 聚合中的顺序基于 numbers 聚合的平均值。在这种情况下,符号 numbers.avg 是必需的,因为 stats 是一个多值聚合并提供多个信息,我们感兴趣平均值。如果是 sum 聚合,聚合的名称就足够了。

Pipeline aggregations


我们将讨论的最后一种聚合是管道聚合。到目前为止,我们已经了解了指标聚合和存储桶聚合。第一个返回指标,而第二个类型返回桶。指标和存储桶 聚合都基于返回的文档。管道聚合是不同的。他们处理其他聚合及其指标的输出,允许诸如移动平均 计算(https://en.wikipedia.org/wiki/Moving_average)。

Note

请记住,管道聚合是在 Elasticsearch 2.0 中引入的,并且被认为是实验性的。这意味着 API 将来可能会发生变化,从而破坏向后兼容性。

Available types

pipeline 聚合有两种类型。所谓的 parent 聚合系列适用于其他聚合的输出。他们能够生成新的存储桶或新的聚合以添加到现有的存储桶中。第二种叫做 兄弟聚合和这些聚合能够在同一级别上生成新的聚合。

Referencing other aggregations

由于它们的性质,管道聚合需要能够访问其他聚合的结果。我们可以通过使用指定格式定义的 buckets_path 属性来做到这一点。我们可以使用一些关键字来准确地告诉 Elasticsearch 我们对哪个聚合和指标感兴趣。 > 将聚合分开,而 . 字符将聚合与其指标分开。例如,my_sum.sum 表示我们采用名为 my_sum 的聚合的 sum 指标 。另一个例子是popular_tags>my_sum.sum,这意味着我们对名为 sum 指标感兴趣class="literal">my_sum,嵌套在 popular_tags 聚合中。除此之外,我们还可以使用名为 _count 的特殊路径。这可用于计算文档计数而不是指定指标的管道聚合。

Gaps in the data

我们的数据可能包含间隙——数据不存在的情况。对于此类用例,我们可以指定 gap_policy 属性并将其设置为 skipinsert_zerosskip 值告诉 Elasticsearch 忽略丢失的数据并从下一个可用值继续,而 insert_zeros 将丢失的值替换为零.

Pipeline aggregation types

我们将在本节中展示的大多数聚合与我们在 章节中关于指标和桶聚合的内容非常相似。因此,我们不会深入讨论它们。我们还想在更多数据中讨论新的、特定的管道聚合。

Min, max, sum, and average bucket aggregations

min_bucketmax_bucketsum_bucket,avg_bucket 聚合是兄弟聚合,类似于 它们返回到 min 的内容, maxsum、avg 聚合。然而,不是处理查询返回的数据,而是处理其他聚合的结果。

为了向您展示这个聚合如何工作的简单示例,让我们计算总和其他聚合返回的所有存储桶。执行此操作的查询如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    }
   }
  },
  "sum_copies" : {
   "sum_bucket" : {
    "buckets_path" : "periods_histogram>copies_per_100_years"
   }
  }
 }
}

如您所见,我们使用了 histogram 聚合,并包含了一个嵌套聚合,用于计算副本字段的总和。我们的 sum_bucket 同级聚合在主聚合之外使用,并使用 buckets_path 属性引用它。它告诉 Elasticsearch 我们有兴趣对 copies_per_100_years 聚合返回的指标值求和。 Elasticsearch 针对此查询返回的结果如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "periods_histogram" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1,
        "copies_per_100_years" : {
          "value" : 0.0
        }
      }, {
        "key" : 1900,
        "doc_count" : 3,
        "copies_per_100_years" : {
          "value" : 7.0
        }
      } ]
    },
    "sum_copies" : {
      "value" : 7.0
    }
  }
}

如您所见,Elasticsearch 在结果中添加了另一个桶,名为 sum_copies, 包含 我们的值 感兴趣。

Cumulative sum aggregation

cumulative_sum 聚合是 一个父管道聚合,它允许我们计算 直方图date_histogram 聚合。聚合的一个简单示例如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "cumulative_copies_sum" : {
     "cumulative_sum" : {
      "buckets_path" : "copies_per_100_years"
     }
    }
   }
  }
 }
}

因为这个聚合是一个 父管道聚合,所以它是在子聚合中定义的。返回的结果如下所示:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "periods_histogram" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1,
        "copies_per_100_years" : {
          "value" : 0.0
        },
        "cumulative_copies_sum" : {
          "value" : 0.0
        }
      }, {
        "key" : 1900,
        "doc_count" : 3,
        "copies_per_100_years" : {
          "value" : 7.0
        },
        "cumulative_copies_sum" : {
          "value" : 7.0
        }
      } ]
    }
  }
}

第一个 cumulative_copies_sum0 因为存储桶中定义的总和。第二个是所有之前的bucket和当前bucket的总和,也就是7。下一个将是所有先前的和下一个桶的总和。

Bucket selector aggregation

bucket_selector 聚合是 另一个兄弟父聚合。它允许使用脚本来决定是否应将存储桶保留在父多存储桶聚合中。例如,要只保留每个周期有多个副本的桶,我们可以运行以下查询(它需要将 script.inline 属性设置为 elasticsearch.yml 文件中的 "literal">on):

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "remove_empty_buckets" : {
     "bucket_selector" : {
      "buckets_path" : {
       "sum_copies" : "copies_per_100_years"
      },
      "script" : "sum_copies > 1"
     }
    }
   }
  }
 }
}

这里有两件重要的事情。第一个是 buckets_path 属性,它与我们目前使用的不同。现在它使用一个键和一个值。键用于引用脚本中的值。第二个重要的事情是 script 属性,它定义了 决定是否应该处理的存储桶的脚本保留。本案例 Elasticsearch 返回的结果如下:

{
  "took" : 330,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "periods_histogram" : {
      "buckets" : [ {
        "key" : 1900,
        "doc_count" : 3,
        "copies_per_100_years" : {
          "value" : 7.0
        }
      } ]
    }
  }
}

正如我们所见,copies_per_100_years 值等于 0 的存储桶已被删除。

Bucket script aggregation

bucket_script 聚合(兄弟父级)允许我们定义多个存储桶路径并在脚本中使用它们。 使用的指标必须是数值类型,返回值也需要是数值。下面是使用此聚合的示例(以下查询需要将 script.inline 属性设置为 on class="literal">elasticsearch.yml 文件):

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "stats_per_100_years" : {
     "stats" : {
      "field" : "copies"
     }
    },
    "example_bucket_script" : {
     "bucket_script" : {
      "buckets_path" : {
       "sum_copies" : "copies_per_100_years",
       "count" : "stats_per_100_years.count"
      },
      "script" : "sum_copies / count * 1000"
     }
    }
   }
  }
 }
}

这里有两件事。首先,我们在 buckets_path 属性中定义了两个条目。我们可以在 bucket_script 聚合中这样做。每个条目都是一个键和一个值。键是我们可以在脚本中使用的值的名称。第二个是我们感兴趣的聚合指标的路径。当然,script属性定义了返回值的脚本。

上述查询的返回结果如下:

{
  "took" : 5,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "periods_histogram" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1,
        "copies_per_100_years" : {
          "value" : 0.0
        },
        "stats_per_100_years" : {
          "count" : 1,
          "min" : 0.0,
          "max" : 0.0,
          "avg" : 0.0,
          "sum" : 0.0
        },
        "example_bucket_script" : {
          "value" : 0.0
        }
      }, {
        "key" : 1900,
        "doc_count" : 3,
        "copies_per_100_years" : {
          "value" : 7.0
        },
        "stats_per_100_years" : {
          "count" : 3,
          "min" : 0.0,
          "max" : 6.0,
          "avg" : 2.3333333333333335,
          "sum" : 7.0
        },
        "example_bucket_script" : {
          "value" : 2333.3333333333335
        }
      } ]
    }
  }
}

Serial differencing aggregation

serial_diff 聚合是一种父管道聚合,它实现了一种技术,其中时间序列数据(例如直方图或日期直方图)中的值在 不同的时间段。这种技术允许绘制时间段之间的数据变化,而不是绘制整个值。你知道一个城市的人口随着时间的推移而增长。如果我们使用以一天为周期的序列差分聚合,我们可以看到每天的增长。

要计算 serial_diff 聚合,我们需要父聚合,即 histogram date_histogram,我们需要给它提供buckets_path,它指向我们感兴趣的metric,以及lag(一个正的非零整数值),它告诉从当前桶中减去哪个前一个桶。我们可以省略 lag, 在这种情况下,Elasticsearch 会将其设置为 1

现在让我们看一个使用讨论过的聚合的简单查询:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "first_difference" : {
     "serial_diff" : {
      "buckets_path" : "copies_per_100_years",
      "lag" : 1
     }
    }
   }
  }
 }
}

前面的查询的响应如下所示:

{
  "took" : 68,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "periods_histogram" : {
      "buckets" : [ {
        "key" : 1800,
        "doc_count" : 1,
        "copies_per_100_years" : {
          "value" : 0.0
        }
      }, {
        "key" : 1900,
        "doc_count" : 3,
        "copies_per_100_years" : {
          "value" : 7.0
        },
        "first_difference" : {
          "value" : 7.0
        }
      } ]
    }
  }
}

如您所见,通过 第二个桶,我们得到了我们的聚合(我们也将在之后的每个桶中得到它)。计算的值为 7 因为 copies_per_100_years 的当前值为 7 并且前一个是 0。从 7 中减去 0 得到 7。

Derivative aggregation

derivative 聚合是父管道聚合的另一个示例。顾名思义,它计算一个 导数(https://en.wikipedia.org/wiki/Derivative) 来自直方图或日期的给定指标 直方图。我们唯一需要提供的是 buckets_path,它指向我们感兴趣的指标。使用此聚合的示例查询如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 100
   },
   "aggs" : {
    "copies_per_100_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "derivative_example" : {
     "derivative" : {
      "buckets_path" : "copies_per_100_years"
     }
    }
   }
  }
 }
}

Moving avg aggregation

我们要讨论的最后一个管道聚合是 moving_avg 。它计算 移动平均指标(https://en.wikipedia.org/wiki/Moving_average) 在父聚合的桶上(是的,这是一个 父管道聚合)。与前面讨论的少数聚合类似,它需要在父直方图或日期直方图聚合上运行。

在计算 移动平均线时,Elasticsearch 将采用窗口(由 window 属性指定并设置为 < code class="literal">5 默认),计算窗口中桶的平均值,将窗口移动一个桶,然后重复。当然,我们还需要提供 buckets_path,它指向应该计算移动平均值的指标。

使用此聚合的示例如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 10
   },
   "aggs" : {
    "copies_per_10_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "moving_avg_example" : {
     "moving_avg" : {
      "buckets_path" : "copies_per_10_years"
     }
    }
   }
  }
 }
}

我们将省略包含前面 查询的响应,因为它非常大。

Predicting future buckets

移动平均聚合的优点 是它支持预测;它可以尝试推断其拥有的数据并创建未来的存储桶。要强制聚合预测桶,我们只需将 predict 属性添加到任何移动平均聚合并将其设置为我们想要获得的预测数。例如,如果我们想在前面的查询中添加五个预测,我们将其更改为如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 10
   }, 
   "aggs" : {
    "copies_per_10_years" : {
     "sum" : {
      "field" : "copies"
     }
    },
    "moving_avg_example" : {
     "moving_avg" : {
      "buckets_path" : "copies_per_10_years",
      "predict" : 5
     }
    }
   }
  }
 }

如果您查看结果并将前一个查询返回的响应与带有预测的响应进行比较,您会注意到前一个查询中的最后一个桶结束在等于 1960 的关键属性上,而具有预测的查询在等于 2010 的关键属性上结束,这正是我们想要实现。

The models

默认情况下,Elasticsearch 使用最简单的模型来计算移动平均聚合,但我们可以通过指定 model 属性来控制它;此属性包含模型的名称和设置对象,我们可以使用它们来提供模型属性。

可能的模型有:simplelinearewmaholtholt_winters。详细讨论每个模型超出了本书的范围,因此如果您对不同 模型的详细信息感兴趣,请参阅官方的 Elasticsearch<关于移动平均线聚合的 id="id1087" class="indexterm"> 文档,可在 https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-pipeline-movavg-聚合.html

使用不同模型的示例查询如下所示:

{
 "aggs" : {
  "periods_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 10   },
   "aggs" : {
    "copies_per_10_years" : {
      "sum" : {
        "field" : "copies"
          }  },
  "moving_avg_example" : {
   "moving_avg" : {
    "buckets_path" : "copies_per_10_years",
    "model" : "holt", 
    "settings" : {
     "alpha" : 0.6,
     "beta" : 0.4
    }
   }
  }
   }
  }
 }
}

Summary


我们刚刚完成的章节是关于 Elasticsearch 中的数据分析:聚合引擎。我们了解了聚合是什么以及它们是如何工作的。我们使用了指标、存储桶和新引入的管道聚合,并了解了我们可以用它们做什么。

在下一章中,我们将超越全文搜索。我们将使用建议器来构建高效的自动完成功能并纠正用户的拼写错误。我们将看到什么是渗透以及如何在我们的应用程序中使用它。我们将使用 Elasticsearch 的地理空间功能,我们将学习如何有效地从 Elasticsearch 获取大量数据。