在 Painless 中使用日期时间

编辑

在 Painless 中使用日期时间

编辑

日期时间 API

编辑

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

日期时间表示

编辑

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

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

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

日期时间解析和格式化

编辑

日期时间解析是从字符串日期时间转换为复杂日期时间,日期时间格式化是从复杂日期时间转换为字符串日期时间。

DateTimeFormatter 是一种复杂类型 (对象 (object)),它定义了字符串日期时间允许的字符序列。日期时间解析和格式化通常需要 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();

日期时间修改

编辑

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

日期时间修改示例

编辑
  • 从以毫秒为单位的数值日期时间中减去三秒

    long milliSinceEpoch = 434931330000L;
    milliSinceEpoch = milliSinceEpoch - 1000L*3L;
  • 向复杂日期时间添加三天

    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);

日期时间差(经过时间)

编辑

使用两个数值日期时间或两个复杂日期时间来计算两个不同日期时间之间的差(经过时间)。使用减法计算相同时间单位(如毫秒)的两个数值日期时间之间的差。对于复杂日期时间,通常可以使用一种方法或另一种复杂类型 (对象 (object)) 来计算差值。如果支持,可以使用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);

日期时间比较

编辑

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

日期时间比较示例

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

    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 示例

编辑
  • 使用数字日期时间作为 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" }

星期几桶聚合示例

编辑

以下示例使用 terms 聚合 作为 桶脚本聚合上下文 的一部分,以显示每周每一天的消息数量。

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

早晚桶聚合示例

编辑

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

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

消息年龄脚本字段示例

编辑

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

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; 

解析用户定义参数中输入的日期时间“now”。

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

查找“now”和接收消息的日期时间之间的年份差。

添加年份差,之后以 Y <years> ... 的格式返回消息的年龄。

添加年份,以便只有月份、日期等的剩余部分作为“now”和接收消息的日期时间之间的差值。重复此模式,直到达到所需的粒度(本例中为秒)。

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