适用版本: 6.8-8.11
1. 错误异常的基本描述 #
在使用 Elasticsearch 的 significant_terms 聚合时,如果为 percentage 显著性启发式(significance heuristic)提供了非空的 JSON 对象,Elasticsearch 会在解析 DSL 阶段直接抛出异常,错误信息如下:
failed to parse [percentage] significance heuristic. expected an empty object; but got [TOKEN] instead
这是一个 解析阶段(Parse Exception) 错误,而非运行时聚合错误。请求尚未进入实际聚合执行阶段,就在 DSL 解析阶段被拒绝,因此不会返回任何聚合结果。
常见现象 #
- 调用聚合接口时直接返回
400 Bad Request,错误类型为parsing_exception或x_content_parse_exception。 - Kibana、cURL 或客户端 SDK 均返回相同错误,且错误栈中会出现
SignificanceHeuristicParser、PercentageScore等关键字。 - 如果 DSL 由代码动态生成,问题往往隐蔽在模板逻辑中,不易通过肉眼直接发现。
- 该错误不会触发集群不稳定,但会导致相关聚合请求持续失败,影响依赖显著性聚合的上层业务功能。
典型报错与异常栈 #
{
"error": {
"root_cause": [
{
"type": "parsing_exception",
"reason": "failed to parse [percentage] significance heuristic. expected an empty object; but got [START_OBJECT] instead"
}
],
"type": "parsing_exception",
"reason": "failed to parse [percentage] significance heuristic. expected an empty object; but got [START_OBJECT] instead"
},
"status": 400
}
2. 为什么会发生这个错误 #
percentage 是一种内置的显著性启发式实现,其设计意图非常简单:直接返回文档频率的百分比得分,不参与任何自定义参数配置。在 Elasticsearch 源码中,PercentageScore 对应的解析器在进入该节点后,仅期望读取到 END_OBJECT 标记,即 percentage 字段的值必须是一个空对象 {}。
如果解析器读到的不是 END_OBJECT,而是任何其他 token(如 START_OBJECT、FIELD_NAME、VALUE_STRING 等),就会抛出上述异常。
出现此问题的常见原因包括:
- 误以为
percentage支持参数:将其他启发式(如mutual_information、chi_square、jlh)的参数结构错误地套用在percentage上。 - DSL 模板过度复用:在动态生成聚合 DSL 的代码中,对不同启发式类型使用了同一套参数渲染逻辑,导致额外字段被注入到
percentage对象中。 - 从其他启发式复制粘贴时未清理字段:例如从
mutual_information的配置复制过来后,忘记删除include_negatives、background_is_superset等字段。 - JSON 生成逻辑缺陷:在应用层代码中,默认给所有启发式对象附加了通用字段(如
field、size等),而percentage无法容纳这些内容。 - 版本差异导致的误解:某些启发式类型在不同版本中参数行为不同,升级后未同步调整 DSL 生成逻辑。
3. 如何排查这个异常 #
建议按以下步骤定位问题:
- 提取完整 DSL:从报错请求的 body 中取出完整的聚合 DSL,重点关注
significant_terms下的significance_heuristic字段。 - 检查
percentage节点内容:确认percentage的值是否为{},以及是否包含任何额外字段、嵌套对象或数组。 - 回溯 DSL 生成逻辑:如果 DSL 由代码或模板生成,检查生成
significance_heuristic的代码片段,确认是否有通用参数被无条件注入。 - 对比其他启发式配置:将当前 DSL 与
mutual_information、chi_square等支持参数的启发式配置对比,确认是否存在结构混淆。 - 在简化环境中复现:剥离无关聚合条件,仅保留
significant_terms+percentage的最小 DSL,验证是否仍然报错。
排查时需要注意的问题 #
- 不要只关注错误本身,还要检查整个
significant_terms聚合的结构是否完整,因为外层错误有时会掩盖更深层的问题。 - 如果使用了 Elasticsearch 客户端库(如 Java High Level REST Client、Python elasticsearch-py 等),确认库版本与集群版本是否匹配,避免客户端自动注入不兼容的参数。
- 当 DSL 通过模板引擎(如 Mustache、Freemarker、Jinja2)生成时,重点检查模板条件分支是否覆盖了
percentage这个无参类型。
4. 如何解决这个错误 #
方案一:将 percentage 节点恢复为空对象
#
这是最直接、也是最常见的修复方式。确保 percentage 的值是一个空的 JSON 对象,不包含任何字段。
错误示例:
{
"significant_terms": {
"field": "user_id",
"significance_heuristic": {
"percentage": {
"include_negatives": true
}
}
}
}
正确示例:
{
"significant_terms": {
"field": "user_id",
"significance_heuristic": {
"percentage": {}
}
}
}
方案二:如果需要额外参数,改用支持参数的启发式 #
如果业务上确实需要配置额外参数(如控制背景集行为、是否包含负值等),应放弃 percentage,改用支持这些参数的启发式类型。
{
"significant_terms": {
"field": "user_id",
"significance_heuristic": {
"mutual_information": {
"include_negatives": true
}
}
}
}
常见可替代的启发式类型包括:
mutual_information:支持include_negatives参数chi_square:支持include_negatives参数jlh:无参数,与percentage类似,但算法不同script:支持自定义脚本,灵活性最高
方案三:修复 DSL 模板生成逻辑 #
如果问题源于代码或模板,需要在生成层做针对性修复:
# 修复前:所有启发式共用同一参数结构
def build_heuristic(heuristic_type, params):
return {heuristic_type: params} # params 可能非空
# 修复后:对 percentage 做无参处理
def build_heuristic(heuristic_type, params=None):
if heuristic_type == "percentage":
return {heuristic_type: {}}
return {heuristic_type: params or {}}
方案四:在网关层拦截并修正 #
如果无法立即修改客户端代码,可以在
INFINI Gateway 层通过请求改写规则,自动将非法的 percentage 节点修复为空对象,作为临时止血方案。
5. 预防建议 #
- 在应用层按启发式类型做 schema 校验:在 DSL 生成后、发送前,对
significance_heuristic结构做校验,尤其检查percentage是否为空对象。 - 为不同启发式类型拆分配置模板:避免将
percentage与其他有参启发式共用同一套参数渲染逻辑,从设计上杜绝误注入的可能。 - 对复杂聚合生成逻辑补充单元测试:覆盖每种启发式类型的输出结果,确保模板修改后不会引入回归问题。
- 建立 DSL 快照测试机制:将关键聚合请求的 DSL 持久化并做 diff 检查,当生成逻辑变更时能快速发现异常输出。
- 使用 INFINI Console 观察聚合请求趋势:通过 INFINI Console 监控聚合请求的错误率与响应时间,及时发现因 DSL 问题导致的批量失败。
6. 小结 #
expected an empty object 的含义非常直接:percentage 显著性启发式节点里本不该有任何额外内容。该错误本质是一个 DSL 结构问题,而非 Elasticsearch 运行时异常。修复时,要么将 percentage 恢复为空对象 {},要么切换到真正支持所需参数的启发式类型。通过完善 DSL 生成逻辑、增加针对性的校验和测试,可以有效避免此类问题再次发生。
相关错误 #
- failed-to-parse-significance-heuristic-unknown-object-how-to-solve-this-elasticsearch-exception
- unexpected-token-token-in-reducername-how-to-solve-this-elasticsearch-exception
- failed-to-parse-request-how-to-solve-this-elasticsearch-exception
- unknown-significance-heuristic-type-how-to-solve-this-elasticsearch-exception
附:日志上下文 #
// org.elasticsearch.search.aggregations.bucket.significant.SignificanceHeuristicParser
public static SignificanceHeuristic parse(XContentParser parser)
throws IOException {
// percentage 启发式期望直接读到 END_OBJECT
if (parser.nextToken() != XContentParser.Token.END_OBJECT) {
throw new ElasticsearchParseException(
"failed to parse [percentage] significance heuristic. " +
"expected an empty object; but got [{}] instead",
parser.currentToken()
);
}
return new PercentageScore();
}





