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玩轉了。