yyyy/MM/dd HH:mm:ssな文字列をISO8601の形式にして格納したい

概要

日付、Datetimeを示すフォーマットは多々ありますが、yyyy-MM-ddが標準なのか、yyyy/MM/ddが標準なのか、どっちだったっけ?ということはよくありますね。

今回は、Elasticseardchのフィールドの定義でdate型のフィールドなのだけども、格納時にはISO8601の形式にしておきたいが、Inputする文字列はそうでない(yyyy/MM/dd HH:mm:ssなときは)どうしようか、という話です。

確認環境

  • Elasticsearch 7.5.0

Input対象の文字列をそのまま入れるなら

2019/12/12 09:00:00 といった文字列を想定します。
これをdate型のフィールドに入れたければ、mappingの定義においてformatを指定しておくと良いです。

{
  "forum1212" : {
    "mappings" : {
      "properties" : {
         "testdate" : {
            "type" : "date",
            "format" : "yyyy/MM/dd HH:mm:ss"
          }
      }
   }
}

こうすることによって、文字列 -> 日付型 で格納がされますね。

ISO8601形式に変換する方法1

上のようなフォーマットを指定したものではなく、普通のdate(つまりISO8601形式)のものが良い場合は、どうしたらよいでしょうか。

フォーマットとしては、下のhogeフィールドのような定義のところに 2019/12/12 00:00:00といった文字列から生成されるdateを入れたいです。

{
  "forum1212" : {
    "mappings" : {
      "properties" : {
         "testdate" : {
            "type" : "date",
            "format" : "yyyy/MM/dd HH:mm:ss"
          },
         "hoge": {
           "type": "date"
         }
      }
   }
}

Ingest Pipeline

もっとも簡単な方法はIngest Pipelineでdate processorを使うことでしょう。

Date Processor | Elasticsearch Reference [master] | Elastic

dateプロセッサのformatsのところに文字列にあうフォーマット形式を書けば良いです。

PUT _ingest/pipeline/test
{
  "description": "test",
  "processors": [
    {
        "date" : {
          "field" : "testdate",
          "target_field" : "hoge",
          "formats" : ["yyyy/MM/dd HH:mm:ss"],
          "timezone" : "UTC"
        }
    }
  ]
}

Simulate

どのようになるかは、simulateをしてみましょう。

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "date" : {
          "field" : "testdate",
          "target_field" : "hoge",
          "formats" : ["yyyy/MM/dd HH:mm:ss"],
          "timezone" : "UTC"
        }
      }
    ]
  },
  "docs": [
    {
      "_index": "aaa",
      "_id": "id1",
      "_source": {
        "testdate": "2019/12/12 00:00:00"
      }
    }
  ]
}

そうしますと、結果はこのようになります。

{
  "docs" : [
    {
      "doc" : {
        "_index" : "aaa",
        "_type" : "_doc",
        "_id" : "id1",
        "_source" : {
          "testdate" : "2019/12/12 00:00:00",
          "hoge" : "2019-12-12T00:00:00.000Z"
        },
        "_ingest" : {
          "timestamp" : "2019-12-12T13:13:11.994933Z"
        }
      }
    }
  ]
}

hogeフィールドが、ISO8601形式になっていることが確認できましたね。

ISO8601形式に変換する方法2

dateプロセッサのアプローチが簡単かと思いますが、日付の加減算があるとか何らかの理由でスクリプトでアプローチする場合もあろうかと思います。

そんなときは、こうすれば良いでしょう。

PUT _ingest/pipeline/test
{
  "description": "test",
  "processors": [
    {
      "script": {
        "lang": "painless",
        "source": """
          String testdate = ctx['testdate'];
          DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
          LocalDateTime date = LocalDateTime.parse(testdate, dtf);
          ctx.hoge = date.format(DateTimeFormatter.ISO_DATE_TIME);
          
        """
      }
    }
  ]
}

ここでのポイントは2つあります。

  • DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss") で文字列をパースするフォーマッターを作るところ
  • LocalDateTimeを使うこと

ZonedDateTimeを使うとトラップにはまる

今回の文字列は yyyy/MM/dd HH:mm:ssという形式なので、タイムゾーンを表すものがありません。

そのため、ZonedDateTimeをうっかりつかってしまうとフォーマットの形式はあっているように見えても、ずっとparseでエラーになります。

タイムゾーンがないので、LocalDateTimeを使ってください。ここがポイントです。

        "caused_by" : {
          "type" : "date_time_parse_exception",
          "reason" : "Text '2019/12/12 09:00:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-12-12T09:00 of type java.time.format.Parsed",
          "caused_by" : {
            "type" : "date_time_exception",
            "reason" : "Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2019-12-12T09:00 of type java.time.format.Parsed",
            "caused_by" : {
              "type" : "date_time_exception",
              "reason" : "Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2019-12-12T09:00 of type java.time.format.Parsed"
            }
          }
        }

Simulate

LocalDateTimeを指定していることを再度確認して、Simulateを実行してみましょう。

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
       "script": {
        "source": """
          String testdate = ctx['testdate'];
          DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
          LocalDateTime date = LocalDateTime.parse(testdate, dtf);
          ctx.hoge = date.format(DateTimeFormatter.ISO_DATE_TIME);
        """
        }
      }
    ]
  },
  "docs": [
    {
      "_index": "aaa",
      "_id": "id1",
      "_source": {
        "testdate": "2019/12/12 00:00:00"
      }
    }
  ]
}

そうしますと、結果はこのように確認できます。

{
  "docs" : [
    {
      "doc" : {
        "_index" : "aaa",
        "_type" : "_doc",
        "_id" : "id1",
        "_source" : {
          "testdate" : "2019/12/12 00:00:00",
          "hoge" : "2019-12-12T00:00:00"
        },
        "_ingest" : {
          "timestamp" : "2019-12-12T13:26:01.755883Z"
        }
      }
    }
  ]
}

良い感じにyyyy-MM-ddの形式に変わりましたね。 もともとタイムゾーン指定がないので、UTCの日付である、という前提ですが必要があればscriptの中で9時間引くとか、足すとかあっても良いかと思います。

まとめ

dateプロセッサを使うのが楽、scriptを使うアプローチはLocalDateTimeなのか、ZonedDateTimeなのか、自分がどっちを使う(使える)のかをよく考えるのが大事。

では、Advent Calendar2019 n日目の記事をこれで終わります。(嘘)

ごきげんよう