我們先來看看Solr日期類型的一些内幕,然後讨論一下Solr日期類型存在的一些問題,最後我們看看怎麼解決現存的問題。
概述
-
DatePointField
在Solr4.x之前,我們隻有
,這類型現在用的應該比較少了,它對應DatePointField
中的Java
類型。實作上,如你所知它就是一個java.util.Date
的時間戳。是以它相當于我們用long
。LongField
-
TrieDateField
在Solr4.x之後,Solr帶來一系列的
,其中就有TrieField
。它對應TrieDateField
,Trie是一種資料結構,也叫字典樹,又叫字首樹。這種結構非常适合用于TrieLongField
區間搜尋
,這也是一種空間換時間的方式。這裡就不展開來聊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
的很多東西,其實并不然,這給我這種文檔狗帶來極大的不便。
-
格式
前面我們也提到過,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存儲的是時間戳。對于時間戳來說,并不存在時區問題,然後按使用者指定時區進行轉義即可嘛,為什麼要搞成這樣呢。
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"