天天看点

Solr Date类型的哪些你不得不了解的细节

我们先来看看Solr日期类型的一些内幕,然后讨论一下Solr日期类型存在的一些问题,最后我们看看怎么解决现存的问题。

概述

  • DatePointField

    在Solr4.x之前,我们只有​

    ​DatePointField​

    ​,这类型现在用的应该比较少了,它对应​

    ​Java​

    ​中的​

    ​java.util.Date​

    ​类型。实现上,如你所知它就是一个​

    ​long​

    ​的时间戳。所以它相当于我们用​

    ​LongField​

    ​。
  • TrieDateField

    在Solr4.x之后,Solr带来一系列的​

    ​TrieField​

    ​,其中就有​

    ​TrieDateField​

    ​。它对应​

    ​TrieLongField​

    ​,Trie是一种数据结构,也叫字典树,又叫前缀树。这种结构非常适合用于​

    ​区间搜索​

    ​,这也是一种空间换时间的方式。这里就不展开来聊Trie树了。 

    TrieDateField实际上就是DatePointField在Trie上的优化,此外便没有其他更改了, 其根本还是一个Long的时间戳。

  • DateRangeField

    这个可高级了,DateRangeField主要是在区间搜索上做优化,这个优化是从更新存储结构上进行的。前面提到TrieDateField是内部的存储结构是Long,也就是时间戳。但现在不是了,他存储的是String,相当于TextField。

    想解释TrieDateField与DateRangeField之间的差异,关键是理解Trie结构。从名字上就可以知道DateRangeField更适合区间搜索的。简单的说,用TrieDateField的话,它就是一个数值,我们很难控制它的有现实意义区间,比如一天、一小时。它只能按数值上意义进行,即按几个​

    ​bit​

    ​来分区。因为很难用几个bit来描述一天、一小时的意义。

    但我们知道​

    ​DateRangeField​

    ​,它是根本是一个​

    ​字符串​

    ​,那么它就可以很轻易按我们的现实意义的东西来分区。

你可以这么理解,2017-02-14T12:36:48Z是一个TextField,然后它采用类似于EdgeNGramTokenizer分词器。所以可得到如下的分词结果:

2017 

201702 

20170214 

2017021412 

201702141236 

20170214123648

​2017​

​表示2017全年的时间区间,即是​

​2017-01-01T00:00:00Z​

​至​

​2017-12-31T23:59:59Z​

​。​

​2017年06月05日十二点​

​的数据,便可用​

​q=daterange:2017-06-05T12​

​的方式。之后我们可以很方便的检索某个单位的所有数据。当然,同时我们也可以用过检索某天,某月的数据。这些便是时间区间的概念了。后面会详细介绍。

DateRangeField所有属性与TextField雷同,它也不支持docValues=true等。而DatePointField和TrieDateField实际上就是一个Long/TrieLong,所以它支持docValues=true,可以通过它来加速Facet和Sort的效率。

二、深入理解DateField

​DatePointField​

​和​

​TireDateField​

​,还有DateRangeField另外一种日期类型。整体来说,Solr所有日期时间类型都是以一个​

​utc​

​时区存储的。对于DatePointField我的态度跟Solr文档一样,不会过多的介绍,因为它TrieDateField在结构用法上完全一样,TrieDateField仅仅只是优化区间搜索,这一点我们强调无数次了。

不要再用TrieDateField

TrieField找机会再来细说。这就是说TrieField不是适合所场景,它仅适合用区间检索,同时这个区间还不能太小。

建议大家弃用TrieDateField呢?

因为DateRangeField的出现,使得TrieDateField的存在非常尴尬。因为它的区间很难控制,毕竟TrieDateField的根本还是TrieLong嘛。

A.Solr蹩足时间日期类型

对于DatePointField和TrieDateField便是Solr蹩足时间日期类型的代表,后面DateRangeField有不小进步,但依然不行。好吧,我们还是先来看一下格式。

A.1. Solr支持哪些时间日期格式呢

Solr-Ref-Guide说了,Solr的日期遵循DateTimeFormatter.ISO_INSTANT,即是XML Schema specification中​​IOS-8601​​​。 

这种格式可以描述为​

