在 Painless 中使用日期时间

编辑

在 Painless 中使用日期时间编辑

日期时间 API编辑

Painless 中的日期时间使用标准 Java 库,并通过 Painless 共享 API 提供。以下 Java 包中的大多数类都可以在 Painless 脚本中使用

日期时间表示编辑

Painless 中的日期时间最常表示为数值、字符串值或复杂值。

数值
日期时间表示为从起始偏移量(称为纪元)开始的数字;在 Painless 中,这通常是 long,表示自 1970-01-01 00:00:00 协调世界时(UTC)纪元以来的毫秒数
字符串
日期时间表示为由标准格式或自定义格式定义的字符序列;在 Painless 中,这通常是 String,采用标准格式 ISO 8601
复杂
日期时间表示为复杂类型(对象),它抽象了日期时间存储方式的内部细节,并通常提供用于修改和比较的实用程序;在 Painless 中,这通常是 ZonedDateTime

在不同日期时间表示之间切换通常是实现脚本目标所必需的。脚本中的典型模式是将数值或字符串日期时间切换为复杂日期时间,修改或比较复杂日期时间,然后将其切换回数值或字符串日期时间以进行存储或返回结果。

日期时间解析和格式化编辑

日期时间解析是从字符串日期时间切换到复杂日期时间,而日期时间格式化是从复杂日期时间切换到字符串日期时间。

DateTimeFormatter 是一种复杂类型(对象),它定义了字符串日期时间允许的字符序列。日期时间解析和格式化通常需要 DateTimeFormatter。有关如何使用 DateTimeFormatter 的更多信息,请参阅 Java 文档

日期时间解析示例编辑

  • 从毫秒解析

    String milliSinceEpochString = "434931330000";
    long milliSinceEpoch = Long.parseLong(milliSinceEpochString);
    Instant instant = Instant.ofEpochMilli(milliSinceEpoch);
    ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of('Z'));
  • 从 ISO 8601 解析

    String datetime = '1983-10-13T22:15:30Z';
    ZonedDateTime zdt = ZonedDateTime.parse(datetime); 

    请注意,parse 方法默认使用 ISO 8601。

  • 从 RFC 1123 解析

    String datetime = 'Thu, 13 Oct 1983 22:15:30 GMT';
    ZonedDateTime zdt = ZonedDateTime.parse(datetime,
            DateTimeFormatter.RFC_1123_DATE_TIME); 

    请注意使用内置的 DateTimeFormatter。

  • 从自定义格式解析

    String datetime = 'custom y 1983 m 10 d 13 22:15:30 Z';
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
            "'custom' 'y' yyyy 'm' MM 'd' dd HH:mm:ss VV");
    ZonedDateTime zdt = ZonedDateTime.parse(datetime, dtf); 

    请注意使用自定义 DateTimeFormatter。

日期时间格式化示例编辑

  • 格式化为 ISO 8601

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    String datetime = zdt.format(DateTimeFormatter.ISO_INSTANT); 

    请注意使用内置的 DateTimeFormatter。

  • 格式化为自定义格式

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
            "'date:' yyyy/MM/dd 'time:' HH:mm:ss");
    String datetime = zdt.format(dtf); 

    请注意使用自定义 DateTimeFormatter。

日期时间转换编辑

日期时间转换是从数值日期时间切换到复杂日期时间,反之亦然。

日期时间转换示例编辑

  • 从毫秒转换

    long milliSinceEpoch = 434931330000L;
    Instant instant = Instant.ofEpochMilli(milliSinceEpoch);
    ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of('Z'));
  • 转换为毫秒

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    long milliSinceEpoch = zdt.toInstant().toEpochMilli();

日期时间片段编辑

日期时间表示通常包含用于提取单个日期时间片段(如年份、小时、时区等)的数据。使用日期时间的单个片段来创建复杂日期时间,并使用复杂日期时间来提取单个片段。

日期时间片段示例编辑

  • 从片段创建复杂日期时间

    int year = 1983;
    int month = 10;
    int day = 13;
    int hour = 22;
    int minutes = 15;
    int seconds = 30;
    int nanos = 0;
    ZonedDateTime zdt = ZonedDateTime.of(
            year, month, day, hour, minutes, seconds, nanos, ZoneId.of('Z'));
  • 从复杂日期时间提取片段

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 100, ZoneId.of(tz));
    int year = zdt.getYear();
    int month = zdt.getMonthValue();
    int day = zdt.getDayOfMonth();
    int hour = zdt.getHour();
    int minutes = zdt.getMinute();
    int seconds = zdt.getSecond();
    int nanos = zdt.getNano();

