Lucene NumericRangeQuery的初步理解

理解NumericRangeQuery, 首先需要理解Lucene是如何存儲數值類型. 文本初步探討了Int和Float兩種數值類型在Lucene中的存儲實現,數值類型的分詞原理,  最後給出NumericRangeQuery的簡單理解.


Lucene最初設計是實現全文檢索功能, 即只處理字符串. 因此, 在處理數值時, Lucene也是將數值編碼爲字符串。

將數值轉換成字符串, Lucene-5.2.0對應的實現類爲org.apache.lucene.util.NumericUtils

其編碼的方式如下:

  1. Int類型的編碼:

public static void main(String[] args){
    BytesRefBuilder act = new BytesRefBuilder();
    NumericUtils.intToPrefixCodedBytes(1, 0, act);

    BytesRef ref = act.get();
    System.out.println(ref.length);
}

可以發現NumericUtils把Int類型編碼爲6byte. 其中的1byte用於區別原數據類型爲Int還是Long,

SHIFT_START_INT  = 0x60;
SHIFT_START_LONG = 0x20;

另外的5byte表示原數. 我們知道, Int是32位, 即4byte. 爲什麼這裏需要5byte呢?

我們先思考另一個問題: 如何將Int轉碼成字符串, 並且保證其順序呢? 即如果兩個整數x,y編碼成字符串a,b 要保證: Integer.compare(x,y) = String.compare(a,b)

首先,整數的取值範圍(-2147483648,2147483647).

插一句, 除去符號位, -2147483648的補碼與0的補碼是一樣的, 實際上2147483648是溢出了的. 換個角度 -2147483648 = -0 關於0和-2147483648的編碼,可以看出,除符號位外,兩者是一樣的

public static void intToBytesRef(){

        BytesRefBuilder act1 = new BytesRefBuilder();
        NumericUtils.intToPrefixCodedBytes(Integer.MIN_VALUE, 0, act1);
        BytesRef ref1 = act1.get();
        System.out.println(ref1);

        BytesRefBuilder act2 = new BytesRefBuilder();
        NumericUtils.intToPrefixCodedBytes(0, 0, act2);
        BytesRef ref2 = act2.get();
        System.out.println(ref2.toString());
}

OK, 思路回到如何將Int轉碼成字符串, 並且保證其順序的問題. 如果我們單獨只關注正數和負數, 那麼會發現:

對於正數, 其補碼範圍爲: 0x00 00 00 01(1)到0x7f ff ff ff(2147483647), 是有序的, 保證了: Integer.compare(x,y) = String.compare(a,b).

對於負數, 其補碼範圍爲: 0x80 00 00 00(-2147483648)到0xff ff ff ff(-1), 是有序的, 保證了: Integer.compare(x,y) = String.compare(a,b).

使用python的struct包, 可以很方便地查看一個整數的補碼:

>>> from struct import *
>>> pack('>i',-2147483648)
'\x80\x00\x00\x00'
>>> pack('>i',0)
'\x00\x00\x00\x00'

如果希望直接查看32-bit的二進制碼, 如下:

>>>"".join([bin(ord(i))[2:].rjust(8,'0') for i in pack('>i', -2)])
'11111111111111111111111111111110'

還有一個問題: 從整體上, 負數得到的編碼是大於正數得到的編碼, 這就不符合Integer.compare(x,y) = String.compare(a,b). 如何處理這一情況呢?

int sortableBits = val ^ 0x80000000;

採用二進制數的異域操作, 將正整數與負整數的符號位交換一下即可. 這樣就可以保證整數編碼後的字符串整體有序了. 所以這裏取名sortableBits

接下來就回到 將Int編碼爲5-byte的問題. For that integer values (32 bit or 64 bit) are made unsigned and the bits are converted to ASCII chars with each 7 bit.即每7bit爲了一個單位.

這是因爲Lucene保存Unicode時使用的是UTF-8編碼,這種編碼的特點是,unicode值爲0-127的字符使用一個字節編碼。其實我們可以把32位的int看出5個7位的整數,這樣的utf8編碼就只有5個字節了.

