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

适用版本: 6.8-8.17

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

could not parse date/time. expected date field [field] to be either a number or a string but found [token] instead 是 Elasticsearch 在解析日期字段时抛出的异常,表示传入的字段值类型不符合日期解析器的预期。

Elasticsearch 的日期字段解析器在读取 JSON 文档时,会检查当前 token 的类型。只有以下两种类型被允许:

  • 数字(VALUE_NUMBER):视为 Unix 时间戳(毫秒),直接转换为日期。
  • 字符串(VALUE_STRING):按照日期格式或内置格式进行解析,也支持日期数学表达式(如 now-1d)。

如果传入的是对象(VALUE_OBJECT)、数组(START_ARRAY)、布尔值(VALUE_BOOLEAN)、null 或其他任意非数字/字符串的 token,解析器会立即抛出此异常并拒绝该文档。

常见现象 #

  • 写入单个文档时返回 400 Bad Request,响应体中包含上述异常信息。
  • 批量写入(_bulk)时,该异常会导致整批请求失败,或仅导致单个 action 失败(取决于客户端配置),错误出现在 items 数组的对应项里。
  • 应用侧常见表现包括:批量写入失败重试次数增加、数据积压、部分文档丢失(因写入被拒绝)、日志中大量出现 parse_exception 关键字。
  • 在 Elasticsearch 服务端日志(elasticsearch.log)中,可以看到完整的异常栈,指向 ElasticsearchParseException 以及对应的字段名和非法 token 类型。

典型报错与异常栈 #

客户端收到的响应通常类似下面这样:

{
  "error": {
    "root_cause": [
      {
        "type": "parse_exception",
        "reason": "could not parse date/time. expected date field [create_time] to be either a number or a string but found [START_OBJECT] instead"
      }
    ],
    "type": "parse_exception",
    "reason": "could not parse date/time. expected date field [create_time] to be either a number or a string but found [START_OBJECT] instead"
  },
  "status": 400
}

服务端日志中的异常栈通常类似下面这样:

org.elasticsearch.ElasticsearchParseException: could not parse date/time. expected date field [create_time] to be either a number or a string but found [START_OBJECT] instead
    at org.elasticsearch.common.joda.JodaDateMathParser.parse(JodaDateMathParser.java:...)
    at org.elasticsearch.index.mapper.DateFieldMapper.parseCreateField(DateFieldMapper.java:...)
    at org.elasticsearch.index.mapper.FieldMapper.parse(FieldMapper.java:...)

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

该异常的根本原因是:写入到日期字段的值在 JSON 中的类型既不是数字也不是字符串。下面列出最常见的几种场景。

2.1 日期字段的值被错误地写成 JSON 对象 #

这是最常见的原因。例如,应用代码在构造日期值时,错误地将 Date 对象直接序列化,导致生成了包含 yearmonth 等字段的 JSON 对象,而不是一个字符串或数字。

错误示例:

{
  "create_time": {
    "year": 2024,
    "month": 6,
    "day": 11
  }
}

Elasticsearch 在解析到 create_time 时遇到的 token 是 START_OBJECT,因此抛出异常。

2.2 日期字段的值被写成数组 #

某些场景下,应用代码可能将日期字段处理成了数组,例如批量处理时误将单个值包装成了列表。

错误示例:

{
  "create_time": [1623408000000]
}

或:

{
  "create_time": ["2024-06-11T00:00:00Z"]
}

数组对应的 token 是 START_ARRAY,不符合日期解析器的要求。

2.3 日期字段的值被写成布尔值 #

极少数情况下,数据来源可能存在质量问题,或者字段映射发生变更后旧数据仍在写入,导致布尔值被写入日期字段。

错误示例:

{
  "create_time": true
}

true 对应的 token 是 VALUE_BOOLEAN,同样会触发异常。

2.4 SDK 或序列化框架的默认行为导致 #

