术语聚合编辑

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

示例

resp = client.search(
    body={"aggs": {"genres": {"terms": {"field": "genre"}}}},
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre'
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggs": {
    "genres": {
      "terms": { "field": "genre" }
    }
  }
}

响应

{
  ...
  "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 的含义由 顺序 定义

field 可以是 关键字数字ipbooleanbinary

默认情况下,您无法对 text 字段运行 terms 聚合。请改用 keyword 子字段。或者,您可以在 text 字段上启用 fielddata,以创建该字段的 分析 术语的桶。启用 fielddata 会显着增加内存使用量。

大小编辑

默认情况下,terms 聚合将返回文档数量最多的前十个术语。使用 size 参数返回更多术语,最多可达 search.max_buckets 限制。

如果您的数据包含 100 或 1000 个唯一术语,您可以增加 terms 聚合的 size 以返回所有术语。如果您有更多唯一术语并且需要所有术语,请改用 复合聚合

较大的 size 值会使用更多内存来计算,并将整个聚合推向 max_buckets 限制。如果请求因 max_buckets 消息而失败,则表明您设置的 size 过大。

分片大小编辑

为了获得更准确的结果,terms 聚合将从每个分片中获取超过前 size 个术语。它将获取前 shard_size 个术语,默认值为 size * 1.5 + 10

这样做是为了处理这种情况:一个术语在一个分片上具有许多文档,但在所有其他分片上都低于 size 阈值。如果每个分片仅返回 size 个术语,则聚合将返回该术语的部分文档计数。因此,terms 返回更多术语,以尝试捕获丢失的术语。这很有帮助,但仍然有可能返回术语的部分文档计数。它只需要一个术语,其每个分片的文档计数差异较大。

您可以增加 shard_size 以更好地考虑这些差异很大的文档计数,并提高对前几个术语选择的准确性。增加 shard_size 比增加 size 要便宜得多。但是,它仍然需要在协调节点上占用更多字节,并在内存中等待。

此指南仅适用于您使用 terms 聚合的默认排序 order 的情况。如果您按文档计数以外的任何其他方式排序,请参阅 顺序

shard_size 不能小于 size(因为这样做没有意义)。当它小于 size 时,Elasticsearch 将覆盖它并将其重置为等于 size

文档计数误差编辑

即使使用较大的 shard_size 值,terms 聚合的 doc_count 值也可能近似。因此,terms 聚合上的任何子聚合也可能近似。

sum_other_doc_count 是未进入前 size 个术语的文档数量。如果此值大于 0,则可以确定 terms 聚合必须丢弃一些桶,原因是它们不适合协调节点上的 size 或不适合数据节点上的 shard_size

每个桶的文档计数误差编辑

如果将 show_term_doc_count_error 参数设置为 true,则 terms 聚合将包含 doc_count_error_upper_bound,它是每个分片返回的 doc_count 误差的上限。它是每个分片上未适合 shard_size 的最大桶的大小之和。

更具体地说,假设在一个分片上有一个非常大的桶,而在所有其他分片上都略低于 shard_size。在这种情况下,terms 聚合将返回该桶,因为它很大,但它将丢失许多分片上的文档数据,因为这些分片上的术语低于 shard_size 阈值。doc_count_error_upper_bound 是这些丢失文档的最大数量。

resp = client.search(
    body={
        "aggs": {
            "products": {
                "terms": {
                    "field": "product",
                    "size": 5,
                    "show_term_doc_count_error": True,
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      products: {
        terms: {
          field: 'product',
          size: 5,
          show_term_doc_count_error: true
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "products": {
	      "terms": {
	        "field": "product",
	        "size": 5,
	        "show_term_doc_count_error": true
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "products": {
      "terms": {
        "field": "product",
        "size": 5,
        "show_term_doc_count_error": true
      }
    }
  }
}

这些误差只能以这种方式计算,前提是术语按文档计数降序排序。当聚合按术语值本身(升序或降序)排序时,文档计数中没有误差,因为如果一个分片没有返回另一个分片结果中出现的特定术语,则它一定没有在索引中包含该术语。当聚合按子聚合排序或按文档计数升序排序时,无法确定文档计数中的误差,并将其设置为 -1 以指示这一点。

顺序编辑

默认情况下,terms 聚合按文档 _count 降序排序术语。这将产生一个有界 文档计数 误差,Elasticsearch 可以报告该误差。

您可以使用 order 参数指定不同的排序顺序,但我们不建议这样做。创建术语排序以返回错误结果非常容易,而且当您这样做时,并不容易发现。仅在谨慎的情况下更改此设置。

尤其要避免使用 "order": { "_count": "asc" }。如果您需要查找罕见术语,请改用 rare_terms 聚合。由于 terms 聚合 从分片获取术语 的方式,按文档计数升序排序通常会产生不准确的结果。

按术语值排序编辑

在这种情况下,桶按实际术语值排序,例如关键字的词典顺序或数字的数值顺序。这种排序在升序和降序方向上都是安全的,并且会产生准确的结果。

按术语的字母顺序(升序)对桶进行排序的示例

resp = client.search(
    body={
        "aggs": {
            "genres": {
                "terms": {"field": "genre", "order": {"_key": "asc"}}
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre',
          order: {
            _key: 'asc'
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "genres": {
	      "terms": {
	        "field": "genre",
	        "order": {
	          "_key": "asc"
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "_key": "asc" }
      }
    }
  }
}

按子聚合排序编辑

由于 terms 聚合 从分片获取结果 的方式,按子聚合排序通常会产生不正确的排序。

子聚合排序在两种情况下是安全的,并且会返回正确的结果:按最大值降序排序,或按最小值升序排序。这些方法有效,因为它们与子聚合的行为一致。也就是说,如果您正在寻找最大的最大值或最小的最小值,则全局答案(来自组合的分片)必须包含在某个本地分片答案中。相反,最小的最大值和最大的最小值将无法准确计算。

还要注意,在这些情况下,排序是正确的,但文档计数和非排序子聚合可能仍然存在误差(并且 Elasticsearch 不会计算这些误差的界限)。

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

resp = client.search(
    body={
        "aggs": {
            "genres": {
                "terms": {
                    "field": "genre",
                    "order": {"max_play_count": "desc"},
                },
                "aggs": {
                    "max_play_count": {"max": {"field": "play_count"}}
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre',
          order: {
            max_play_count: 'desc'
          }
        },
        aggregations: {
          max_play_count: {
            max: {
              field: 'play_count'
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "genres": {
	      "terms": {
	        "field": "genre",
	        "order": {
	          "max_play_count": "desc"
	        }
	      },
	      "aggs": {
	        "max_play_count": {
	          "max": {
	            "field": "play_count"
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "max_play_count": "desc" }
      },
      "aggs": {
        "max_play_count": { "max": { "field": "play_count" } }
      }
    }
  }
}

按多值指标子聚合(由聚合名称标识)对桶进行排序

resp = client.search(
    body={
        "aggs": {
            "genres": {
                "terms": {
                    "field": "genre",
                    "order": {"playback_stats.max": "desc"},
                },
                "aggs": {
                    "playback_stats": {"stats": {"field": "play_count"}}
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre',
          order: {
            'playback_stats.max' => 'desc'
          }
        },
        aggregations: {
          playback_stats: {
            stats: {
              field: 'play_count'
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "genres": {
	      "terms": {
	        "field": "genre",
	        "order": {
	          "playback_stats.max": "desc"
	        }
	      },
	      "aggs": {
	        "playback_stats": {
	          "stats": {
	            "field": "play_count"
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "playback_stats.max": "desc" }
      },
      "aggs": {
        "playback_stats": { "stats": { "field": "play_count" } }
      }
    }
  }
}

管道聚合不能用于排序

管道聚合 在所有其他聚合完成后,在减少阶段运行。因此,它们不能用于排序。

还可以根据层次结构中“更深”的聚合对桶进行排序。只要聚合路径是单桶类型,则支持此操作,其中路径中的最后一个聚合可以是单桶类型或指标类型。如果它是单桶类型,则顺序将由桶中的文档数量(即 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> ] ;
resp = client.search(
    body={
        "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"}
                            }
                        },
                    }
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      countries: {
        terms: {
          field: 'artist.country',
          order: {
            "rock>playback_stats.avg": 'desc'
          }
        },
        aggregations: {
          rock: {
            filter: {
              term: {
                genre: 'rock'
              }
            },
            aggregations: {
              playback_stats: {
                stats: {
                  field: 'play_count'
                }
              }
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "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"
	              }
	            }
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
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" } }
          }
        }
      }
    }
  }
}

以上将根据摇滚歌曲的平均播放次数对艺术家的国家/地区桶进行排序。

可以使用多个条件对桶进行排序,方法是提供排序条件数组,例如以下数组

resp = client.search(
    body={
        "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"}
                            }
                        },
                    }
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      countries: {
        terms: {
          field: 'artist.country',
          order: [
            {
              "rock>playback_stats.avg": 'desc'
            },
            {
              _count: 'desc'
            }
          ]
        },
        aggregations: {
          rock: {
            filter: {
              term: {
                genre: 'rock'
              }
            },
            aggregations: {
              playback_stats: {
                stats: {
                  field: 'play_count'
                }
              }
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "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"
	              }
	            }
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
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" } }
          }
        }
      }
    }
  }
}

以上将根据摇滚歌曲的平均播放次数对艺术家国家桶进行排序,然后按其doc_count降序排序。

如果两个桶在所有排序标准上都具有相同的值,则使用桶的术语值作为升序字母顺序的决胜局,以防止桶的非确定性排序。

按计数升序排序edit

按文档_count升序排序术语会导致 Elasticsearch 无法准确报告的无界错误。因此,我们强烈建议不要使用以下示例中所示的"order": { "_count": "asc" }

resp = client.search(
    body={
        "aggs": {
            "genres": {
                "terms": {"field": "genre", "order": {"_count": "asc"}}
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre',
          order: {
            _count: 'asc'
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "genres": {
	      "terms": {
	        "field": "genre",
	        "order": {
	          "_count": "asc"
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "_count": "asc" }
      }
    }
  }
}

最小文档计数edit

可以使用min_doc_count选项仅返回匹配超过配置命中次数的术语。

resp = client.search(
    body={
        "aggs": {"tags": {"terms": {"field": "tags", "min_doc_count": 10}}}
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      tags: {
        terms: {
          field: 'tags',
          min_doc_count: 10
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "tags": {
	      "terms": {
	        "field": "tags",
	        "min_doc_count": 10
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "min_doc_count": 10
      }
    }
  }
}

以上聚合将仅返回在 10 次或更多命中中找到的标签。默认值为1

术语在分片级别上收集和排序,并在第二步中与从其他分片收集的术语合并。但是,分片没有关于全局文档计数的信息。是否将术语添加到候选列表的决定仅取决于使用本地分片频率在分片上计算的顺序。只有在合并所有分片的本地术语统计信息后,才会应用min_doc_count标准。从某种意义上说,添加术语作为候选的决定是在没有完全确定术语是否真的会达到所需的min_doc_count的情况下做出的。如果低频术语填充了候选列表,这可能会导致最终结果中缺少许多(全局)高频术语。为了避免这种情况,可以增加shard_size参数以允许分片上更多候选术语。但是,这会增加内存消耗和网络流量。

shard_min_doc_countedit

参数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_size来弥补丢失的桶。将shard_min_doc_count设置得太高会导致术语在分片级别被过滤掉。此值应设置得远低于min_doc_count/#shards

脚本edit

如果文档中的数据与您要聚合的数据不完全匹配,请使用运行时字段。例如,如果“选集”需要在特殊类别中,那么您可以运行以下代码

resp = client.search(
    body={
        "size": 0,
        "runtime_mappings": {
            "normalized_genre": {
                "type": "keyword",
                "script": "\n        String genre = doc['genre'].value;\n        if (doc['product'].value.startsWith('Anthology')) {\n          emit(genre + ' anthology');\n        } else {\n          emit(genre);\n        }\n      ",
            }
        },
        "aggs": {"genres": {"terms": {"field": "normalized_genre"}}},
    },
)
print(resp)
response = client.search(
  body: {
    size: 0,
    runtime_mappings: {
      normalized_genre: {
        type: 'keyword',
        script: "\n        String genre = doc['genre'].value;\n        if (doc['product'].value.startsWith('Anthology')) {\n          emit(genre + ' anthology');\n        } else {\n          emit(genre);\n        }\n      "
      }
    },
    aggregations: {
      genres: {
        terms: {
          field: 'normalized_genre'
        }
      }
    }
  }
)
puts response
GET /_search
{
  "size": 0,
  "runtime_mappings": {
    "normalized_genre": {
      "type": "keyword",
      "script": """
        String genre = doc['genre'].value;
        if (doc['product'].value.startsWith('Anthology')) {
          emit(genre + ' anthology');
        } else {
          emit(genre);
        }
      """
    }
  },
  "aggs": {
    "genres": {
      "terms": {
        "field": "normalized_genre"
      }
    }
  }
}

这将看起来像

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

这有点慢,因为运行时字段必须访问两个字段而不是一个字段,并且因为有一些针对非运行时keyword字段的优化,我们必须放弃这些优化才能使用运行时keyword字段。如果您需要速度,可以索引normalized_genre字段。

过滤值edit

可以过滤将为其创建桶的值。这可以通过使用基于正则表达式字符串或精确值数组的includeexclude参数来完成。此外,include子句可以使用partition表达式进行过滤。

使用正则表达式过滤值edit

resp = client.search(
    body={
        "aggs": {
            "tags": {
                "terms": {
                    "field": "tags",
                    "include": ".*sport.*",
                    "exclude": "water_.*",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      tags: {
        terms: {
          field: 'tags',
          include: '.*sport.*',
          exclude: 'water_.*'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "tags": {
	      "terms": {
	        "field": "tags",
	        "include": ".*sport.*",
	        "exclude": "water_.*"
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "include": ".*sport.*",
        "exclude": "water_.*"
      }
    }
  }
}

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

语法与正则表达式查询相同。

使用精确值过滤值edit

对于基于精确值的匹配,includeexclude参数可以简单地接受一个字符串数组,该数组表示索引中找到的术语。

resp = client.search(
    body={
        "aggs": {
            "JapaneseCars": {
                "terms": {"field": "make", "include": ["mazda", "honda"]}
            },
            "ActiveCarManufacturers": {
                "terms": {"field": "make", "exclude": ["rover", "jensen"]}
            },
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      "JapaneseCars": {
        terms: {
          field: 'make',
          include: [
            'mazda',
            'honda'
          ]
        }
      },
      "ActiveCarManufacturers": {
        terms: {
          field: 'make',
          exclude: [
            'rover',
            'jensen'
          ]
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "JapaneseCars": {
	      "terms": {
	        "field": "make",
	        "include": [
	          "mazda",
	          "honda"
	        ]
	      }
	    },
	    "ActiveCarManufacturers": {
	      "terms": {
	        "field": "make",
	        "exclude": [
	          "rover",
	          "jensen"
	        ]
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "JapaneseCars": {
      "terms": {
        "field": "make",
        "include": [ "mazda", "honda" ]
      }
    },
    "ActiveCarManufacturers": {
      "terms": {
        "field": "make",
        "exclude": [ "rover", "jensen" ]
      }
    }
  }
}

使用分区过滤值edit

有时,单个请求/响应对中要处理的唯一术语太多,因此将分析分解成多个请求可能很有用。这可以通过在查询时将字段的值分组到多个分区中,并在每个请求中仅处理一个分区来实现。考虑以下请求,该请求正在查找最近没有登录任何访问权限的帐户

$params = [
    'body' => [
        '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',
                        ],
                    ],
                ],
            ],
        ],
    ],
];
$response = $client->search($params);
resp = client.search(
    body={
        "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"}}},
            }
        },
    },
)
print(resp)
response = client.search(
  body: {
    size: 0,
    aggregations: {
      expired_sessions: {
        terms: {
          field: 'account_id',
          include: {
            partition: 0,
            num_partitions: 20
          },
          size: 10_000,
          order: {
            last_access: 'asc'
          }
        },
        aggregations: {
          last_access: {
            max: {
              field: 'access_date'
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "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"
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
const response = await client.search({
  body: {
    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'
            }
          }
        }
      }
    }
  }
})
console.log(response)
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_id 均匀地组织到二十个分区(0 到 19)中,而此请求中的partition设置仅过滤掉属于分区 0 的 account_id。后续请求应请求分区 1,然后是分区 2 等,以完成过期帐户分析。

请注意,返回的结果数量的size设置需要与num_partitions进行调整。对于此特定的帐户过期示例,平衡sizenum_partitions的值的过程如下

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

如果我们有断路器错误,那么我们正在尝试在一个请求中做太多事情,必须增加num_partitions。如果请求成功,但日期排序的测试响应中的最后一个 account ID 仍然是我们可能想要过期的帐户,那么我们可能错过了我们感兴趣的帐户,并且设置的数字太低。我们必须要么

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

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

分区不能与exclude参数一起使用。

多字段术语聚合edit

terms聚合不支持从同一文档中的多个字段收集术语。原因是terms聚合不会收集字符串术语值本身,而是使用全局序数来生成字段中所有唯一值的列表。全局序数会导致重要的性能提升,这在跨多个字段时是不可能的。

您可以使用三种方法来执行跨多个字段的terms聚合

脚本
使用脚本从多个字段检索术语。这会禁用全局序数优化,并且比从单个字段收集术语要慢,但它使您能够在搜索时实现此选项。
copy_to字段
如果您提前知道要从两个或多个字段收集术语,那么在您的映射中使用copy_to在索引时创建一个新的专用字段,该字段包含来自两个字段的值。您可以对单个字段进行聚合,这将受益于全局序数优化。
multi_terms聚合
使用multi_terms聚合将来自多个字段的术语组合成一个复合键。这也将禁用全局序数,并且比从单个字段收集术语要慢。它比使用脚本更快,但灵活性更低。

收集模式edit

延迟子聚合的计算

对于具有许多唯一术语和少量所需结果的字段,在顶级父级聚合被修剪后延迟子聚合的计算可能更有效。通常,聚合树的所有分支都在一次深度优先遍历中展开,然后才进行任何修剪。在某些情况下,这可能非常浪费,并且可能会遇到内存限制。一个示例问题场景是查询电影数据库以查找 10 位最受欢迎的演员及其 5 位最常见的搭档

resp = client.search(
    body={
        "aggs": {
            "actors": {
                "terms": {"field": "actors", "size": 10},
                "aggs": {
                    "costars": {"terms": {"field": "actors", "size": 5}}
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      actors: {
        terms: {
          field: 'actors',
          size: 10
        },
        aggregations: {
          costars: {
            terms: {
              field: 'actors',
              size: 5
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "actors": {
	      "terms": {
	        "field": "actors",
	        "size": 10
	      },
	      "aggs": {
	        "costars": {
	          "terms": {
	            "field": "actors",
	            "size": 5
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
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 是基数大于请求大小或基数未知(例如数字字段或脚本)的字段的默认模式。可以覆盖默认启发式方法,并在请求中直接提供收集模式。

resp = client.search(
    body={
        "aggs": {
            "actors": {
                "terms": {
                    "field": "actors",
                    "size": 10,
                    "collect_mode": "breadth_first",
                },
                "aggs": {
                    "costars": {"terms": {"field": "actors", "size": 5}}
                },
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      actors: {
        terms: {
          field: 'actors',
          size: 10,
          collect_mode: 'breadth_first'
        },
        aggregations: {
          costars: {
            terms: {
              field: 'actors',
              size: 5
            }
          }
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "actors": {
	      "terms": {
	        "field": "actors",
	        "size": 10,
	        "collect_mode": "breadth_first"
	      },
	      "aggs": {
	        "costars": {
	          "terms": {
	            "field": "actors",
	            "size": 5
	          }
	        }
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
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 模式时,属于最上层桶的文档集会被缓存以供后续重放,因此这样做会产生与匹配文档数量成线性关系的内存开销。请注意,order 参数仍然可以用于引用使用 breadth_first 设置的子聚合中的数据 - 父聚合理解,在调用任何其他子聚合之前,需要先调用此子聚合。

嵌套聚合(如 top_hits)需要访问使用 breadth_first 收集模式的聚合下的评分信息,需要在第二次传递时重放查询,但仅针对属于顶级桶的文档。

执行提示edit

术语聚合可以通过不同的机制执行。

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

Elasticsearch 试图提供合理的默认值,因此通常不需要配置此项。

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

map 仅在很少的文档匹配查询时才应考虑。否则,基于序数的执行模式会快得多。默认情况下,map 仅在对脚本运行聚合时使用,因为它们没有序数。

resp = client.search(
    body={
        "aggs": {
            "tags": {"terms": {"field": "tags", "execution_hint": "map"}}
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      tags: {
        terms: {
          field: 'tags',
          execution_hint: 'map'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "tags": {
	      "terms": {
	        "field": "tags",
	        "execution_hint": "map"
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "execution_hint": "map" 
      }
    }
  }
}

可能的值是 mapglobal_ordinals

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

缺失值edit

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

resp = client.search(
    body={
        "aggs": {"tags": {"terms": {"field": "tags", "missing": "N/A"}}}
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      tags: {
        terms: {
          field: 'tags',
          missing: 'N/A'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "tags": {
	      "terms": {
	        "field": "tags",
	        "missing": "N/A"
	      }
	    }
	  }
	}`)),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "missing": "N/A" 
      }
    }
  }
}

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

混合字段类型edit

在多个索引上聚合时,聚合字段的类型可能在所有索引中都不相同。某些类型彼此兼容 (integerlongfloatdouble),但当类型是十进制数和非十进制数的混合时,术语聚合将把非十进制数提升为十进制数。这会导致桶值精度损失。

故障排除edit

尝试格式化字节失败edit

在多个索引上运行术语聚合(或其他聚合,但在实践中通常是术语聚合)时,您可能会收到以“尝试格式化字节失败……”开头的错误。这通常是由于两个索引对要聚合的字段没有相同的映射类型造成的。

使用显式 value_type 虽然最好纠正映射,但如果字段在其中一个索引中未映射,您可以解决此问题。设置 value_type 参数可以通过将未映射的字段强制转换为正确的类型来解决此问题。

resp = client.search(
    body={
        "aggs": {
            "ip_addresses": {
                "terms": {
                    "field": "destination_ip",
                    "missing": "0.0.0.0",
                    "value_type": "ip",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      ip_addresses: {
        terms: {
          field: 'destination_ip',
          missing: '0.0.0.0',
          value_type: 'ip'
        }
      }
    }
  }
)
puts response
GET /_search
{
  "aggs": {
    "ip_addresses": {
      "terms": {
        "field": "destination_ip",
        "missing": "0.0.0.0",
        "value_type": "ip"
      }
    }
  }
}