日期时间修改编辑

使用数值日期时间或复杂日期时间来进行修改,例如将日期时间增加几秒或从日期时间减去几天。使用标准 数值运算符 来修改数值日期时间。使用 方法(或字段)来修改复杂日期时间。请注意,许多复杂日期时间是不可变的,因此在修改时会创建一个新的复杂日期时间,需要 赋值 或立即使用。

日期时间修改示例编辑

  • 从以毫秒为单位的数值日期时间减去 3 秒

    long milliSinceEpoch = 434931330000L;
    milliSinceEpoch = milliSinceEpoch - 1000L*3L;
  • 将复杂日期时间增加 3 天

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime updatedZdt = zdt.plusDays(3);
  • 从复杂日期时间减去 125 分钟

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime updatedZdt = zdt.minusMinutes(125);
  • 设置复杂日期时间的年份

    ZonedDateTime zdt =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime updatedZdt = zdt.withYear(1976);

日期时间差(经过时间)编辑

使用两个数值日期时间或两个复杂日期时间来计算两个不同日期时间之间的差(经过时间)。使用 减法 来计算两个相同时间单位(如毫秒)的数值日期时间之间的差。对于复杂日期时间,通常可以使用方法或其他复杂类型(对象)来计算差值。使用 ChronoUnit 来计算两个复杂日期时间之间的差值(如果支持)。

日期时间差示例编辑

  • 两个数值日期时间之间的毫秒差

    long startTimestamp = 434931327000L;
    long endTimestamp = 434931330000L;
    long differenceInMillis = endTimestamp - startTimestamp;
  • 两个复杂日期时间之间的毫秒差

    ZonedDateTime zdt1 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 11000000, ZoneId.of('Z'));
    ZonedDateTime zdt2 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 35, 0, ZoneId.of('Z'));
    long differenceInMillis = ChronoUnit.MILLIS.between(zdt1, zdt2);
  • 两个复杂日期时间之间的天数差

    ZonedDateTime zdt1 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 11000000, ZoneId.of('Z'));
    ZonedDateTime zdt2 =
            ZonedDateTime.of(1983, 10, 17, 22, 15, 35, 0, ZoneId.of('Z'));
    long differenceInDays = ChronoUnit.DAYS.between(zdt1, zdt2);

日期时间比较编辑

使用两个数值日期时间或两个复杂日期时间来进行日期时间比较。使用标准 比较运算符 来比较两个相同时间单位(如毫秒)的数值日期时间。对于复杂日期时间,通常可以使用方法或其他复杂类型(对象)来进行比较。

日期时间比较示例编辑

  • 两个以毫秒为单位的数值日期时间之间的大于比较

    long timestamp1 = 434931327000L;
    long timestamp2 = 434931330000L;
    
    if (timestamp1 > timestamp2) {
       // handle condition
    }
  • 两个复杂日期时间之间的相等比较

    ZonedDateTime zdt1 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime zdt2 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    
    if (zdt1.equals(zdt2)) {
        // handle condition
    }
  • 两个复杂日期时间之间的小于比较

    ZonedDateTime zdt1 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime zdt2 =
            ZonedDateTime.of(1983, 10, 17, 22, 15, 35, 0, ZoneId.of('Z'));
    
    if (zdt1.isBefore(zdt2)) {
        // handle condition
    }
  • 两个复杂日期时间之间的大于比较

    ZonedDateTime zdt1 =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime zdt2 =
            ZonedDateTime.of(1983, 10, 17, 22, 15, 35, 0, ZoneId.of('Z'));
    
    if (zdt1.isAfter(zdt2)) {
        // handle condition
    }

日期时间时区编辑

字符串日期时间和复杂日期时间都具有时区,默认值为 UTC。数值日期时间没有足够明确的信息来拥有时区,因此始终假定为 UTC。使用 方法(或字段)以及 ZoneId 来更改复杂日期时间的时区。将字符串日期时间解析为复杂日期时间以更改时区,然后将复杂日期时间格式化为所需的字符串日期时间。请注意,许多复杂日期时间是不可变的,因此在修改时会创建一个新的复杂日期时间,需要 赋值 或立即使用。