到這裏, 再看NumericUtils.intToPrefixCodedBytes()的代碼就會很清晰了.

  public static void intToPrefixCodedBytes(final int val, final int shift, final BytesRefBuilder bytes) {
    // ensure shift is 0..31
    if ((shift & ~0x1f) != 0) {
      throw new IllegalArgumentException("Illegal shift value, must be 0..31; got shift=" + shift);
    }
    int nChars = (((31-shift)*37)>>8) + 1;    // i/7 is the same as (i*37)>>8 for i in 0..63
    bytes.setLength(nChars+1);   // one extra for the byte that contains the shift info
    bytes.grow(NumericUtils.BUF_SIZE_LONG);  // use the max
    bytes.setByteAt(0, (byte)(SHIFT_START_INT + shift));
    int sortableBits = val ^ 0x80000000;
    sortableBits >>>= shift;
    while (nChars > 0) {
      // Store 7 bits per byte for compatibility
      // with UTF-8 encoding of terms
      bytes.setByteAt(nChars--, (byte)(sortableBits & 0x7f));
      sortableBits >>>= 7;
    }
  }

關於shift參數, 由於是前綴編碼PrefixCodedBytes, shift用於處理前綴問題,與本文討論的主題無關, 暫不考慮.

浮點數(Float/Double)在計算機中的存儲存儲遵循IEEE-754標準. 通常我們用到的是單精度(float)和雙精度(double)這兩種,對應的字節數是4byte和和8byte. 下面以Float爲例, 來了解計算機是如何存儲浮點數. IEEE 754-1985 將存儲空間分成三個部分,從左到右(最高位到最低位)的順序依次是:符號位(sign)、exponent(指數位)、fraction(分數位)。 其中sign佔1-bit, exponent佔8-bit, fraction佔23-bit。

 對於單精度: 1-8-23 (32);對於雙精度: 1-11-52 (64) 例如單精度浮點數5.5,二進制表示如下:

------------------------------------------------
|   0 |1000 0001 |011 0000 0000 0000 0000 0000 |
------------------------------------------------
|Sign | exponent |        fraction             |
------------------------------------------------

接下來,我們逆向思考: 上面這樣的二進制數, 如何轉換纔得到5.5的呢? 首先給出計算公式:

v = (-1)^s * 2^E * M

首先處理符號位 s=0, 所以 (-1)^0 = 1 ;

然後處理指數位. 指數位單獨拎出來計算, 其值爲

>>> int('10000001',2)129

2^E = 2^(129-127) = 4 ; 爲什麼要減去127呢? 這裏的指數位採用的是biased exponent, 翻譯過來就是有偏移的指數(本來應該是129, 無端減去127, 當然偏移了). 本來指數的取值範圍爲[-127,127], 但是爲了方便計算機對不同浮點數進行大小比較, 將指數偏移127, 使得負指數也表示成了一個正數.

最後處理分數位 23-bit fraction的處理與指數位不同, 我總結的8字祕訣就是exponent看值, fraction數個. 即對於23-bit fraction從左到右, 

第 1位:  2^(-1) = 0.5 

第 2位 2^(-2) = 0.25 . . 

第10位2^(-10) = 0.0009765625

 . . 

第23位 2^(-23)= 1.1920928955078125e-07 

所以對於fraction 011 0000 0000 0000 0000 0000

f = 1*2^(-2) + 1*2^(-3) = 0.375; 
M = f + 1 = 1.375

綜上所述: 5.5 = 1 * 4 * 1.375

其實可以證明, fraction最大值近似爲1. 即2^(-1) +2^(-2) + ... + 2^(-n)的極限爲1.

對於fraction, 其值M的計算規則需要考慮exponent. 根據exponent的取值分爲3種情況: e = 0 和 e =[1,254] 和 e=255. 由於Float的exponent只有8位, 所以其最大值爲255.

e=[1,254] 是通常情況, 覆蓋了99%以上的浮點數. 我們稱之爲規格化的值, 此時 M= 1 + f 

e=0 是第一種特殊情況, 我們稱之爲非規格化的值, 此時 M = f 

e=255是第二種特殊情況, 若fraction中23-bit全是0,表示無窮大(infinite); 否則表示NaN(Not a Number)

爲了能夠多看幾個例子, 多做幾個實驗, 從而對這個轉化過程形成感覺. 用python實現了兩個簡單的函數. 一個是將浮點數轉換成二進制字符串, 一個是將二進制字符串轉換成浮點數.感謝stackoverflow貢獻了如此精妙的實現方法.

