SolrのDateRangeFieldで日時の範囲を扱う

はじめに

Solrで日時を扱うためのフィールドタイプとして、通常使う DatePointField の他にもう一つ DateRangeField があります。その名の通り幅の有る時間を取り扱えます。

DateRangeFieldとDatePointField

店舗の営業時間を扱うことを考えてみます。

<fieldType name="pdate" class="solr.DatePointField" docValues="true"/>
<field name="start" type="pdate" indexed="true"/>
<field name="end" type="pdate" indexed="true"/>

与えられた時刻(たとえば2020年3月1日19時)に営業している店舗を調べるには、営業開始時刻(start)と営業終了時刻(end)のフィールドを定義して、「startが2020年3月1日19時よりも前」かつ「endが2020年3月1日19時よりも後」である店舗を検索します。 (話を簡単にするためこの記事ではタイムゾーンのことは考えないことにします)

start:[* TO 2020-03-01T19:00:00Z] AND end:[2020-03-01T19:00:00Z * TO]

対して DateRangeFieldを使えば、営業時間のフィールドが1つで済みます

<fieldType name="rdate" class="solr.DateRangeField" docValues="true"/>
<field name="start_end" type="rdate" indexed="true" multiValued="true"/>

投入するデータは以下のような形式です。

[
    {
     "id":"1",
     "start_end":[
	 "[2020-03-01T18:00Z TO 2020-03-01T23:00Z]"
     ]
    }
]

以下のクエリで2020年3月1日19時に営業している店舗を検索できます。

start_end:"2020-03-01T19:00:00Z"

DateRangeFieldを使うパターンだと営業時間を複数登録するのも簡単です。

[
    {
     "id":"2",
     "start_end":[
	 "[2020-03-01T11:00Z TO 2020-03-01T14:00Z]",
	 "[2020-03-01T18:00Z TO 2020-03-01T23:00Z]",
	 "[2020-03-02T18:00Z TO 2020-03-02T23:00Z]",
	 "[2020-03-04T11:00Z TO 2020-03-04T14:00Z]",
	 "[2020-03-04T18:00Z TO 2020-03-04T23:00Z]"
     ]
    }
]

クエリは先程と同じもので大丈夫です。

start_end:"2020-03-01T19:00:00Z"

DatePointFieldでstartとendの2つのフィールドを使うパターンだと、どうでしょう?
start と end を multiValued にしても解決しません。

[
    {
     "id":"10",
     "start":["2020-03-01T11:00Z","2020-03-01T18:00Z"],
     "end":["2020-03-01T14:00Z","2020-03-01T23:00Z"]
    }
]

と登録したとして、startのどちらがendのどちらに対応しているのかが検索時に区別できないからです。したがって、別のレコードに分けなければなりません。

[
    {
     "id":"10",
     "start":"2020-03-01T11:00Z",
     "end":"2020-03-01T14:00Z"
    },
    {
     "id":"11",
     "start":"2020-03-01T18:00Z",
     "end":"2020-03-01T23:00Z"
    }
]

営業時間だけを扱っている分にはこれでも良さそうに見えますが、他の店舗情報(店舗名、住所、電話番号等)もインデックスに含めるとすると、以下のどちらかを考えなければならなくなります。

  • すべてのレコードに店舗情報を含める(すごく冗長)
  • 店舗情報と営業時間情報を分けてJOINする(処理が複雑になる)

どちらにしても、DateRangeField を使った場合のシンプルさには比べるべくもありません。

検索時のオプション

インデックス側とクエリ側のどちらも時間範囲である場合、以下の5通りの組み合わせが考えられます。

  1. 全く重ならない(例: インデックス「1時から2時」クエリ「4時から5時」)
  2. 完全に一致する(例: インデックス「1時から2時」クエリ「4時から5時」)
  3. 一部分が重なる(例: インデックス「1時から2時」クエリ「1時半から3時」)
  4. インデックス側がクエリ側に含まれる(例: インデックス「1時から2時」クエリ「0時から3時」)
  5. クエリ側がインデックス側に含まれる(例: インデックス「1時から2時」クエリ「1時20分から1時40分」)

検索クエリとしてヒットしたかどうかの判定時に、1はヒットしない、2はヒットしたでいいのですが、3,4,5は要件によって判定を変えたいことがあります。そこで、以下のオプションが用意されています。

  • Intersects(デフォルト): 2,3,4,5がヒット
  • Within: 2,4がヒット
  • Contains: 2,5がヒット

と思ったら、8.4.0の実際の挙動は以下の通りでした。

  • Intersects(デフォルト): 2,3,4,5がヒット
  • Within: 4がヒット
  • Contains: 2,5がヒット

具体的には

[2020-03-01T01:00Z TO 2020-03-01T02:00Z]

というデータに対して

{!field f=dateRange op=Within}[2020-03-01T01:00:00Z TO 2020-03-01T02:00:00Z]

というクエリがヒットしませんでした。opがContainsやIntersectsならヒットしました。Withinの場合は両端のどちらかが一致しているとヒットにはならず、インデックス側が完全に内側に無いといけないようです。

おわりに

DateRangeFieldを使うと、幅の有る時間のデータをシンプルに取り扱うことができます。「日付データ→pdate」と短絡せず、要件に合わせて使い分けるようにしたいところです。

コメント