日期时间时区示例编辑

  • 修改复杂日期时间的时区

    ZonedDateTime utc =
            ZonedDateTime.of(1983, 10, 13, 22, 15, 30, 0, ZoneId.of('Z'));
    ZonedDateTime pst = utc.withZoneSameInstant(ZoneId.of('America/Los_Angeles'));
  • 修改字符串日期时间的时区

    String gmtString = 'Thu, 13 Oct 1983 22:15:30 GMT';
    ZonedDateTime gmtZdt = ZonedDateTime.parse(gmtString,
            DateTimeFormatter.RFC_1123_DATE_TIME); 
    ZonedDateTime pstZdt =
            gmtZdt.withZoneSameInstant(ZoneId.of('America/Los_Angeles'));
    String pstString = pstZdt.format(DateTimeFormatter.RFC_1123_DATE_TIME);

    请注意使用内置的 DateTimeFormatter。

日期时间输入编辑

根据 Painless 上下文,日期时间作为脚本输入的方式有很多种。通常,日期时间输入将从用户指定的参数、原始源文档或索引文档中获取。

来自用户参数的日期时间输入编辑

在脚本规范期间使用 params 部分 将数值日期时间或字符串日期时间作为脚本输入传递。在脚本中访问用户定义的参数取决于 Painless 上下文,但参数通常可以通过名为 params 的输入访问。

示例

  • 将来自用户参数的数值日期时间解析为复杂日期时间

    • 输入

      ...
      "script": {
          ...
          "params": {
              "input_datetime": 434931327000
          }
      }
      ...
    • 脚本

      long inputDateTime = params['input_datetime'];
      Instant instant = Instant.ofEpochMilli(inputDateTime);
      ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of('Z'));
  • 将用户参数中的字符串日期时间解析为复杂日期时间

    • 输入

      ...
      "script": {
          ...
          "params": {
              "input_datetime": "custom y 1983 m 10 d 13 22:15:30 Z"
          }
      }
      ...
    • 脚本

      String datetime = params['input_datetime'];
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
              "'custom' 'y' yyyy 'm' MM 'd' dd HH:mm:ss VV");
      ZonedDateTime zdt = ZonedDateTime.parse(datetime, dtf); 

      请注意使用自定义 DateTimeFormatter。

来自源文档的日期时间输入编辑

使用原始的 文档作为脚本输入,以访问该文档中特定字段的数字日期时间或字符串日期时间。在脚本中访问原始源文档取决于 Painless 上下文,并不总是可用。原始源文档最常通过名为 ctx['_source']params['_source'] 的输入访问。

示例

  • 将源文档中的数字日期时间解析为复杂日期时间

    • 输入

      {
        ...
        "input_datetime": 434931327000
        ...
      }
    • 脚本

      long inputDateTime = ctx['_source']['input_datetime']; 
      Instant instant = Instant.ofEpochMilli(inputDateTime);
      ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of('Z'));

      注意访问 _source 取决于 Painless 上下文。

  • 将源文档中的字符串日期时间解析为复杂日期时间

    • 输入

      {
        ...
        "input_datetime": "1983-10-13T22:15:30Z"
        ...
      }
    • 脚本

      String datetime = params['_source']['input_datetime']; 
      ZonedDateTime zdt = ZonedDateTime.parse(datetime); 

      注意访问 _source 取决于 Painless 上下文。

      请注意,parse 方法默认使用 ISO 8601。

来自索引文档的日期时间输入编辑

使用索引文档作为脚本输入,以访问该文档中特定字段的复杂日期时间,其中该字段被映射为 标准日期纳秒日期。映射为 数字 的数字日期时间字段和映射为 关键字 的字符串日期时间字段也可以通过索引文档访问。在脚本中访问索引文档取决于 Painless 上下文,并不总是可用。索引文档最常通过名为 doc 的输入访问。

示例

  • 将索引文档中的复杂日期时间格式化为字符串日期时间

    • 假设

      • 字段 input_datetime 作为查询的一部分存在于所有索引中
      • 所有索引文档都包含字段 input_datetime
    • 映射

      {
        "mappings": {
          ...
          "properties": {
            ...
            "input_datetime": {
              "type": "date"
            }
            ...
          }
          ...
        }
      }
    • 脚本

      ZonedDateTime input = doc['input_datetime'].value;
      String output = input.format(DateTimeFormatter.ISO_INSTANT); 

      请注意使用内置的 DateTimeFormatter。

  • 查找索引文档中两个复杂日期时间之间的差异

    • 假设

      • 字段 startend 可能 作为查询的一部分存在于所有索引中
      • 字段 startend 可能 在所有索引文档中具有值
    • 映射

      {
        "mappings": {
          ...
          "properties": {
            ...
            "start": {
              "type": "date"
            },
            "end": {
              "type": "date"
            }
            ...
          }
          ...
        }
      }
    • 脚本

      if (doc.containsKey('start') && doc.containsKey('end')) { 
      
          if (doc['start'].size() > 0 && doc['end'].size() > 0) { 
      
              ZonedDateTime start = doc['start'].value;
              ZonedDateTime end = doc['end'].value;
              long differenceInMillis = ChronoUnit.MILLIS.between(start, end);
      
              // handle difference in times
          } else {
              // handle fields without values
          }
      } else {
          // handle index with missing fields
      }

      当查询的结果跨越多个索引时,某些索引可能不包含特定字段。使用 doc 输入上的 containsKey 方法调用来确保字段作为当前文档的索引的一部分存在。

      文档中某些字段可能没有值。使用 doc 输入中字段上的 size 方法调用来确保该字段至少对当前文档具有一个值。

