📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 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_exceptionx_content_parse_exception
  • Kibana、cURL 或客户端 SDK 均返回相同错误,且错误栈中会出现 SignificanceHeuristicParserPercentageScore 等关键字。
  • 如果 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_OBJECTFIELD_NAMEVALUE_STRING 等),就会抛出上述异常。

出现此问题的常见原因包括:

  1. 误以为 percentage 支持参数:将其他启发式(如 mutual_informationchi_squarejlh)的参数结构错误地套用在 percentage 上。
  2. DSL 模板过度复用:在动态生成聚合 DSL 的代码中,对不同启发式类型使用了同一套参数渲染逻辑,导致额外字段被注入到 percentage 对象中。
  3. 从其他启发式复制粘贴时未清理字段:例如从 mutual_information 的配置复制过来后,忘记删除 include_negativesbackground_is_superset 等字段。
  4. JSON 生成逻辑缺陷:在应用层代码中,默认给所有启发式对象附加了通用字段(如 fieldsize 等),而 percentage 无法容纳这些内容。
  5. 版本差异导致的误解:某些启发式类型在不同版本中参数行为不同,升级后未同步调整 DSL 生成逻辑。

3. 如何排查这个异常 #

建议按以下步骤定位问题:

  1. 提取完整 DSL:从报错请求的 body 中取出完整的聚合 DSL,重点关注 significant_terms 下的 significance_heuristic 字段。
  2. 检查 percentage 节点内容:确认 percentage 的值是否为 {},以及是否包含任何额外字段、嵌套对象或数组。
  3. 回溯 DSL 生成逻辑:如果 DSL 由代码或模板生成,检查生成 significance_heuristic 的代码片段,确认是否有通用参数被无条件注入。
  4. 对比其他启发式配置:将当前 DSL 与 mutual_informationchi_square 等支持参数的启发式配置对比,确认是否存在结构混淆。
  5. 在简化环境中复现:剥离无关聚合条件,仅保留 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 生成逻辑、增加针对性的校验和测试,可以有效避免此类问题再次发生。

相关错误 #

附:日志上下文 #

// 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();
}