词项聚合

编辑

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

示例

resp = client.search(
    aggs={
        "genres": {
            "terms": {
                "field": "genre"
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    aggregations: {
      genres: {
        terms: {
          field: 'genre'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  aggs: {
    genres: {
      terms: {
        field: "genre",
      },
    },
  },
});
console.log(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 仅返回最靠前的词项;此数字是响应中不包含的所有桶的文档计数的总和

最靠前的桶的列表,靠前的含义由排序定义

field 可以是KeywordNumericipbooleanbinary

默认情况下,您不能在 text 字段上运行 terms 聚合。请改用 keyword 子字段。或者,您可以启用 fielddata 以在 text 字段上为该字段的已分析词项创建桶。启用 fielddata 会显著增加内存使用量。

大小

编辑

默认情况下,terms 聚合返回具有最多文档的前十个词项。使用 size 参数返回更多词项,最多达到search.max_buckets限制。

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

size 的较大值会使用更多内存来计算,并将整个聚合推向 max_buckets 限制。如果请求失败并出现关于 max_buckets 的消息,您就知道您设置的值过大了。

分片大小

编辑

为了获得更准确的结果,terms 聚合从每个分片中提取的词项多于前 size 个词项。它提取前 shard_size 个词项,默认值为 size * 1.5 + 10

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

您可以增加 shard_size 以更好地考虑这些差异很大的文档计数,并提高选择靠前词项的准确性。增加 shard_size 比增加 size 更便宜。但是,它仍然会通过网络传输更多字节,并在协调节点上等待内存。

此指导仅在您使用 terms 聚合的默认排序 order 时适用。如果您按降序文档计数以外的任何内容排序,请参阅排序

shard_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(
    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)
const response = await client.search({
  aggs: {
    products: {
      terms: {
        field: "product",
        size: 5,
        show_term_doc_count_error: true,
      },
    },
  },
});
console.log(response);
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(
    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)
const response = await client.search({
  aggs: {
    genres: {
      terms: {
        field: "genre",
        order: {
          _key: "asc",
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "_key": "asc" }
      }
    }
  }
}

按子聚合排序

编辑

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

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

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

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    genres: {
      terms: {
        field: "genre",
        order: {
          max_play_count: "desc",
        },
      },
      aggs: {
        max_play_count: {
          max: {
            field: "play_count",
          },
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "max_play_count": "desc" }
      },
      "aggs": {
        "max_play_count": { "max": { "field": "play_count" } }
      }
    }
  }
}

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    genres: {
      terms: {
        field: "genre",
        order: {
          "playback_stats.max": "desc",
        },
      },
      aggs: {
        playback_stats: {
          stats: {
            field: "play_count",
          },
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "playback_stats.max": "desc" }
      },
      "aggs": {
        "playback_stats": { "stats": { "field": "play_count" } }
      }
    }
  }
}

管道聚合不能用于排序

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

还可以根据层级结构中“更深层”的聚合结果对桶进行排序。只要聚合路径是单桶类型,就可以支持此操作,其中路径中的最后一个聚合可以是单桶类型,也可以是指标类型。如果是单桶类型,则排序将由桶中的文档数(即 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(
    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)
const response = await client.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",
              },
            },
          },
        },
      },
    },
  },
});
console.log(response);
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(
    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)
const response = await client.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",
              },
            },
          },
        },
      },
    },
  },
});
console.log(response);
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 降序排序。

如果两个桶的所有排序条件值都相同,则桶的词项值将用作升序字母顺序的并列关系打破者,以防止桶的排序不确定。

按计数升序排序

编辑

按升序文档 _count 对词项进行排序会产生一个 Elasticsearch 无法准确报告的无界错误。因此,我们强烈建议不要使用 "order": { "_count": "asc" },如下例所示

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    genres: {
      terms: {
        field: "genre",
        order: {
          _count: "asc",
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "genres": {
      "terms": {
        "field": "genre",
        "order": { "_count": "asc" }
      }
    }
  }
}

最小文档计数

编辑

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    tags: {
      terms: {
        field: "tags",
        min_doc_count: 10,
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "min_doc_count": 10
      }
    }
  }
}

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

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

shard_min_doc_count

编辑

参数 shard_min_doc_count 调节分片对于词项是否应相对于 min_doc_count 实际添加到候选列表的*确定性*。仅当词项在集合中的本地分片频率高于 shard_min_doc_count 时,才会考虑这些词项。如果您的字典包含许多低频词项,并且您对这些词项不感兴趣(例如拼写错误),那么您可以设置 shard_min_doc_count 参数以在分片级别过滤掉候选词项,这些词项在合并本地计数后很可能无法达到要求的 min_doc_countshard_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

脚本

编辑