某些编程语言的 JSON 序列化框架在序列化 null 值或特殊对象时,可能产生非预期的结果。例如:

  • Java 的 ObjectMapper 在序列化 LocalDateTime 时,若未配置日期格式化模块,可能输出为一个包含多个字段的对象。
  • Python 的 datetime 对象若未调用 .isoformat() 或自定义序列化器,可能被序列化为默认的对象表示形式。
  • 前端 JavaScript 中,若 Date 对象未调用 .toISOString() 就直接放入请求体,结果可能是 {} 或包含内部字段的对象,取决于框架行为。

2.5 动态模板或 mapping 变更后的不兼容写入 #

如果索引的 mapping 中某个字段原本是 keywordobject 类型,后来被重建为 date 类型,那么旧格式的数据(例如原本是对象结构的字段)在新 mapping 下写入时就会触发此异常。

3. 如何排查和解决这个异常 #

建议按"先确认错误字段和值类型,再定位数据来源,最后修复"的顺序处理:

排查步骤 #

  1. 确认异常指向的具体字段名:从异常信息中的 expected date field [field] 部分获取字段名,例如 create_time
  2. 查看实际写入的 JSON 文档:在应用日志、批量请求日志或 Elasticsearch 慢日志中找到触发异常的完整文档内容。
  3. 检查该字段值的 JSON token 类型:确认它是对象、数组、布尔值还是其他非数字/字符串类型。
  4. 追溯数据来源的序列化逻辑:找到应用代码中构造该字段值的代码路径,确认序列化方式。
  5. 检查 mapping 是否发生过变更:通过 GET /<index>/_mapping 确认字段类型是否为 date,并回顾近期是否有 mapping 变更。
  6. 确认 Elasticsearch 版本差异:ES 7.x 前后日期解析逻辑有差异,且 date_nanos 类型的行为略有不同,需结合版本确认。

使用 Elasticsearch API 辅助排查 #

查看索引 mapping,确认字段类型:

curl -X GET "localhost:9200/my-index/_mapping?pretty"

查看最近写入失败的文档(如果有启用审计日志或慢日志):

# 查看 Elasticsearch 日志中相关错误
grep -i "could not parse date/time" /var/log/elasticsearch/elasticsearch.log | tail -50

4. 如何解决这个错误 #

4.1 修复数据格式 —— 改为数字时间戳 #

如果应用侧使用时间戳,确保写入的是数字(毫秒),而不是被引号包裹的字符串数字(虽然字符串数字也能被解析,但需确保格式正确)。

正确示例:

{
  "create_time": 1623408000000
}

Java 示例:

import java.time.Instant;

long timestamp = Instant.now().toEpochMilli();
// 直接写入数字,不要加引号
json.put("create_time", timestamp);

4.2 修复数据格式 —— 改为字符串日期 #

确保日期以字符串形式提供,并符合 mapping 中定义的格式(默认为 strict_date_optional_time||epoch_millis)。

正确示例:

{
  "create_time": "2024-06-11T00:00:00Z"
}

或:

{
  "create_time": "2024-06-11"
}

Java 示例:

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

String dateStr = ZonedDateTime.now(ZoneOffset.UTC)
    .format(DateTimeFormatter.ISO_INSTANT);
json.put("create_time", dateStr);

Python 示例:

from datetime import datetime, timezone

now = datetime.now(timezone.utc).isoformat()
doc = {"create_time": now}

JavaScript/Node.js 示例:

const doc = {
  create_time: new Date().toISOString()
};

4.3 修复数据格式 —— 使用日期数学表达式 #

Elasticsearch 支持在日期字段中使用数学表达式字符串,例如:

{
  "create_time": "now"
}
{
  "create_time": "now-1d"
}

这在索引文档时由 Elasticsearch 服务端解析,客户端无需自行格式化日期。

4.4 修复 mapping 或使用 multi-field #

如果业务上该字段确实需要同时支持多种类型,可以考虑以下方案:

方案 A:修改 mapping,使用 date 类型并指定宽松格式

curl -X PUT "localhost:9200/my-index/_mapping" -H 'Content-Type: application/json' -d '
{
  "properties": {
    "create_time": {
      "type": "date",
      "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss"
    }
  }
}
'

方案 B:使用 multi-field,保留原始字符串

