1. 程式人生 > >Solr Date型別的哪些你不得不瞭解的細節

Solr Date型別的哪些你不得不瞭解的細節

我們先來看看Solr日期型別的一些內幕,然後討論一下Solr日期型別存在的一些問題,最後我們看看怎麼解決現存的問題。
概述

    DateField

    在Solr4.x之前,我們只有DateField,這型別現在用的應該比較少了,它對應Java中的java.util.Date型別。實現上,如你所知它就是一個long的時間戳。所以它相當於我們用LongField。在高版本的Solr已經看不到這個類了。

    TrieDateField

    在Solr4.x之後,Solr帶來一系列的TrieField,其中就有TrieDateField。它對應TrieLongField,Trie是一種資料結構,也叫字典樹,又叫字首樹。這種結構非常適合用於區間搜尋,這也是一種空間換時間的方式。這裡就不展開來聊Trie樹了。

    DateRangeField

    這個可高階了,DateRangeField主要是在區間搜尋上做優化,這個優化是從更新儲存結構上進行的。前面提到TrieDateField是內部的儲存結構是Long,也就是時間戳。但現在不是了,他儲存的是String,相當於TextField。

    DatePointField

    出生於Solr6.5,是一種新的結構PointField,此結構與TrieField類似。DatePointField實際上就是TrieDateField在Trie上的優化,此外便沒有其他更改了, 其根本也還是一個Long(LongPointField)的時間戳。

    想解釋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

在Solr的世界裡,其實除了有你熟悉的DatePointField和TireDateField,還有DateRangeField另外一種日期型別。整體來說,Solr所有日期時間型別都是以一個utc時區儲存的。對於DatePointField我的態度跟Solr文件一樣,不會過多的介紹,因為它TrieDateField在結構用法上完全一樣,TrieDateField僅僅只是優化區間搜尋,這一點我們強調無數次了。

    不要再用TrieDateField

我們前面說過了,TrieField是以空間換時間的一種方式,TrieField優化了區間檢索的效能。關於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儲存的說法,以及它的分詞方式。

很多情況下,DateRangeField表示就是一個時間區間。如,2017-05-20,正常來說它就是一個時間區間。但是在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基本可以分為兩類:
- 取整

取整即是取指定單位後面的數值置零,比如自然月,自然日等。

例如:NOW/DAY, NOW/HOURS表示,取當天零點零分;取當時零分。如果時下是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

TZ,TimeZone的縮寫,它的作用非常單一。它僅僅只能修正DateMathParser在計算時的時間區,比如當q=daterange:NOW時,當有TZ=Asia/Shanghai時,它表示北京時間。否則NOW會表示為UTC時間。

它並不能修正Solr輸出、輸入時區。
C.接下來我們來看看Solr日期的那些坑

首先官方給出來文件SOLR-Ref-Guide中提及關於Working with Dates的很多東西,其實並不然,這給我這種文件狗帶來極大的不便。

    格式
    前面我們也提到過,Solr的日期時間格式的限制是非常苛刻的,並非像文件所介紹的那樣。
    DateRangeField的搜尋格式也有問題
    雖然介紹過,再提一次。q=dateRange:2017-06T12這格式並不支援。這種格式,當然在DateRangeField,Solr會把它解釋為2017-06-12,不過其它日期時間型別並不支援了,這也又驗證我們對DateRangeField分詞解釋了。
    對於格式NOW+6MONTHS+3DAYS/DAY的解釋
    Solr文件對NOW+6MONTHS+3DAYS/DAY的解釋很大高上,然並沒有。這貨有點難理解,其實它等同於NOW/DAY+6MONTHS+3DAYS。
    所以啊,我建議大家在使用DateMathParser的時候,儘量不要搞事情,不要寫這些奇葩計算公式。
    為什麼Solr輸出輸入都用UTC時區呢?
    我以為這是Solr最坑的地方了,實現對TrieDateField/DateField,Lucene儲存的是時間戳。對於時間戳來說,並不存在時區問題,然後按使用者指定時區進行轉義即可嘛,為什麼要搞成這樣呢。

其實DateRangeField時,儲存的資料是字串。理論上,Solr並不需要強制使用UTC的時間的。即是你可以在提交文件時,可能自己先轉成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 (type == NumberType.DATE)
        ? ((Date) toObject(f)).toInstant().toString()
            : toObject(f).toString();
}

    1
    2
    3
    4
    5
    6

下面是TrieDateField的原始碼,同時我在最後加上我們對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 format.format((Date) toObject(f));
    }
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    對於DateRangeField
    再來看看DateRangeField而言,更簡單,即是把所有含Z都刪除即可。實現Solr在儲存時不帶儲存Z。

上面我們已經怎麼去擴充套件和實現自己的SchemaField了,接下來就是怎麼用了。首先需要把上面的程式碼打成一個jar包,然後在solrconfig.xml把引用進來,然後在schema.xml加下面這行程式碼:

<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" />

    1
    2
    3

總結一下

TrieDateField不要再用了,需要用區間檢索請採用DateRangeField,若不需要區間檢索那就好好用DateField吧;Z表示UTC時區,因此我們只能自己去擴充套件DatePointField了。這也是為什麼Solr輸出都是UTC時間,因為都帶用Z,所以我們自定義了DateField;又介紹使用過程需要用的一些小細節。等等

當你熟悉這些細節之後,才玩轉Solr,而不是被Solr玩轉了。