​yyyy-MM-ddTHH:mm:ssZ​

​,这里的​

​Z​

​表示采用了​

​UTC​

​时区。

关于 DateField 有效格式有且仅有以下几种: 

1. 2017-07-06T00:00:00Z 

2. 2017-07-06T00:00:00.0Z 

3. 2017-07-06T00:00:00.00Z 

4. 2017-07-06T00:00:00.000Z

可以用​

​"​

​把日期包起来,也可以在​

​:​

​前面加一个​

​\​

​,此外都不允许。

包括solr-ref-guide提及的datefield:[1972-05-20T17:33:18.772 TO *]也非法的。

A.2. DateRangeField的一些特殊技能

DateRangeField自带一些特殊技能,它的表示方式比较丰富,除上面提及几种格式,还有如下几种: 

1. yyyy 

2. yyyy-MM 

3. yyyy-MM-dd 

4. yyyy-MM-ddTHH 

5. yyyy-MM-ddTHH:mm 

6. yyyy-MM-ddTHH:mm:ss

Solr-Ref-Guide对DateRangeField更是不得了,简直是开了挂了。但事实并没有那么的美好,接下来我们就看看这些黑洞。

yyyy-MMTHH 其实是不可以的 

文档对​

​yyyy-MMTHH​

​的说明是这样的​

​Likewise but for an hour of the day​

​。由于文档用了​

​the day​

​和自己的实验结果,我认为文档写错了。应该是​

​yyyy-MM-ddTHH​