curl -X PUT "localhost:9200/my-index/_mapping" -H 'Content-Type: application/json' -d '
{
  "properties": {
    "create_time": {
      "type": "date",
      "fields": {
        "keyword": {
          "type": "keyword"
        }
      }
    }
  }
}
'

4.5 修复批量写入中的错误文档 #

如果异常出现在批量写入中,需要先找出并修复错误文档,然后重新提交。可以通过以下步骤处理:

  1. 将批量请求体保存到文件。
  2. 使用脚本定位格式错误的行(检查对应字段的 JSON 结构)。
  3. 修复后重新提交,或使用 _bulk?refresh=wait_for 控制刷新行为。

常用修复思路总结 #

  • 优先将日期字段值改为 ISO-8601 字符串(如 2024-06-11T00:00:00Z),兼容性最好,可读性也最强。
  • 如果使用时间戳,确保是数字类型,不要加引号,单位默认为毫秒。
  • 检查 SDK 序列化配置,确保日期对象被正确序列化为字符串或数字,而不是对象。
  • 对日期字段建立类型校验机制,在应用侧拦截非法类型后再发送请求。
  • 如果历史数据存在格式问题,考虑重建索引并通过 reindex API 配合脚本修复数据格式。

后续注意事项与推荐建议 #

  • 在应用代码中,对日期字段的序列化逻辑进行统一封装,避免散落在各处的临时格式化代码造成不一致。
  • 为 Elasticsearch 写入操作添加响应状态码检查,对 400 错误进行专门的日志记录和告警,便于快速发现问题。
  • 在批量写入场景中,建议在应用侧先做一次 JSON 序列化后的 schema 校验,提前拦截格式错误的文档。
  • 定期审查索引 mapping,确保字段类型与业务数据的实际类型一致,避免 mapping 变更后旧数据无法写入。

借助 INFINI 产品提升排障效率 #

  • INFINI Console 适合查看集群健康度、索引 mapping、写入错误趋势和请求画像,帮助快速判断异常是数据格式问题、mapping 问题还是服务端问题。
  • INFINI Gateway 适合部署在 Elasticsearch 前面做请求观测、流量治理和异常请求拦截,可以在请求到达 Elasticsearch 之前对日期字段等特定字段进行格式校验和转换,避免非法数据触发异常。
  • 如果需要长期治理,建议把写入失败日志、慢查询、调用来源和 mapping 变更记录统一接入监控面板,缩短从"发现写入失败"到"定位根因"的时间。

5. 小结 #

could not parse date/time. expected date field to be either a number or a string but found [token] instead 是一个典型的数据格式错误,而非服务端故障。它的根因几乎总是客户端写入的数据中,日期字段的值类型不符合 Elasticsearch 日期解析器的要求。

处理这类异常时,最有效的方法是:

  1. 从异常信息中确认具体的字段名和非法 token 类型。
  2. 检查实际写入的 JSON 文档,定位数据格式问题。
  3. 修复应用侧的序列化逻辑,确保日期字段以数字(时间戳)或字符串(ISO-8601 等格式)的形式写入。
  4. 必要时调整 mapping 或使用 INFINI Gateway 在请求入口处做格式校验。

只要把数据格式校验、序列化规范、mapping 管理和监控手段固定下来,这类异常几乎可以完全避免。

相关错误 #

附:日志上下文 #

下面保留源码中的相关片段,便于结合异常调用栈定位问题:

// 来自 Elasticsearch 源码(DateFieldMapper / DateMathParser 相关逻辑)
if (token == XContentParser.Token.VALUE_NULL) {
    return null;
}
if (token != XContentParser.Token.VALUE_NUMBER
    && token != XContentParser.Token.VALUE_STRING) {
    throw new ElasticsearchParseException(
        "could not parse date/time. expected date field [{}] to be either a number or a string but found [{}] instead",
        fieldName, token
    );
}

常见非法 token 类型对照:

token 类型说明示例值
START_OBJECTJSON 对象{"year":2024,"month":6}
START_ARRAYJSON 数组[1623408000000]
VALUE_BOOLEAN布尔值true / false
VALUE_EMBEDDED_OBJECT二进制嵌入对象二进制数据
VALUE_NULLnull 值null(上方单独处理)