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

适用版本: 6.8-7.15

1. 错误异常的基本描述 #

Aggregation definition for [aggregationName] starts with a [START_OBJECT]; expected a [START_OBJECT] 是 Elasticsearch 在解析聚合(Aggregation)DSL 时抛出的语法异常。该错误表明聚合定义的 JSON 结构不符合 Elasticsearch 的预期格式,具体是聚合名称后面紧跟的内容不是一个合法的 JSON 对象({ })。

常见现象 #

  • 搜索请求返回 HTTP 400 Bad Request,响应体中包含 parse_exceptionx_content_parse_exception
  • 应用侧表现为搜索接口调用失败,聚合结果缺失,或整个查询请求被拒绝。
  • 在 Elasticsearch 服务端日志中可检索到如下关键字:
    • Aggregation definition for [...] starts with a [
    • parse_exception
    • x_content_parse_exception

典型报错与异常栈 #

实际报错信息通常如下:

{
  "error": {
    "root_cause": [
      {
        "type": "parse_exception",
        "reason": "Aggregation definition for [my_agg] starts with a [START_ARRAY]; expected a [START_OBJECT]"
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "failed_shards": [
      {
        "reason": {
          "type": "parse_exception",
          "reason": "Aggregation definition for [my_agg] starts with a [START_ARRAY]; expected a [START_OBJECT]"
        }
      }
    ]
  },
  "status": 400
}

2. 为什么会发生这个错误 #

Elasticsearch 的聚合 DSL 有严格的 JSON 结构要求:每个聚合名称后面必须跟一个 JSON 对象({ }),该对象内部定义聚合的类型和具体参数。当解析器在读取聚合名称后遇到的不是 {,就会抛出此异常。

常见原因 #

  • 聚合名称后缺少对象包裹:直接把聚合类型或参数写在聚合名称后面,而没有用 { } 包裹。
  • 错误使用数组 [] 代替对象 {}:在聚合定义中误用方括号,例如 "my_agg": [ { "terms": ... } ]
  • DSL 拼接或模板渲染错误:通过字符串拼接或模板引擎生成 DSL 时,结构被破坏,导致聚合定义格式非法。
  • 嵌套聚合结构错误:在定义嵌套聚合(sub-aggregations)时,子聚合没有正确嵌套在 aggs 对象内。
  • JSON 结构层级错误:聚合类型字段(如 termsavg)没有作为对象的键,而是被直接放在了错误的位置。

3. 如何排查这个异常 #

建议按以下步骤逐一排查:

3.1 获取完整请求体 #

  1. 从应用日志或 Elasticsearch 慢查询日志中抓取完整的搜索请求体。
  2. 如果是通过代码拼接 DSL,建议在发送前打印最终生成的 JSON 字符串。
  3. 使用 JSON 格式化工具(如 jq 或在 IDE 中格式化)检查 DSL 结构是否合法。

3.2 定位问题聚合 #

  1. 如果请求中包含多个聚合,逐个移除聚合定义,直到错误消失,从而定位具体是哪个聚合有问题。
  2. 重点检查错误提示中的聚合名称(aggregationName),确认其定义结构。
  3. 对比正确聚合 DSL 的官方示例,检查结构差异。

3.3 使用 Kibana Dev Tools 验证 #

在 Kibana Dev Tools 或 CURL 中直接发送简化后的 DSL,逐步添加聚合子句,确认每一步是否合法:

# 基础查询,先不加聚合
GET /my_index/_search
{
  "size": 0
}

# 逐步添加聚合,验证每一步
GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "my_agg": {
      "terms": { "field": "category.keyword" }
    }
  }
}

4. 如何解决这个错误 #

4.1 正确的聚合 DSL 结构 #

标准的聚合 DSL 结构如下:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "aggregation_name": {
      "aggregation_type": {
        "field": "field_name"
      }
    }
  }
}

4.2 常见错误与修复对照 #

错误示例 1:聚合名称后直接跟数组

{
  "aggs": {
    "my_agg": [
      { "terms": { "field": "category.keyword" } }
    ]
  }
}

修复后:

{
  "aggs": {
    "my_agg": {
      "terms": { "field": "category.keyword" }
    }
  }
}

错误示例 2:聚合类型不在对象内

{
  "aggs": {
    "my_agg": "terms",
    "field": "category.keyword"
  }
}

修复后:

{
  "aggs": {
    "my_agg": {
      "terms": { "field": "category.keyword" }
    }
  }
}

错误示例 3:嵌套子聚合结构错误

{
  "aggs": {
    "by_category": {
      "terms": { "field": "category.keyword" },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

注意:子聚合 aggs 必须嵌套在父聚合的对象内部,与上述结构一致才是正确的。

4.3 通过 INFINI Gateway 辅助排查 #

如果请求经过 INFINI Gateway,可以在请求日志中直接查看发送给 Elasticsearch 的原始 DSL,快速确认是否存在格式问题,无需登录服务器抓取日志。

5. 如何预防此类错误 #

5.1 开发阶段 #

  • 使用强类型的客户端库(如 Java High Level REST Client)构建 DSL,避免手动拼接 JSON 字符串。
  • 在单元测试中对生成 DSL 的方法进行验证,确保输出是合法 JSON 且结构正确。
  • 参考 Elasticsearch 官方聚合文档 编写聚合查询,避免凭记忆猜测结构。

5.2 运行时防护 #

  • INFINI Console 中配置对 400 错误和 parse_exception 的告警,及时发现 DSL 问题。
  • 通过 INFINI Gateway 对异常 DSL 进行请求拦截和日志记录,防止非法请求进入集群。
  • 建立搜索 DSL 的代码审查机制,聚合查询的变更必须经过评审后上线。

5.3 最佳实践 #

  • 聚合名称使用有意义的英文命名,避免使用特殊字符(方括号 []、尖括号 <> 等)。
  • 复杂聚合查询分步骤构建,每添加一层嵌套就验证一次结果。
  • 对生产环境的聚合查询设置合理的 size 上限,避免结果过大导致额外问题。

6. 小结 #

Aggregation definition starts with a [...] 错误的根本原因是聚合 DSL 的 JSON 结构不符合 Elasticsearch 的解析要求,最常见的问题是聚合名称后没有正确使用对象 {} 包裹聚合类型定义。修复方法是仔细检查聚合名称后的结构,确保其为一个合法的 JSON 对象,且内部包含正确的聚合类型字段。

通过规范的 DSL 构建方式、充分的本地验证以及 INFINI Console 和 INFINI Gateway 的辅助观测,可以在开发和运行时有效避免此类问题。

相关错误 #

附:源码上下文 #

以下为 Elasticsearch 源码中触发此异常的代码片段,帮助理解底层校验逻辑:

// org.elasticsearch.search.aggregations.AggregatorParsers
if (token != XContentParser.Token.START_OBJECT) {
    throw new ParsingException(
        parser.getTokenLocation(),
        "Aggregation definition for [" + aggregationName +
        "] starts with a [" + token + "]; expected a [" +
        XContentParser.Token.START_OBJECT + "]."
    );
}