现在日期时间编辑

在大多数 Painless 上下文中,不支持当前日期时间 now。这主要有两个原因。第一个是脚本通常每个文档运行一次,因此每次运行脚本时都会返回不同的 now。第二个是脚本通常以分布式方式运行,没有适当同步 now 的方法。相反,请使用字符串日期时间或数字日期时间传递一个用户定义的参数作为 now。数字日期时间是首选,因为不需要解析它进行比较。

现在日期时间示例编辑

  • 使用数字日期时间作为 now

    • 假设

      • 字段 input_datetime 作为查询的一部分存在于所有索引中
      • 所有索引文档都包含字段 input_datetime
    • 映射

      {
        "mappings": {
            ...
            "properties": {
              ...
              "input_datetime": {
              "type": "date"
            }
            ...
          }
          ...
        }
      }
    • 输入

      ...
      "script": {
          ...
          "params": {
              "now": <generated numeric datetime in milliseconds since epoch>
          }
      }
      ...
    • 脚本

      long now = params['now'];
      ZonedDateTime inputDateTime = doc['input_datetime'];
      long millisDateTime = inputDateTime.toInstant().toEpochMilli();
      long elapsedTime = now - millisDateTime;
  • 使用字符串日期时间作为 now

    • 假设

      • 字段 input_datetime 作为查询的一部分存在于所有索引中
      • 所有索引文档都包含字段 input_datetime
    • 映射

      {
        "mappings": {
          ...
          "properties": {
            ...
            "input_datetime": {
              "type": "date"
            }
            ...
          }
          ...
        }
      }
    • 输入

      ...
      "script": {
          ...
          "params": {
              "now": "<generated string datetime in ISO-8601>"
          }
      }
      ...
    • 脚本

      String nowString = params['now'];
      ZonedDateTime nowZdt = ZonedDateTime.parse(nowString); 
      long now = ZonedDateTime.toInstant().toEpochMilli();
      ZonedDateTime inputDateTime = doc['input_datetime'];
      long millisDateTime = zdt.toInstant().toEpochMilli();
      long elapsedTime = now - millisDateTime;

      注意,这会解析每次运行脚本时相同的字符串日期时间。使用数字日期时间以避免显著的性能下降。

上下文中的日期时间示例编辑

加载示例数据编辑

运行以下 curl 命令以将上下文示例所需的数据加载到 Elasticsearch 集群中

  1. 为示例数据创建 映射

    PUT /messages
    {
      "mappings": {
        "properties": {
          "priority": {
            "type": "integer"
          },
          "datetime": {
            "type": "date"
          },
          "message": {
            "type": "text"
          }
        }
      }
    }
  2. 加载示例数据。

    POST /_bulk
    { "index" : { "_index" : "messages", "_id" : "1" } }
    { "priority": 1, "datetime": "2019-07-17T12:13:14Z", "message": "m1" }
    { "index" : { "_index" : "messages", "_id" : "2" } }
    { "priority": 1, "datetime": "2019-07-24T01:14:59Z", "message": "m2" }
    { "index" : { "_index" : "messages", "_id" : "3" } }
    { "priority": 2, "datetime": "1983-10-14T00:36:42Z", "message": "m3" }
    { "index" : { "_index" : "messages", "_id" : "4" } }
    { "priority": 3, "datetime": "1983-10-10T02:15:15Z", "message": "m4" }
    { "index" : { "_index" : "messages", "_id" : "5" } }
    { "priority": 3, "datetime": "1983-10-10T17:18:19Z", "message": "m5" }
    { "index" : { "_index" : "messages", "_id" : "6" } }
    { "priority": 1, "datetime": "2019-08-03T17:19:31Z", "message": "m6" }
    { "index" : { "_index" : "messages", "_id" : "7" } }
    { "priority": 3, "datetime": "2019-08-04T17:20:00Z", "message": "m7" }
    { "index" : { "_index" : "messages", "_id" : "8" } }
    { "priority": 2, "datetime": "2019-08-04T18:01:01Z", "message": "m8" }
    { "index" : { "_index" : "messages", "_id" : "9" } }
    { "priority": 3, "datetime": "1983-10-10T19:00:45Z", "message": "m9" }
    { "index" : { "_index" : "messages", "_id" : "10" } }
    { "priority": 2, "datetime": "2019-07-23T23:39:54Z", "message": "m10" }