>>> import struct
>>> def float2bin(num):
...   return ''.join(bin(ord(c)).replace('0b', '').rjust(8, '0') for c in struct.pack('!f', num))
... 
>>> 
>>> def bin2float(bits):
...   return struct.unpack('f',struct.pack('I',int(bits,2)))
... 
>>> float2bin(0.1)
'00111101110011001100110011001101'
>>> float2bin(1.0)
'00111111100000000000000000000000'
>>> float2bin(0.5)
'00111111000000000000000000000000'
>>> float2bin(2.0)
'01000000000000000000000000000000'
>>>

當然, 也可以用Java查看一個Float的二進制字符串

System.out.println(Integer.toBinaryString(Float.floatToIntBits(5.5f)));

多解析幾個實例後, 就能夠理解Float的二進制存儲機制.

瞭解了Float的存儲原理後, 再學習Lucene對Float的處理方法, 就簡明很多了.

首先看一個簡單的浮點數存儲和檢索的例子

package learn.learn;

import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FloatField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.NumericUtils;

public class NumericRangeQueryDemo {
    static Directory d = null;
    public static void index() throws IOException{
        d = FSDirectory.open(Paths.get("indexfile"));
        IndexWriterConfig conf = new IndexWriterConfig(new StandardAnalyzer());
        IndexWriter iw = new IndexWriter(d, conf);
        Document doc = new Document();
        doc.add(new FloatField("f2", 2.5f, Field.Store.YES));
        iw.addDocument(doc);
        doc = new Document();
        iw.close();

    }

    public static void search() throws IOException{
        d = FSDirectory.open(Paths.get("indexfile"));
        IndexReader r = DirectoryReader.open(d);
        IndexSearcher searcher = new IndexSearcher(r);

        BytesRefBuilder act = new BytesRefBuilder();
        NumericUtils.intToPrefixCodedBytes(NumericUtils.floatToSortableInt(2.5f), 0, act);

        TopDocs n = searcher.search(new TermQuery(new Term("f2",act.get())), 2);
        System.out.println(n.totalHits);
        Document doc = searcher.doc(0);
        System.out.println(doc);

    }

    public static void main(String[] args) throws IOException {
        index();
        search();
    }
}


前面講到Lucene處理Int類型是將int轉換成6字節有序的字符串. 對於Float類型, 則是先轉換成int, 然後按int類型的方式處理. 關鍵點在於NumericUtils.floatToSortableInt() . 題外話: 理解Lucene處理數值的原理,關鍵點在於理解NumericUtils類.

分析Float型數據, 與前面分析Int型數據一樣, 正負拆開. 如果這個float是正數,那麼把它看成int也是正數,而且根據前面的說明,指數在前,所以順序也是保持好的。如果它是個負數,把它看成int也是負數,但是順序就反了. 例如:

 float2bin(-1.0) = '10111111100000000000000000000000'
 float2bin(-2.0) = '11000000000000000000000000000000'

-1.0 > -2.0 但是, '10111111100000000000000000000000' < '11000000000000000000000000000000' 因此NumericUtils.floatToSortableInt()作了相應的處理

  // Lucene-5.2.0
  public static int sortableFloatBits(int bits) {
    return bits ^ (bits >> 31) & 0x7fffffff;
  }

根據運算符優先級, 計算順序爲bits ^ ( (bits >> 31) & 0x7fffffff ); 注意這裏的位移是算術位移, 即如果bits爲負數, 則左移31位後,就變成了0xffffffff.

即 符號位不變, 正數保持, 負數翻轉. 這樣做雖然會導致 負數二進制字符串 > 正數二進制字符串 的情況出現, 但是NumericUtils.intToPrefixCoded()會做稍後的處理, 所以最終保證了 Integer.compare(x,y) = String.compare(a,b)


前面瞭解到Lucene對Int類型和Float類型處理機制如下: 1. 對於是Float類型, 將Float轉成Int, 然後按照Int類型處理. 2. 對於Int類型, 將其轉換成Sortable Int, 然後按照7-bit爲一個單位轉換成長度爲6的字節數組.

本節的目標是瞭解Lucene對數值類型進行分詞的過程. 瞭解了這一過程, 就很容易理解Lucene數值類型的查詢原理, 比如NumericRangeQuery.

我們知道, Lucene對英文分詞, 基本上就是按空格進行切分, 比如"show me the code", 分詞後的形式就是["show", "me", "the", "code"] 數值類型分詞與文本分詞不同, 比如整數1, 轉換成字節數組後,其值爲[60 8 0 0 0 1](注意數組中是16進制, 而非10進制)

