3.4         Lucene工具箱之OpenBitSet

    

Lucene中,DocId具有这样的特征:唯一/递增。而且在搜索的过程,不同term之间的DocId集合进行逻辑运算的需求非常之多。OpenBitSet正是集合运算的利器。

3.4.1             OpenBitSet的原理

假设有一个byte,一共有8个二进制位,如下图:

0


0


0


0


0


0


0


0


7


6


5


4


3


2


1


0


如果每个二进制位表示个数,这个Byte可以存储[0,7]8个数。

比如存储46这两个数,则byte中各个二进制位的状态如下:

0


1


0


1


0


0


0


0


7


6


5


4


3


2


1


0


用二进制位的下标表示存储的数,并在将二进制位的相应状态设为1OpentBitSet正是利用上述原理来存储数据。

3.4.2 OpenBitSet的简单应用

假设有两个集合A = {134105},B={5328}。计算A集合与B集合的并集;计算A集合与B集合的交集。

                   int[] a = {1,3,4,10,5};

       int [] b = {5,3,2,8};

       OpenBitSet setA = new OpenBitSet();

       for(int i : a) setA.set(i);

       OpenBitSet setB = new OpenBitSet();

       for(int i : b) setB.set(i);

      

       OpenBitSet unionSet = setA.clone();

       unionSet. union(setB); //AB的并集

       DocIdSetIterator iterator = unionSet.iterator();

       while(iterator.nextDoc()!=DocIdSetIterator.NO_MORE_DOCS){

           System.out.print(iterator.docID()+", ");

       }System.out.println();

      

       OpenBitSet intersectionSet = setA.clone();

       intersectionSet. intersect(setB);//AB的交集

       iterator = intersectionSet.iterator();

       while(iterator.nextDoc()!=DocIdSetIterator.NO_MORE_DOCS){

           System.out.print(iterator.docID()+", ");

       }System.out.println();

输出结果如下:

    并集:1, 2, 3, 4, 5, 8, 10,

交集:3, 5,

3.4.3 OpenBitSet的源码分析

    OpenBitSet利用二进制位来存储数据,一个long类型最高只有64位,能存储63个数。

    如果存储[0,63]之间的数,需要1long类型串联起来。

如果存储[0,127]之间的数,需要2long类型串联起来。

如果存储[0,191]之间的数,需要3long类型串联起来。

……

如果存储[0,(64N+m)] (N,m为非负整数,m<64)之间的数,需要Nlong类型串联起来.

所以OpenBitSet的核心就是一个long类型的数组bits

public class OpenBitSetextendsDocIdSet implements Bits, Cloneable {

  protectedlong[]bits;

这个数组需要开多大呢?依据存储数据的最大值而定。OpenBitSet有构造函数如下:

  publicOpenBitSet() {

    this(64);

  }

这个构造函数调用了另一个需要传参的构造函数:

  /** Constructs an OpenBitSet large enough to hold numBits.

   */

  publicOpenBitSet(long numBits) {

    this.numBits = numBits;

    bits = new long[bits2words(numBits)];

    wlen= bits.length;

  }


该构造函数中调用了bits2words()方法来通过传入的参数计算bits数组的大小。

tits2words(64) = 1;表示存储[0,63]之间的数需要1long类型。

tits2words(256)=2;表示需要存储[0,255]之间的数需要2long类型。

依此类推……

这样传参避免我们人工计算bits数组的大小,也封装了实现原理。

OpenBitSet的数据存储

首先要清楚的是,在OpenBitSet中:

[0,63]存储在bits[0]64个位中

[64,127]存储在bits[1]64个位中

……

[64N,64N+63]存储在bits[N]64个位中

任何一个非负整数,都可以表示成:64*N+m (N,m都是非负整数,m<64)。其中N表示bits数组的下标,m表示bits[N]64个位中需要把状态置为1的二进制位的下标。

存储数据的原代码如下:

  /** sets a bit, expanding the set size if necessary */

  publicvoidset(longindex) {

    int wordNum = expandingWordNum(index);

    int bit = (int)index & 0x3f;

    long bitmask = 1L << bit;

    bits[wordNum] |= bitmask;

  }

整个set方法有4句代码,我们一句一句分析:

1句代码求公式64*N+m中的N。参数index除以64或者 index>>6就可以了。左移6位即除以2^6=64.

2句代码求公式64*N+m中的m。注意0x3f= 64 =(111111)2index%64 即为 index & 0x3f

    3句和第4句即把bits[N]的第m位设置1

    最后总结一下OpenBitSet数据存储的特点:OpenBitSet无法存储重复的数据。数据存储到OpenBitSet中后就是有序的了。OpenBitSet适合存储密集程度高,且量大的数据。OpenBitSet中存储的数据适合位运算,比如取交集、并集、补集……

    由于直接从word中粘贴来受到了长度的限制,我又不想在博客编辑器中重新写一遍,所以关于OpenBitSetIterator相关的内容和Lucene4.2的其它细节可以从我的《Lucene4.x源码解读》第4章4.3节中了解。OpenBitSetIterator分析了bitList的实现原理。

    《Lucene4.x源码解读》会不定时更新,可以关注我的新浪微博 @帅广应s 。