星期几桶聚合示例编辑

以下示例使用 术语聚合 作为 桶脚本聚合上下文 的一部分,以显示每个星期几的消息数量。

GET /messages/_search?pretty=true
{
  "aggs": {
    "day-of-week-count": {
      "terms": {
        "script": "return doc[\"datetime\"].value.getDayOfWeekEnum();"
      }
    }
  }
}

早晚桶聚合示例编辑

以下示例使用 术语聚合 作为 桶脚本聚合上下文 的一部分,以显示早上和晚上收到的消息数量。

GET /messages/_search?pretty=true
{
  "aggs": {
    "am-pm-count": {
      "terms": {
        "script": "return doc[\"datetime\"].value.getHour() < 12 ? \"AM\" : \"PM\";"
      }
    }
  }
}

消息年龄脚本字段示例编辑

以下示例使用 脚本字段 作为 字段上下文 的一部分,以显示“现在”与收到消息之间的时间差。

GET /_search?pretty=true
{
  "query": {
    "match_all": {}
  },
  "script_fields": {
    "message_age": {
      "script": {
        "source": "ZonedDateTime now = ZonedDateTime.ofInstant(Instant.ofEpochMilli(params[\"now\"]), ZoneId.of(\"Z\")); ZonedDateTime mdt = doc[\"datetime\"].value; String age; long years = mdt.until(now, ChronoUnit.YEARS); age = years + \"Y \"; mdt = mdt.plusYears(years); long months = mdt.until(now, ChronoUnit.MONTHS); age += months + \"M \"; mdt = mdt.plusMonths(months); long days = mdt.until(now, ChronoUnit.DAYS); age += days + \"D \"; mdt = mdt.plusDays(days); long hours = mdt.until(now, ChronoUnit.HOURS); age += hours + \"h \"; mdt = mdt.plusHours(hours); long minutes = mdt.until(now, ChronoUnit.MINUTES); age += minutes + \"m \"; mdt = mdt.plusMinutes(minutes); long seconds = mdt.until(now, ChronoUnit.SECONDS); age += hours + \"s\"; return age;",
        "params": {
          "now": 1574005645830
        }
      }
    }
  }
}

以下是脚本的分解形式

ZonedDateTime now = ZonedDateTime.ofInstant(
        Instant.ofEpochMilli(params['now']), ZoneId.of('Z')); 
ZonedDateTime mdt = doc['datetime'].value; 

String age;

long years = mdt.until(now, ChronoUnit.YEARS); 
age = years + 'Y '; 
mdt = mdt.plusYears(years); 

long months = mdt.until(now, ChronoUnit.MONTHS);
age += months + 'M ';
mdt = mdt.plusMonths(months);

long days = mdt.until(now, ChronoUnit.DAYS);
age += days + 'D ';
mdt = mdt.plusDays(days);

long hours = mdt.until(now, ChronoUnit.HOURS);
age += hours + 'h ';
mdt = mdt.plusHours(hours);

long minutes = mdt.until(now, ChronoUnit.MINUTES);
age += minutes + 'm ';
mdt = mdt.plusMinutes(minutes);

long seconds = mdt.until(now, ChronoUnit.SECONDS);
age += hours + 's';

return age; 

将日期时间“现在”解析为来自用户定义参数的输入。

将收到消息的日期时间存储为 ZonedDateTime

查找“现在”与收到消息的日期时间之间的年份差。

将年份差添加到后面以 Y <years> ... 的格式返回,以表示消息的年龄。

添加年份,以便仅剩余月份、天数等的差值作为“现在”与收到消息的日期时间之间的差值。重复此模式,直到达到所需的粒度(本例中为秒)。

Y <years> M <months> D <days> h <hours> m <minutes> s <seconds> 的格式返回消息的年龄。