はじめに
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時から2時」クエリ「4時から5時」)
- 完全に一致する(例: インデックス「1時から2時」クエリ「4時から5時」)
- 一部分が重なる(例: インデックス「1時から2時」クエリ「1時半から3時」)
- インデックス側がクエリ側に含まれる(例: インデックス「1時から2時」クエリ「0時から3時」)
- クエリ側がインデックス側に含まれる(例: インデックス「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」と短絡せず、要件に合わせて使い分けるようにしたいところです。