如果文档中的数据与您想要聚合的内容不完全匹配,请使用运行时字段。例如,如果需要将“选集”归入一个特殊类别,那么您可以运行此命令

resp = client.search(
    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
const response = await client.search({
  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",
      },
    },
  },
});
console.log(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 字段。

过滤值

编辑

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

使用正则表达式过滤值

编辑
resp = client.search(
    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)
const response = await client.search({
  aggs: {
    tags: {
      terms: {
        field: "tags",
        include: ".*sport.*",
        exclude: "water_.*",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "include": ".*sport.*",
        "exclude": "water_.*"
      }
    }
  }
}

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

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

使用确切值过滤值

编辑

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    JapaneseCars: {
      terms: {
        field: "make",
        include: ["mazda", "honda"],
      },
    },
    ActiveCarManufacturers: {
      terms: {
        field: "make",
        exclude: ["rover", "jensen"],
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "JapaneseCars": {
      "terms": {
        "field": "make",
        "include": [ "mazda", "honda" ]
      }
    },
    "ActiveCarManufacturers": {
      "terms": {
        "field": "make",
        "exclude": [ "rover", "jensen" ]
      }
    }
  }
}

使用分区过滤值

编辑

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

$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(
    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({
  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 均匀地组织为 20 个分区(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。如果请求成功,但日期排序的测试响应中的最后一个帐户 ID 仍然是我们可能想要过期的帐户,那么我们可能会错过感兴趣的帐户,并且将我们的数字设置得太低。我们必须

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

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

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

多字段词项聚合

编辑

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

您可以使用三种方法在多个字段上执行 terms 聚合

脚本
使用脚本从多个字段检索词项。这将禁用全局序号优化,并且比从单个字段收集词项要慢,但它使您可以灵活地在搜索时实现此选项。
copy_to 字段
如果您事先知道要从两个或多个字段中收集词项,则在映射中使用 copy_to 在索引时创建一个新的专用字段,该字段包含两个字段的值。您可以聚合此单个字段,这将受益于全局序号优化。
multi_terms 聚合
使用 multi_terms 聚合将多个字段的词条组合成一个复合键。 这也会禁用全局序号,并且比从单个字段收集词条要慢。 它比使用脚本更快,但灵活性较差。

收集模式

编辑

延迟计算子聚合

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    actors: {
      terms: {
        field: "actors",
        size: 10,
      },
      aggs: {
        costars: {
          terms: {
            field: "actors",
            size: 5,
          },
        },
      },
    },
  },
});
console.log(response);
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(
    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)
const response = await client.search({
  aggs: {
    actors: {
      terms: {
        field: "actors",
        size: 10,
        collect_mode: "breadth_first",
      },
      aggs: {
        costars: {
          terms: {
            field: "actors",
            size: 5,
          },
        },
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "actors": {
      "terms": {
        "field": "actors",
        "size": 10,
        "collect_mode": "breadth_first" 
      },
      "aggs": {
        "costars": {
          "terms": {
            "field": "actors",
            "size": 5
          }
        }
      }
    }
  }
}

可能的值为 breadth_firstdepth_first

当使用 breadth_first 模式时,属于最上层桶的文档集会被缓存以供后续重放,因此这样做会产生内存开销,该开销与匹配的文档数量呈线性关系。 请注意,当使用 breadth_first 设置时,仍然可以使用 order 参数来引用来自子聚合的数据 - 父聚合知道这个子聚合需要在任何其他子聚合之前调用。

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

执行提示

编辑

词条聚合可以通过不同的机制执行

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

Elasticsearch 尝试使用合理的默认值,因此通常不需要配置此项。

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

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    tags: {
      terms: {
        field: "tags",
        execution_hint: "map",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "execution_hint": "map" 
      }
    }
  }
}

可能的值为 mapglobal_ordinals

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

缺失值

编辑

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

resp = client.search(
    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)
const response = await client.search({
  aggs: {
    tags: {
      terms: {
        field: "tags",
        missing: "N/A",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "tags": {
      "terms": {
        "field": "tags",
        "missing": "N/A" 
      }
    }
  }
}

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

混合字段类型

编辑

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

故障排除

编辑

尝试格式化字节失败

编辑

当在多个索引上运行词条聚合(或其他聚合,但实际上通常是词条聚合)时,您可能会收到以 “Failed trying to format bytes…​” 开头的错误。 这通常是由于两个索引对于正在聚合的字段没有相同的映射类型引起的。

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

resp = client.search(
    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
const response = await client.search({
  aggs: {
    ip_addresses: {
      terms: {
        field: "destination_ip",
        missing: "0.0.0.0",
        value_type: "ip",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "aggs": {
    "ip_addresses": {
      "terms": {
        "field": "destination_ip",
        "missing": "0.0.0.0",
        "value_type": "ip"
      }
    }
  }
}