在 Painless 中使用日期时间

编辑

在 Painless 中使用日期时间

编辑

日期时间 API

编辑

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

日期时间表示

编辑

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

数值
日期时间表示为自称为纪元的起始偏移量开始的数字;在 Painless 中,这通常是一个 long,表示自 1970-01-01 00:00:00 祖鲁时间纪元以来的毫秒数
字符串
日期时间表示为由标准格式或自定义格式定义的字符序列;在 Painless 中,这通常是一个 字符串,采用 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();

日期时间修改

编辑

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

日期时间修改示例

编辑
  • 从数字日期时间中减去三秒(以毫秒为单位)

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

日期时间差(经过时间)

编辑

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

    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
    }
  • 两个复杂日期时间的 greater than 比较

    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
      }

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

      文档中的某些字段可能没有值。使用 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\";"
      }
    }
  }
}

消息存在时间的脚本字段示例

编辑

以下示例使用 脚本字段 作为 字段上下文 的一部分,以显示“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> 格式返回消息的存在时间。