// Lucene-5.2.0
public static void main(String[] args) throws IOException {
    BytesRefBuilder bytes = new BytesRefBuilder();
    NumericUtils.intToPrefixCodedBytes(1, 0, bytes);
    System.out.println(bytes.toBytesRef()); // [60 8 0 0 0 1]
}

對於[60 8 0 0 0 1], 如果按照默認的precisionStep=8, 則分詞的結果爲:

[60 8 0 0 0 1]
[68 4 0 0 0]
[70 2 0 0]
[78 1 0]

分詞的代碼爲:

public static void main(String[] args) throws IOException {
    final NumericTokenStream stream= new NumericTokenStream(8).setIntValue(1);
    final TermToBytesRefAttribute bytesAtt = stream.getAttribute(TermToBytesRefAttribute.class);
    final TypeAttribute typeAtt = stream.getAttribute(TypeAttribute.class);
    final NumericTokenStream.NumericTermAttribute numericAtt = stream.getAttribute(NumericTokenStream.NumericTermAttribute.class);
    final BytesRef bytes = bytesAtt.getBytesRef();
    stream.reset();
    for (int shift=0; shift<32; shift+=NumericUtils.PRECISION_STEP_DEFAULT_32) {
      stream.incrementToken();
      bytesAtt.fillBytesRef();
      System.out.println(bytesAtt.getBytesRef());
    }
    stream.end();
    stream.close();

}

數值分詞其實就是拆分前綴. 上面的結果不像通常理解的前綴關係,這是因爲添加了shift信息. 如果同時對多個數進行分詞, 排序後對比, 就能體會到前綴的意義了.

前綴的比特數由precisionStep決定, 對於NumericUtils.intToPrefixCodedBytes(), 對應着參數shift

  public static void intToPrefixCodedBytes(final int val, final int shift, final BytesRefBuilder bytes) {
    // ensure shift is 0..31
    if ((shift & ~0x1f) != 0) {
      throw new IllegalArgumentException("Illegal shift value, must be 0..31; got shift=" + shift);
    }
    int nChars = (((31-shift)*37)>>8) + 1;    // i/7 is the same as (i*37)>>8 for i in 0..63
    bytes.setLength(nChars+1);   // one extra for the byte that contains the shift info
    bytes.grow(NumericUtils.BUF_SIZE_LONG);  // use the max
    bytes.setByteAt(0, (byte)(SHIFT_START_INT + shift));
    int sortableBits = val ^ 0x80000000;
    sortableBits >>>= shift;
    while (nChars > 0) {
      // Store 7 bits per byte for compatibility
      // with UTF-8 encoding of terms
      bytes.setByteAt(nChars--, (byte)(sortableBits & 0x7f));
      sortableBits >>>= 7;
    }
  }

上面的代碼, 在Lucene處理Int類型數據的方法與原理一文中也貼過. 再看上面的代碼, 是否覺得清晰了許多?

前綴具有什麼優良的特性呢? 在數據結構上, 前綴屬於典型的以空間換時間策略. 即通過存儲空間的消耗,換取到極短的查詢時間. 如果學習過Trie和線段數, 樹狀數組這些數據結構, 可能會更容易理解Lucene這裏的做法.

前綴圖示

