對文本搜索引擎的倒排索引(數據結構和算法)、評分系統、分詞系統都清楚掌握之后,本人對數值索引和搜索一直有很大的興趣,最近對Lucene對數值索引和范圍搜索做了些學習,並將主要內容整理如下:
1. Lucene不直接支持數值(以及范圍)的搜索,數值必須轉換為字符(串);
2. Lucene搜索數值的初步方案;
3. Lucene如何索引數值,並支持范圍查詢。
1. Lucene不直接支持數值搜索
Lucene不直接支持數值(以及范圍)的搜索,數值必須轉換為字符(串)——這是由倒排索引這個核心所決定,lucene要求term按照字典序(lexicographic sortable)排列。如果只是簡單的將數值轉為字符串,會帶來很多的問題:
2. Lucene搜索數值的初步方案
2.1 如直接保存11,24,3,50,按照字典序查詢范圍[24,50],會將3一起帶出來。這個問題有個簡單的解決方案,就是將字符串補全成定長的串,如000011,000024,000003,000050。這樣就能解決[000024,000050]這樣的字符范圍查詢。
2.2 建立索引的時候,term按照數字順序排序,上面的例子以3,11,24,50,搜索也能正確。
顯而易見,上述方案有“硬傷”:
2.1方案的問題是,固定多少位難以控制,補的位數多則浪費空間,少則存儲的數值范圍有限;
2.2方案的問題是,對范圍[24,50]查詢,必須要展開成25,26...50,這樣Boolean query查詢效率會低到無法接受。
3. Lucene如何索引數值,並支持范圍查詢
首先可以把數值轉換成字符串,且保持順序。也就是說如果 number1 < number2 ,那么transform(number) < transform(number)。transform就是把數值轉成字符串的函數,如果拿數學術語來說,transform就是單調的。
*注意, 數字做索引時, 只能是同一類型, 例如不可能是同一個field, 里面有int, 又有float的.
3.1 Lucene 對NumericField建索引的時候,首先把Numeric Value轉成 Lexicographic Sortable Binary然后根據某個步長(Precision Step 后面詳說)不斷右移然后轉換成 Lexicographic Sortable String建索引,本質上相當於建了一個Trie。
怎么把numeric value轉成 Lexicographic Sortable Binary 所有的Byte的詞典順序就是Numeric順序。
對於Long 二進制表示方式 http://en.wikipedia.org/wiki/Two's_complement
最高位是符號位0表示正數 1表示負數。對於正數來說低63位越大這個數越大,對於負數來說也是低63位越大(0xFFFFFFFFFFFFFFFF是-1,最大的負整數)這個數越大。所以只要把符號位取反Long就可以按字節映射成一個 Lexicographic Sortable Binary了。
對於Double 二進制表示方式 http://en.wikipedia.org/wiki/Binary64
The real value assumed by a given 64-bit double-precision datum with a given biased exponent and a 52-bit fraction is
對於正Double來說低63位越大這個數越大,對於負Double來說低63位越大這個數越小。負數情況和Long是相反的,因此對於小於0的Double把低63位取反,然后和Long相同再把符號位取反,Double就可以按字節映射成一個 Lexicographic Sortable Binary了。
對於Int和Float 32位的類型一樣道理,就不贅述了。
3.2 利用Trie的性質把RangeQuery分解成盡量少TermQuery,然后用這些TermQuery做搜索就可以了
原理就是Shift從0開始以precisionStep為步長遞增,對每一個Shift試圖找到最多兩個子Range:Lower和Upper,然后中間的Range繼續遞歸直到break發生,這時的Range成為Center Range。當Shift=n時,對於split出來的Range滿足把minBound的低Shift位全部置0和把maxBound的低Shift位全部置1后之間的所有數值都在要查詢的Range中。基本思想和樹狀數組類似。
看例子更容易明白比如[1, 10000]這個Range,通過splitRange出來的Range:
Shift: 0
Lower: [0x1,0xF], 表示從1到15
Upper: [0x2710,0x2710] 表示10000到10000
Shift: 4
Lower:[0x10, 0xF0] 表示從16(0x10)到255(0xFF)
Upper:[0x2700, 0x2700] 表示從9984(0x2700)到 9999(0x270F)
Shift: 8
Lower: [0x100,0xF00] 表示從256(0x100)到 4095(0xFFF)
Upper: [0x2000,0x2600] 表示從8192(0x2000)到9983(0x26FF)
Shift: 12
Center: [0x1000, 0x1000] 表示從4096(0x1000)到8191(0x1FFF)
一共7個Range最后一個Range是Center Range, 這7個Range也正好覆蓋了[1,10000]
addRange中會對每個split出來的Long Range的minBound和maxBoud右移Shift位然后轉成Lexicographic Sortable String,最后和建索引時一樣在前面加一個Byte表示Shift。因為Shift是以precisionStep為步長遞增的,所以splitRange出來的多個Lexicographic Sortable String Range是遞增的(Pair順序比較)。這樣查找所有屬於這些Range中的Term,只需要對這個field一直seek forward,不需要seek backward。
對於上面的例子,這7個Range轉換成Lexicographic Sortable String, 然后用這些Range去查找所有屬於這些Range范圍內的Term。
比如shift: 8
Lower: [0x100,0xF00] 表示從256(0x100)到 4095(0xFFF)
0x100,最高位變成1 成為 0x80,00,00,00,00,00,01,00 然后右移8位變成 0x80,00,00,00,00,00,01 然后每7個bit變成一個Byte成為
0x40, 00, 00, 00, 00, 00, 00,01
0xF00 同理變成0x40, 00, 00, 00, 00, 00, 00,0F。
在最前面加一個Byte表示Shift那么最終的Lexicographic Sortable String
0x100 -> 0x28,40, 00, 00, 00, 00, 00, 00,01
0xF00 -> 0x28,40, 00, 00, 00, 00, 00, 00,0F
第一個Byte 0x28表示Shift為8,0x20是偏移量,區分不同數值類型。
這樣如果要查找[256, 4095]的數值共有3840個,那么只需要查找15個Term
0x28,40, 00, 00, 00, 00, 00, 00,01 ~ 0x28,40, 00, 00, 00, 00, 00, 00,0F
整體來看[0, 10000]之間共1000個數值,最多需要查找的Term數量是55個。
[0x1,0xF] 15
[0x2710,0x2710] 1
[0x10, 0xF0] 15
[0x2700, 0x2700] 1
[0x100,0xF00] 15
[0x2000,0x2600] 7
[0x1000, 0x1000] 1
如果不做Trie樹,那么需要最多遍歷查找10000個Term。
理論上對於precisionStep=4時一個Range最多需要查找多少個Term?
根據splitRange可以看出除了最后一次Shift,前面的每次Shift最多產生兩個Range(Lower 和 Upper),最后一個Shift產生的是Center Range。
64位的數字Value最多Shift 64/4=16次。 所以最多有Lower和Upper最多各15個Range, Center 1個Range,每個Range最多覆蓋15個Term。
為什么不是16個Term?16個Term的話,這個Range的存在是沒有意義可以進位到下一個Shift。
只有一種情況是特殊的就是無法進位的時候,比如Range是[Long.MIN_VALUE, Long.MAX_VALUE] 只得到一個Center Range在Shift=60時,覆蓋了16個Term的。
所以理論上對precisionStep=4,最多需要查找的Term 31個Range * 15個Term/Range = 465
更一般的結論
n = [ (bitsPerValue/precisionStep - 1) * (2^precisionStep - 1 ) * 2 ] + (2^precisionStep - 1 )
precisionStep=8, n=3825
precisionStep=2, n=189
顯然precisionStep越小n越小,但是precisionStep越小意味着對每個Field需要index的Term越多,對64位的數值需要index的Term是64/precisionStep。
以上主要討論了LongField的搜索,對於DoubleField只是需要做一步處理就是對於小於0的Double,低63位取反,接下來和LongField完全相同流程。對於Int和Float只是數值類型從64位變成32位了,其余的都一樣。