`

【Lucene3.0 初窥】索引创建(5):索引数据池及内存数据细节

阅读更多

上接《索引创建 (2):DocumentWriter处理流程三

 

1.4 索引数据池存储细节

 

倒排索引(token->posting list)表的数据信息在内存中并不是直接存储在postingsHash中的,而是存放在三大数据缓冲池中——CharBlockPool,ByteBlockPool,IntBlockPool 这三个池均都由若干个固定长度的buffer数组构成。DocumentsWriter对它们进行管理和维护(包括分配新的块或者回收不用的块的操作),以达到节省内存和提高效率的作用。

 

◆ 三大索引数据池

 

1、 token字符数据缓冲池—— CharBlockPool

 

    CharBlockPool 用于存储token的字符串信息。比如 token="lucene"。其在内存中的表示其实就是多个buffer[]组成的缓冲池buffers[][]。缓冲池初始大小为10*16384个 字符空间,每一次可扩展1.5倍。

final class CharBlockPool {
  //缓冲池buffers,初始化时缓冲池由10个buffer数组组成,每一个buffer的大小为固定16384(2^14)
  public char[][] buffers = new char[10][];
  //缓冲池中buffer的数量
  int numBuffer;
  //目前我们所使用的第bufferUpto号buffer数组,初始值-1表示缓冲池还没有开辟任何buffer容量
  int bufferUpto = -1;  
  //表示当前buffer中可以使用的位置序号
  public int charUpto = DocumentsWriter.CHAR_BLOCK_SIZE;            
  //表示当前缓冲池中可以使用的buffer数组
  public char[] buffer; 
  //表示当前buffer头位置的偏移 
  public int charOffset = -DocumentsWriter.CHAR_BLOCK_SIZE;    
  
  final private DocumentsWriter docWriter;

  public CharBlockPool(DocumentsWriter docWriter) {
    this.docWriter = docWriter;
  }
  //回收缓冲池
  public void reset() {
	//通过DocumentWriter回收缓冲池的全部buffer数组
    docWriter.recycleCharBlocks(buffers, 1+bufferUpto);
    bufferUpto = -1;
    charUpto = DocumentsWriter.CHAR_BLOCK_SIZE;
    charOffset = -DocumentsWriter.CHAR_BLOCK_SIZE;
  }
  //在缓冲池中开辟新的buffer
  public void nextBuffer() {
	//当前开辟的buffer数量已经达到了缓冲池buffers允许的最大buffer量。
    if (1+bufferUpto == buffers.length) {
    	//扩大1.5倍缓冲池buffers容量,也就是最大允许的buffer数量扩大1.5倍
        char[][] newBuffers = new char[(int) (buffers.length*1.5)][];
        //拷贝原缓冲池数据
        System.arraycopy(buffers, 0, newBuffers, 0, buffers.length);
        buffers = newBuffers;
    }
    //则通过DocumentWriter分配一个新的buffer数组
    buffer = buffers[1+bufferUpto] = docWriter.getCharBlock();
    bufferUpto++;
    charUpto = 0;
    charOffset += DocumentsWriter.CHAR_BLOCK_SIZE;
  }
}

 

2、 token的docID,词频和位置信息缓冲池—— ByteBlockPool

 

    ByteBlockPool 用于存储token所在的文档,词频和位置信息。其内存结构与CharBlockPool相同,也是一个buffers[][]二维数组。但是ByteBlockPool缓冲池中的buffer采用了分片(slice)分配方式。每片(slice)的大小由nextLevelArray和levelSizeArray来确定。

//表示当前层的下一层是第几层,第9层的以后所有层都是第9层,也就是说最大的slice容量为200
final static int[] nextLevelArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};
//表示该层的大小,也就是slice的容量
final static int[] levelSizeArray = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};
//第一个开辟的slice的初始层次为0,初始容量为5
final static int FIRST_LEVEL_SIZE = levelSizeArray[0];

举个例子说明上面的层次与slice开辟的空间大小的关系。假如在buffer中开辟第一个slice,那么这个slice初始为第0层,容量为levelSizeArray[0]=5 bytes。那么下一个slice的大小怎么确定呢?当前已经开辟的slice在第0层,那么下一个slice就是在第nextLevelArray[0]层,即第1层。其大小为levelSizeArray[1]=14bytes。依次类推:第三个slice的大小为20bytes.... 第九个slice以后的大小全部都是200bytes。

 

我们下面来看看ByteBlockPool分片分配的源代码:

final class ByteBlockPool {

  //在buffer中开辟一个指定大小的slice片
  public int newSlice(final int size) {
	//buffer[]的大小为BYTE_BLOCK_SIZE=32768(2^15)
    //如果buffer中空闲空间不够slice的大小,则开辟一个新的buffer
    if (byteUpto > DocumentsWriter.BYTE_BLOCK_SIZE-size)
        nextBuffer();
    //存储本次创建的slice在buffer中的初始位置
    final int upto = byteUpto;
    //存储buffer中空闲区域的初始位置
    byteUpto += size;
    //slice与slice之间的分割标志,即块以16结束
    buffer[byteUpto-1] = 16;
    return upto;
  }


  //在buffer中分配一个新的片(slice),此方法仅仅在upto已经是当前块的结尾的时候方才调用来分配新片。
  public int allocSlice(final byte[] slice, final int upto) {

    //可根据上一个slice的结束符来得到要分批的新slice所在的层数,以确定slice的大小
    //从而我们可以推断,每个层次的slice都有不同的结束符,第1层为16,第2层位17,第3层18,依次类推。
    final int level = slice[upto] & 15;
    //获得新的层数,从而得到新的片的大小
    final int newLevel = nextLevelArray[level];
    final int newSize = levelSizeArray[newLevel];
    //如果buffer中空闲空间不够slice的大小,则开辟一个新的buffer
    if (byteUpto > DocumentsWriter.BYTE_BLOCK_SIZE-newSize)
      nextBuffer();
    
    final int newUpto = byteUpto;
    final int offset = newUpto + byteOffset;
    byteUpto += newSize;

    //将上一个slice的倒数第2-4个字节复制到新slice中的前3个字节,然后把新开辟的slice的初始位置记录在上一个slice的最后四个字节中
    //具体的做法我们将用下面细节中例子详细阐明
    buffer[newUpto] = slice[upto-3];
    buffer[newUpto+1] = slice[upto-2];
    buffer[newUpto+2] = slice[upto-1];
    slice[upto-3] = (byte) (offset >>> 24);
    slice[upto-2] = (byte) (offset >>> 16);
    slice[upto-1] = (byte) (offset >>> 8);
    slice[upto] = (byte) offset;
        
    // 在新slice的结尾写入新的结束符,结束符和层次的关系就是(endbyte = 16 | level)
    buffer[byteUpto-1] = (byte) (16|newLevel);
    // 返回新slice可以使用的容量在buffer中的初始位置序号
    // newUpto+3 是因为新slice的前三个字节存储了上一个slice的倒数2-4个字节中的数据。
    return newUpto+3;
  }
}

 

3、 ByteBlockPool 片地址信息缓冲池—— IntBlockPool

 

       IntBlockPool的职责就是用来记录ByteBlockPool中slice在buffer中的位置序号。每一个token都会有两种信息同时存储在ByteBlockPool的slice中。也就是说每一次ByteBlockPool都会同时分配两个相同大小的slice,一个用来存储docID+词频;另一个用来存储位置信息。而这两个块的初始位置序号都会同时记录在IntBlockPool中的。

 

       IntBlockPool在内存中的结构与CharBlockPool完全一样,这里就不详细介绍了。

 


索引存储的数据细节

 

在《索引创建(4):DocumentWriter 处理流程三 》中,我们对存储在三大数据池中的数据格式还没有深刻的了解。在这里,我们要详细探究倒排索引表中的数据信息是如何存储在这些数据池中的?这些重要信息包括:token字符串、所在文档词频、所在文档的docID、所在文档的位置。

 

我们用一个例子来说明整个过程:

DOC1:    lucene lucene lucene lucene lucene term . 

DOC2:    lucene lucene lucene lucene lucene term term.

DOC3:    term term term lucene lucene lucene lucene lucene.

DOC4:    term

然后我们一个一个token的加入倒排索引,观察PostingList结构和三大数据池的变化

 

1、加入DOC1中的第一个lucene (docID=0, postion=0)

      由于lucene是第一个token,索引中原来没有这个词,因此会在CharBlockPool中的buffer[]上分配6个char来存放"lucene"字符串,并将CharBlockPool中分配的6个char空间的首地址偏移记录在P.testStart中。

 

      在ByteBlockPool中分配两个slice,每个slice最初大小为5,以byte=16结束。第一个slice用来存放docID+freq信息,第二个slice用来存放position信息。但是有两点要注意一下:

      (1) 当前token的docID+freq并不会立刻写进第一个slice。只是在内存中进行freq++而已。直到处理下一篇文档DOC2中出现了相同的token的时候,才会把上一篇DOC1中该token的docID+freq写入(因为在当前DOC1文档中,是否后面还会继续加入相同token,只有当前文档结束以后才好统计词频)。

       (2) 当前token的position信息表明了token所在文档的词语位置。但是存入ByteBlockPool的第二个slice中的数据时经过差值存储技术(该技术涉及到存储优化,将在后续中讲到)的。也就是说假如当前token的position值:curPos=1。而在上一次出现了相同的token的position值:lastPos=0(这个值记录在P.lastPosition中)。则当前token记录在ByteBlockPool的position数据为:prox=(curPos-lastPos)<<1=2。

        这两点要明确,后面不再过多讲述。如果是第一次出现的token,则存储在ByteBlockPool第二个slice的prox值为:0<<1=0

 

        在IntBlockPool中分配两个int空间,一个存储ByteBlockPool第一个slice的位置,目前值为0。因为当前没有docid+freq信息写入。第二个存储ByteBlockPool的第二个slice的初始位置+1,即值为6。因为第5个位置写入了当前token的prox信息。所以IntBlockPool中存放的是下一个要写入的位置。

 

        加入第一个lucene之后,要注意p.lastPosition记录了此次token的position值。一遍一下次加入相同的token的时候,方便差值存储下一个position值。

 

 

2、加入DOC1中的第二个lucene (docID=0, postion=1)

 

倒排索引结构postingHash中已经有了lucene,因此第二次加入的lucene将不会在CharBlockPool中重新分配空间。而只是让freq++(但还是不写入ByteBlockPool,因为还没有进入下一篇DOC)。并且在ByteBlockPool第二个slice中的IntBlockPool[1]=6位置上加入第二个lucene的prox=(position-lastPosition)<<1=2。然后让IntBlockPool[1]++。指向下一次prox信息记录的空闲空间。最后lastPosition=1。记录下此次加入相同token的位置。

 

3、继续加入DOC1中的后续token,直到加入完第四个lucene (docID=0, position=3)

 

 

4、加入DOC1中第五个Lucene(docID=0, position=4)

 

此时我们发现,在加入第四个Lucene之后。IntBlockPool[1]=9指向了ByteBlockPool的第二slice结束符16。说明ByteBlockPool开辟的片已经不够用了。此时会调用ByteBlockPool.allocSlice()方法,分配一个新的slice,容量大小为14(levelSizeArray[1])。然后将原来slice中连同结束位在内的四个byte作为指针(下图绿色部分),指向新的slice的地址(下图第10个位置)。即把6,7,8三个位置上的值记录在10,11,12上,第9个位置上原来的分隔符16赋值为新slice的初始地址10。新加入的第5个lucene的prox将赋值在第13个位置上。

 

很显然,下图绿色部分的内容实际上就是新开辟的slice的初始地址。目前的缓存数据很小,只需要最后一个byte就可以存储。如果新开辟的slice初始位置数据很大,怎可能就需要4个byte了。比如,绿色部分存储的值为[0,0,1,1],则表示的值为100000001=257。即新开辟的slice初始位置在buffer[257]上。

 

5、加入DOC1中第一个 term (docID=0, position=5)

 

此时添加的"term"是一个新词,因此在postingHash中需要开辟一个新的PostingList用于存放"term"的相关信息。而且同时在CharBlockPool、IntBlockPool、ByteBlockPool中都开辟了新的空间(过程与第一次加入“lucene”相同)。另外、term的prox=position<<1=10(“term"第一个加入的新词,因此没有lastPosition)。

 

 

6、加入DOC2中第一个 lucene (docID=1, position=0)

 

      此时,当出现了DOC2中的lucene的时候,说明DOC1的所有”lucene“都处理完了,这个时候就可以把DOC1中"lucene"的词频和docID(在此之前一直都存放在内存中"lucene"的PostingList对象中)全部写入ByteBlockPool中的IntBlockPool[0]所指的位置了(注意到下图红色箭头标注ByteBlockPool的第一个slice中的数据)。

 

     PostingList的很多数据都发生了改变。其中docFreq=1开始记录在新的文档中”lucene“的频率。lastDocCode是DOC2文档号将要存储在ByteBlockPool中的数据(lastDocCode=docID<<1)。

 

     IntBlockPool也发生了变化,指向ByteBlockPool中存储"lucene"的两个slice的地址偏移也发生了变化。其中IntBlockPool[0]指向了新的偏移位置2。也就说其他文档出现lucene的时候,这个位置可以存储DOC2文档的lucene的DocCode+freq。

 

      ByteBlockPool在"lucene"的第一个slice中存储了DocCode和Freq(这里存储的不是docID,而是DocCode=docID<<1)。

 

 

7、随后添加DOC2中的四个“lucene”后,开始 加入DOC2中第一个“term” (docID=1, position=5)

 

DOC2文档的前五个"lucene"的prox已经写入了ByteBlockPool的14-18位置,值为{0,2,2,2,2}。

当开始加入DOC2的第一个"term"的时候,情况和上面第一次加入“lucene”的一样(参见第6点)。会将DOC1中的"term"的docID(docCode=0<<1=0)+freq写入ByteBlockPool的第24,25的位置上。而当前DOC2的"term"的prox=position<<1=10(因为在DOC2中"term"还是第一次出现)写入第30个位置上。


8、加入DOC3的第一个"term" (docID=2, position=0)

 

此时需要把DOC2的“term”的docID+freq写入第26、27位置上。并将当前DOC的"term"的prox=possion<<1=0写入第32个位置。注意写入DOC2的docID数据值应该是docCode=docID<<1=1<<1=2

 

 

9、加入DOC3的第二个"term" (docID=2, position=1)

 

发现ByteBlockPool在IntBlockPool[3]=33处位置上的数据位slice分隔符16,说明当前"term"的第二个slice已满,需要分配一个新的slice。这个过程与"lucene"(上面第4点)相同。新扩大的slice的层次为2,大小为14,结束符为17。而且需要将原slice的最后四位(30-33)变成指针位(指向34),其中30-32位移动到34-36位处。

 

当前DOC3的第二个"term"的prox=position-lastPosition=1<<1=2,写入第37号位置。

 

 

10、随后加入DOC3的第一个"lucene" (docID=2, position=3) ,直到加入DOC3的第四个"lucene" (docID=2, position=6)

 

此时在加入DOC3的第一个"lucene"的时候就已经把DOC2中"lucene"的docID+freq写入第2、3个位置上,注意docCode=docID<<1=1<<1=2。然后将当前DOC3的第一个"lucene"到第四个"lucene"的prox={6,2,2,2}全部写入第19-22个位置上。

 

11、加入DOC3的第五个"lucene" (docID=2, position=7)

 

此时ByteBlockPool在IntBlockPool[1]=23的位置上数据是分隔符17,表明"lucene"用于存储position的第二个slice已经用完,需要重新分配一个新的slice: 层次为3,大小为20,结束符为18,即下图从48到67的位置。原来slice的20-23的位置变成了指针位(指向新slice的48),20-22的数据移动到了48-50位上,而且当前DOC3的第五个"lucene"的prox=2记录到了第51个位置上。

 

12、加入DOC4的第一个"term" (docID=3, position=0)

 

此时加入DOC4的第一个"term",使得ByteBlockPool在IntBlockPool[2]=28的位置上写入DOC3的"term"的docID=2,freq=3。但是ByteBlockPool在第28个位置上的值为分隔符16,表明"term"的第一个slice用完,需要分配一个新的slice:层次为2,大小为14,结束符为17,即从68到81的位置。从25到28的四个byte组成一个int指针指向新分配的slice的首地址68。将25-27上的原数据赋值给68-70中,然后将DOC3的"term"的docCode=docID<<1=4和freq=3写入第71,72的地址上。

 

然后将当前DOC4的"term"的prox=position<<1=0写入第39的位置上。

 

 

总结

 

      经过对Doc1和Doc2建立索引过程中的PostingList和CharBlockPool、IntBlockPool、ByteBlockPool的数据变化我们可以很清楚的看出。Lucene的倒排索引结构在内存中并不是我们想象的那样:每个token对应一个DocID+Freq的链表结构。

       在Lucene中,每个token以字符串作为关键字唯一的定位到postingHash这张哈希表中。而哈希表的每个元素都是PostingList对象,PostingList并不是表示docID+freq的链表结构。其作用就是为了定位到token信息所存储的三大数据池中的位置(textStart、byteStart、intStart)以及记录上一次同一个token加入倒排索引的信息。真正每个token对应的posting结构记录在ByteBlockPool中。我们来看看下面的图(DOC1-DOC4的"lucene"和"term"的倒排索引结构)。

 

 

      此外,Lucene的docID是用docCode=docID<<1来存储的,而position是用prox=(position-lastPosition)<<1来存储的。至于为什么这样储存,是考虑到数据的压缩以降低倒排索引表所消耗的空间代价,具体详见Lucene的压缩储存技术。

 

      Lucene的这种倒排索引的内存组织结构,非常适合在磁盘中存储。关于这一点,我们将要在以后Lucene索引文件中详细提到。

 

 

 

 

2
0
分享到:
评论

相关推荐

    lucene 原理与代码分析

    ### Lucene索引过程分析 索引过程涉及多个步骤: 1. **创建IndexWriter对象**:用于管理和控制索引的写入操作。 2. **创建文档和字段**:定义文档结构,包括字段类型和内容。 3. **文档添加**:将文档添加到...

    Lucene 原理与代码分析完整版.pdf

    #### Lucene索引过程分析 - **创建IndexWriter对象**:负责管理索引的写入操作。 - **创建文档Document对象**:封装文档内容。 - **将文档加入IndexWriter**:提交文档到索引中。 - **索引缓存管理**:管理各种缓存...

    Lucene_原理与代码分析完整版

    例如,缓存管理涉及到对字符、字节和整数缓存池的操作,以优化内存使用和磁盘读写效率。 #### 五、Lucene段合并过程分析 随着新文档的不断添加,Lucene会定期进行段合并操作,以优化索引结构并减少磁盘空间占用。...

    ssm+lucene+redis

    3. 实现搜索功能:利用Lucene创建索引,这通常在后台服务启动时完成,或者在数据新增、更新时动态更新索引。在用户发起搜索请求时,使用Lucene的查询API匹配索引,返回匹配结果。 4. 数据库交互:MyBatis用于处理...

    Lucene+原理与代码

    #### Lucene索引过程分析 **Lucene**的索引过程主要包括以下几个步骤: 1. **创建IndexWriter对象**: IndexWriter负责创建和更新索引。 2. **创建文档Document对象,并加入域Field**: 每个文档由多个域组成,每个...

    Lucene+mysql相关jar包

    分词索引是Lucene的核心功能之一,它将文本拆分成可搜索的单词或短语(称为“术语”),然后创建一个索引结构,使得查询时能快速定位到包含特定术语的文档。 现在我们来看一下压缩包中包含的各个jar文件: 1. `...

    Lucene 原理与代码分析完整版

    #### Lucene索引过程分析 索引过程是Lucene中最为核心的部分之一,涉及多个复杂步骤和技术细节。 1. **创建IndexWriter对象**:这是索引构建的第一步,通过设置不同的选项可以控制索引的行为,如是否支持删除文档...

    大数据与数据挖掘技术 FirteX-高性能全文索引和检索平台 共29页.pptx

    相比之下,Lemur是一个以C++开发的信息检索工具包,主要面向研究领域,支持多种索引方式和检索模型,但其动态词典可能导致索引文件不兼容,内存池策略限制了其检索功能,且架构封闭,扩展性不佳。而Lucene,尽管是一...

    SSM框架构建积分系统和基本商品检索系统(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)

    5. **Lucene**:Lucene是Apache软件基金会的全文搜索引擎库。在这个商品检索系统中,Lucene被用来建立索引,实现高效的全文搜索功能。用户可以通过关键词快速查找相关商品,提升用户体验。 6. **MAVEN**:Maven是...

    基于java实现的(结合lucene)版的公交搜索系统_javaso.zip

    在本项目中,Lucene用于处理公交线路数据,构建索引,并提供快速的搜索服务。学习者需要掌握Lucene的基本操作,如创建索引、查询、评分等。 5. **文件操作与数据存储**:公交线路数据可能存储在文件中,项目可能...

    j2ee面试考察点.pdf

    1. **Lucene索引过程**:包括文档分析、词项创建、倒排索引构建等步骤,使搜索变得高效。 2. **Lucene搜索过程**:从查询解析到倒排索引匹配,再到结果排序,涉及多个阶段。 3. **实时搜索设计**:如Zoie系统,...

    elasticsearch6.6.1源码

    3. 内存管理:内存使用优化,如利用内存池提升缓存性能。 七、监控与运维 Elasticsearch提供了丰富的监控指标,如节点状态、索引健康状况、搜索性能等,方便运维人员进行问题排查和性能调优。Kibana作为可视化工具...

    基于java的开发源码-毕业论文:搜索引擎系统附源代码.zip

    这个项目可能是为了展示如何构建一个搜索引擎,包括数据抓取、索引创建、查询处理以及结果排名等核心功能。 【描述】中的信息与标题相同,再次强调了这是一个基于Java的搜索引擎系统项目,源代码作为辅助材料,可以...

    java技能总结

    2. **创建 JavaBean 对象**:封装要索引的数据。 3. **创建 Document 对象**:将 JavaBean 对象转换为 Lucene 可识别的格式。 4. **使用 IndexWriter 将 Document 对象写入索引库**。 5. **查询索引库**:通过关键字...

    Python库 | jsonpyes-1.2.6.tar.gz

    5. **映射管理**:`jsonpyes`允许开发者定义和管理字段的映射(Mapping),这在确保数据正确索引和搜索时至关重要。通过`put_mapping`和`get_mapping`方法,可以实现映射的添加和查看。 6. **批量操作**:对于大量...

    java学习路线图 java学习路线图

    - **内存分析**:学习Java内存模型,理解栈、堆以及垃圾回收机制。 - **递归**:学习如何编写和理解递归函数。 - **集合类**:熟悉ArrayList、LinkedList、HashSet、HashMap等数据结构的使用。 - **泛型**:学习...

Global site tag (gtag.js) - Google Analytics