天天看點

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"      

總結一下