(說明,本圖來源於博客: http://blog.csdn.net/zhufenglonglove/article/details/51700898 致謝!  )

我們知道, Lucene存儲的是倒排索引, 即term ---> [docid, docid, ... ] . 假設有如下的需求: 查詢價格在[421, 448]的商品. 假如商品的價格信息如下: A=423, B=445 對於前綴索引, 其索引結構是這樣的:

423---> [A]
425 --> [A]
42  --> [A,B]
4   --> [A,B]

在查詢的時候, 只需要查詢前綴4, 就可以了.

爲了對Lucene的前綴更有感覺, 可以對一系列的整數進行分詞, 然後查看分詞的結果. 代碼如下:

    public static void tokenAnalyzer(Set<String> list , int val) throws IOException{

        final NumericTokenStream stream= new NumericTokenStream(8).setIntValue(val);
        final TermToBytesRefAttribute bytesAtt = stream.getAttribute(TermToBytesRefAttribute.class);
        final TypeAttribute typeAtt = stream.getAttribute(TypeAttribute.class);
        final NumericTokenStream.NumericTermAttribute numericAtt = stream.getAttribute(NumericTokenStream.NumericTermAttribute.class);
        final BytesRef bytes = bytesAtt.getBytesRef();
        stream.reset();
        for (int shift=0; shift<32; shift+=NumericUtils.PRECISION_STEP_DEFAULT_32) {
          stream.incrementToken();
          bytesAtt.fillBytesRef();
          list.add(bytesAtt.getBytesRef().toString());

        }
        stream.end();
        stream.close();
    }

    public static void main(String[] args) throws IOException {
        TreeSet<String> list = new TreeSet<String>();
        for(int i=1;i<512;i++){
            tokenAnalyzer(list, i);
        }
        System.out.println("size of list is "+list.size());
        for(String s: list)System.out.println(s);
    }

結果如下:

size of list is 515
[60 8 0 0 0 10]
    ...
[60 8 0 0 3 e]
[60 8 0 0 3 f]
[68 4 0 0 0]
[68 4 0 0 1]
[70 2 0 0]
[78 1 0]

如果查詢區間[1,255]的文檔信息, 則只需要查詢[68 4 0 0 0]就OK了. 如果單純地使用BooleanQuery,不構建前綴索引, 則需要拼接255個TermQuery.兩者之間的查詢性能, 可想而之.

前面說到, 前綴的缺點就是空間消耗. 這一點可以在建立索引時通過precisionStep參數來調整. precisionStep越小, 空間消耗越大, precisionStep越大, 空間消耗越小. 需要注意的是, 在業務中,並不是precisionStep越小, 查詢性能越好. 究竟precisionStep設置多大才能達到最佳的平衡點, 需要具體業務, 具體對待.


對於NumericRangeQuery的分析, NumericUtils.splitRange()是核心

搜索的樣例代碼如下:

import java.io.IOException;
import java.nio.file.Paths;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.IntField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.NumericRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

public class NumericRangeQueryDemo {
    static Directory d = null;
    public static void index() throws IOException{
        d = FSDirectory.open(Paths.get("indexfile"));
        IndexWriterConfig conf = new IndexWriterConfig(new StandardAnalyzer());
        IndexWriter iw = new IndexWriter(d, conf);
        Document doc =null;
        for(int i=0;i<512;i++)
        {
            doc = new Document();
             doc.add(new IntField("f2", i, Field.Store.YES));
             iw.addDocument(doc);
        }

        iw.close();

    }

    public static void search() throws IOException{
        d = FSDirectory.open(Paths.get("indexfile"));
        IndexReader r = DirectoryReader.open(d);
        IndexSearcher searcher = new IndexSearcher(r);

        Query  query = NumericRangeQuery.newIntRange("f2", 0, 255, true, true);
        TopDocs n = searcher.search(query, 2);
        System.out.println(n.totalHits);
        Document doc = searcher.doc(0);
        System.out.println(doc);

    }

    public static void main(String[] args) throws IOException {
        index();
        search();
    }
}

我們先不管splitRange()代碼的細節, 先根據前面理解到的知識, 來預測對於某一個[minBound,maxBound], spiltRange後在NumericRangeQuery.NumericRangeTermsEnum.rangeBounds中生成的結果是什麼?

例如:

當: precisitionStep=8, [minBound,maxBound]=[0, 16777215]時, rangeBounds=[[78 1 0], [78 1 0]]
當: precisitionStep=8, [minBound,maxBound]=[0, 65535]時, rangeBounds=[70 2 0 0], [70 2 0 0]
當: precisitionStep=8, [minBound,maxBound]=[0, 255]時, rangeBounds=[[68 4 0 0 0], [68 4 0 0 0]]
當: precisitionStep=8, [minBound,maxBound]=[0,1023]時, rangeBounds=[[68 4 0 0 0], [68 4 0 0 3]]
當: precisitionStep=8, [minBound,maxBound]=[0, 511]時, rangeBounds=[[68 4 0 0 0], [68 4 0 0 1]]
當: precisitionStep=8, [minBound,maxBound]=[0, 254]時, rangeBounds=[[60 8 0 0 0 0], [60 8 0 0 1 7e]]
當: precisitionStep=8, [minBound,maxBound]=[0, 127]時, rangeBounds=[[60 8 0 0 0 0], [60 8 0 0 0 7f]]
當: precisitionStep=8, [minBound,maxBound]=[10, 1023]時, rangeBounds=[[60 8 0 0 0 a], [60 8 0 0 1 7f], [68 4 0 0 1], [68 4 0 0 3]]

研究幾個案例後, 關於splitRange()的邏輯, 就比較有感覺了. 例如: [minBound,maxBound]=[2, 1024]

首先會處理: [2,255], [1024,1024], 生成 [60 8 0 0 0 2], [60 8 0 0 1 7f], [60 8 0 0 8 0], [60 8 0 0 8 0]

然後會處理: [256,768], 生成 [68 4 0 0 1], [68 4 0 0 3] 所以最後splitRange生成的結果是[[60 8 0 0 0 2], [60 8 0 0 1 7f], [60 8 0 0 8 0], [60 8 0 0 8 0],[68 4 0 0 1], [68 4 0 0 3]] 結束.

總體的策略是先枝葉, 後主幹.

通過上面的案例,結合前面理解的NumericTokenStream, 可以發現,在precisionStep=8時, [0,65535] 區間管理如下:

                 [0,65535]

[0,255], [256,511], ... , [62324,62579], [62580, 65535]

取值區間確定後, 當拿到的term比較多時, 一般是超過16個, 則使用bitset, 否則使用booleanQuery, 代碼邏輯見MultiTermQueryConstantScoreWrapper.createWeight(). 在MultiTermQueryConstantScoreWrapper.createWeight()創建的ConstantScoreWeight對象的rewrite()方法.

最後, 再看具體代碼的實現, 理解作者編碼的細節, 每個變量的作用.

  /** This helper does the splitting for both 32 and 64 bit. */
  private static void splitRange(
    final Object builder, final int valSize,
    final int precisionStep, long minBound, long maxBound
  ) {
    if (precisionStep < 1)
      throw new IllegalArgumentException("precisionStep must be >=1");
    if (minBound > maxBound) return;
    for (int shift=0; ; shift += precisionStep) {
      // calculate new bounds for inner precision
      /*
       * diff的作用就是將每一輪的處理控制在算精度範圍內, 以precisitionStep=8爲例: 
       * diff=2^8
       * diff=2^16
       * diff=2^24
       * 即每一次擴大8-位
       * */
      final long diff = 1L << (shift+precisionStep),
        /*
         * mask, 直譯就是掩碼, 以precisionStep=8爲例:
         * mask = 0x00000000000000ff
         * mask = 0x000000000000ff00
         * mask = 0x0000000000ff0000
         * */
        mask = ((1L<<precisionStep) - 1L) << shift;
      /*
       * hasLower/hasUpper 用於判別當前邊界是枝葉還是樹幹. 主要作用於第一輪, 即shift=0時
       * */
      final boolean
        hasLower = (minBound & mask) != 0L,
        hasUpper = (maxBound & mask) != mask;
      /*
       * nextMinBound/nexMaxBound  可以形象理解爲標記斷點
       * */
      final long
        nextMinBound = (hasLower ? (minBound + diff) : minBound) & ~mask,
        nextMaxBound = (hasUpper ? (maxBound - diff) : maxBound) & ~mask;
      final boolean
        lowerWrapped = nextMinBound < minBound,
        upperWrapped = nextMaxBound > maxBound;
      /*
       * 這下面的邏輯就是真正的剪枝了, 需要注意的是, addRange會重新調整maxBound.
       * 例如: 對於區間[0,1024], 在這裏看到的split後的區間是[0,768], [1024,1024],
       * 實際上,在addRange函數中,通過  maxBound |= (1L << shift) - 1L; 將區間修正爲
       * [0,1023], [1024,1024]
       * */
      if (shift+precisionStep>=valSize || nextMinBound>nextMaxBound || lowerWrapped || upperWrapped) {
        // We are in the lowest precision or the next precision is not available.
        addRange(builder, valSize, minBound, maxBound, shift);
        // exit the split recursion loop
        break;
      }

      if (hasLower)
        addRange(builder, valSize, minBound, minBound | mask, shift);
      if (hasUpper)
        addRange(builder, valSize, maxBound & ~mask, maxBound, shift);

      // recurse to next precision
      minBound = nextMinBound;
      maxBound = nextMaxBound;
    }
  }

參考: 

http://blog.csdn.net/zhufenglonglove/article/details/51700898

http://blog.csdn.net/debiann/article/details/23012699 

http://brokendreams.iteye.com/blog/2256239


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章