​,在DateRangeField,Solr把它解释为’yyyy-MM-dd`,这验证了我们的对DateRangeField存储的说法,以及它的分词方式。RangeQuery时,它就必须是一个时间点。当出现在时间区间的下限时,它是​

​2017-05-20T00:00:00Z​

​,如果出现在时间区间的上限时,它的意义是​

​2017-05-20T23:59:59Z​

​。

DateRangeField还支持下面几种区间检索。 

1. dateRange:[2017 TO 2017] —— 等同于 dateRange:2017。 

2. dateRange:[2017 TO 2017-05] —— 等同于 dateRange:[2017-01-01T00:00:00Z TO 2017-05-31T24:00:00Z] 

3. dateRange:[2017-05 TO 2017] —— 等同于 dateRange:[2017-05-01T00:00:00Z TO 2017-12-31T24:00:00Z] 

等等,可以自行组合。

B.开挂指令,DateMathParser

​DateMathParser​

​提供。

先来看一下,DateMathParser内置的一些关键字:(必须是大写) 

NOW 

YEAR 

MONTH 

DAY 

DATE 

HOUR 

MINUTE 

SECOND 

MILLI 

MILLISECOND 

TZ

注,所有时间单位都可以带S,也可以不带,意义一样。

DateMathParser基本可以分为两类: 

- 取整

​零​

​,比如自然月,自然日等。​

​2017-05-20T23:32:33Z​

​,那么即是​

​2017-05-20T00:00:00Z​

​,​

​2017-05-20T23:00:00Z​

​。​

​/​

​并不是我们数学意义上的​

​除​

​,它是取整,相当于数学意义上的​

​A/B*B​

​。

  • 加减

除了取整之外,还有另一个非常实用的功能便是时间前后推移了。 

NOW-1DAY,往后推移一天。如果时下是​

​2017-05-20T23:32:33Z​

​,由Solr计算后便得到​

​2017-05-19T23:32:33Z​

​;当然后若是​

​NOW+1DAY​

​便会得到​

​2017-05-21T23:32:33Z​

​。

这两类计算都非常好理解,也非常好用。

​2017-05-20T23:32:33Z​

​,我想想看看今天零点到在数据时,我们可以直接用​

​NOW/DAY​

​即可。 

但如果我想搜索昨天零点到今天零点的数据,应该怎么办呢?对就是​

​datetime:[NOW/DAY-1DAY TO NOW/DAY]​

​,便能得到​

​datetime:[2017-05-19T00:00:00Z TO 2017-05-20T00:00:00Z]​

​。

若仅仅如此,那你也太小看看我们大Solr了。Solr当然必须要能支持取整和加减的混合运算的啊。

Solr的时间计算都把时间转成时间戳进行计算的,因此计算结果必然是一个某的时刻,而非一个时间区间;在浏览器测试时,还需要要注意把​

​+​

​转义。

B.1 关键字 NOW

NOW可以指定自己的时间,用来修正当前时间。它仅支持时间戳,且精确到毫秒。也就是说它用来代替计算公式中NOW的含义的,当搜索时并没有采用时间计算公式时,它没有什么任意意义,当然也不会报错的。

B.2 关键字 TZ

它仅仅只能修正DateMathParser在计算时的时间区,比如当​

​q=daterange:NOW​

​时,当有​

​TZ=Asia/Shanghai​

​时,它表示北京时间。否则NOW会表示为UTC时间。

它并不能修正Solr输出、输入时区。

C.接下来我们来看看Solr日期的那些坑

​SOLR-Ref-Guide​

​中提及关于​

​Working with Dates​

​的很多东西,其实并不然,这给我这种文档狗带来极大的不便。

  1. 格式

    前面我们也提到过,Solr的日期时间格式的限制是非常苛刻的,并非像文档所介绍的那样。

  2. DateRangeField的搜索格式也有问题

    虽然介绍过,再提一次。

​q=dateRange:2017-06T12​

  1. 这格式并不支持。这种格式,当然在DateRangeField,Solr会把它解释为

​2017-06-12​

  1. ,不过其它日期时间类型并不支持了,这也又验证我们对DateRangeField分词解释了。
  2. 对于格式

​NOW+6MONTHS+3DAYS/DAY​

  1. 的解释

    Solr文档对

​NOW+6MONTHS+3DAYS/DAY​

  1. 的解释很大高上,然并没有。这货有点难理解,其实它等同于

​NOW/DAY+6MONTHS+3DAYS​

  1. 所以啊,我建议大家在使用DateMathParser的时候,尽量不要搞事情,不要写这些奇葩计算公式。

  2. 为什么Solr输出输入都用UTC时区呢?

    我以为这是Solr最坑的地方了,实现对TrieDateField/DateField,Lucene存储的是时间戳。对于时间戳来说,并不存在时区问题,然后按用户指定时区进行转义即可嘛,为什么要搞成这样呢。

​yyyy-MM-ddTHH:mm:ssZ​

​的形式。这样,你就可以采用你机器的时区,但这样便有歧义了,不建议你这么用。

C.如何解决Solr时区问题

想要优雅解决这个问题其实并不难,自己自定义一个FieldType即可。 

简单的说,对于DatePointField/TrieDateField的话,你只需要Copy对应的DateField代码,然后把​

​toExternal(IndexableField f)​

​中的​

​Date#toInstant()​

​更改为​

​DateFormat.parse()​

​即可。

  • 对TrieDateField和DatePointField

    看一下

​TrieField​

  • 实现的源码吧,了解java date的同学一眼就能看问题的所在,即toInstant是一定是UTC时区的,因此我们需要覆盖它的实现即好。
@Override
public String toExternal(IndexableField f) {
    return      

​toExternal​

​重新实现了。

package cn.dmsolr.schema;

import ...

public class TrieDateField extends TrieField implements DateValueFieldType
    {
        this.type = NumberType.DATE;
    }

    @Override
    public Date toObject(IndexableField f) {
        return (Date)super.toObject(f);
    }

    @Override
    public Object toNativeType(Object val) {
        if (val instanceof String) {
            return DateMathParser.parseMath(null, (String)val);
        }
        return super.toNativeType(val);
    }

    @Override // 关键代码
    public String toExternal(IndexableField f) {
        final DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        format.setTimeZone(SolrRequestInfo.getRequestInfo().getClientTimeZone());
        return      
  • 对于DateRangeField

    再来看看DateRangeField而言,更简单,即是把所有含

​Z​

  • 都删除即可。实现Solr在存储时不带存储

​Z​

<fieldType name="dm_pdate" class="cn.dmsolr.schema.DatePointField" docValues="false"
<fieldType name="dm_tdate" class="cn.dmsolr.schema.TrieDateField" docValues="false"
<fieldType name="dm_range" class="cn.dmsolr.schema.DateRangeField" docValues="false"      

总结一下