Lucene索引过程中的内存管理与数据存储

Lucene的索引过程分两个阶段,第一阶段把文档索引到内存中;第二阶段,即内存满了,就把内存中的数据刷新到硬盘上。

         倒排索引信息在内存存储方式

Lucene有各种Field,比如StringField,TextField,IntField,FloatField,DoubleField…,Lucene在处理的过程中把各种Field都处理成相应的byte[],以最本质的方式来看待各种Field的内容,统一了数据的存储形式。

在写入内存阶段,第一步就是需要理清各个类之间的关系。

在索引的过程中,需要有ByteBlockPool,IntBlockPool, ParallelPostingsArray三个类来协调配合存储数据. ByteBlockPool存储Term信息/Freq信息/Prox信息,IntBlockPool起着协调控制的作用; ParallelPostingsArray同时起着协调控制和统计docFreq的作用.三者紧密结合,构成了Lucene索引内存阶段的铁三角.

在Lucene的设计里,IntBlockPool和ByteBlockPool的作用域是IndexChain,即每个IndexChain都会生成独立的ByteBlockPool和IntBlockPool ,这样就不会出现多线程间可变数据共享的问题,这种做法实际上是一种约定方式的线程封闭,即ByteBlockPool本身并不是线程安全的,不像ThreadLocal或者栈封闭。由于每个IndexChain都需要处理多个Field,所以IntBlockPool和ByteBlockPool是Field所共享的。需要注意的是ParallelPostingsArray的作用域是Field,即每个Field都有一个postingsArray。

从IndexChain的TermHash开始,各个类的协调关系如下图所示:

第一次看这幅图会有错综复杂的感觉,的确如此。有以下几点需要注意:

1. TermsHash创建了IntBlockPool和ByteBlockPool。其中bytePool和termBytePool指向同一个对象。而且整个图中所用到的intPool和bytePool都是共享TermsHash创建的对象。

2. BytesRefHash中的bytesStart和ParallelPostingsArray中的textStarts共享同一个对象。

3. IntBlockPool管理着ByteBlockPool的Slice块信息的写入起始位置

把目光专注到ParallelPostingsArray的三个成员变量上面:

textStarts存储的是每一个term在ByteBlockPool里面的起始位置,通过textStarts[termID]可以快速找到termID对应的term 。

byteStarts存储的是term在ByteBlockPool的结束位置的下一个位置。

IntStarts存储的是term在IntBlockPool的地址信息,而IntBlockPool则存储着term在ByteBlockPool中的Slice位置信息。

比如两个词”new term”,PostingArray和IntBlockPool及ByteBlockPool的数据指示关系如下:(注下图只表示各个部分的联系)

Lucene在存储倒排索引的时候默认的存储选项是:

即需要存储DOCID;Freq;Positions三种信息。这三种信息都是随着Term存储在ByteBlockPool中。其存储的过程如下:

第一步:把term.length存储到ByteBlockPool.buffer中。这会占用1或者2个byte,由term的大小决定。由于term的最大长度为32766,所以term.length最多会占用两个byte。

第二步:把term的byte数组形式存储到ByteBlockPool.buffer中。

第三步:紧接着term开辟5个byte大小的slice,用来存储term在每个doc中的freq信息。

第四步,再开辟一块Slice用来存储positions信息.

第三步和第四步开辟的Slice除了存储的内容不同外,结构是没有差别的。 如果一个Slice用完了,那么按照ByteBlockPool设置的规则再开辟14个byte的slice.

如果slice又用完了,则再开辟20个byte的slice…..

下图的两个数组代表了slice的开辟规则:一共有9种不同层次的slice,编号从1-9,每一种层次的slice大小都不相同,最小是5byte,最大是200byte。

每个代码块实际可用的Bytes= Slice.length-1 。这是因为slice的最后一个byte里面存储着该slice的结束标志。5_Bytes_Slice的结束符是16,14Bytes_Slice的结束符是17,依次加1就可以了。

新开辟的slice会与前面用完的slice连接起来,像链表一样。

连接的方式比较特殊:把前一个Slice除结束符外最后的三个位置里面存储的数据转移到新的Slice中,这样前一个Slice的最后4个位置就用来存储新的Slice在buffer中的地址信息。两个Slice连接的代码如下:

 /*@xh 传入的参数slice 与函数体中的 buffer指向同一块内存地址。这样做的目的在于编码上清晰。* */
 public int allocSlice(final byte[] slice, final int upto) {
        /*@xh
         * slice[upto]里面存储的是当前slice的结束标志,slice[upto] & 15即得到当前层ID。
         * 通过数组NEXT_LEVEL_ARRAY 得到下一层的ID,
         * 通过数组LEVEL_SIZE_ARRAY 得到下一层的Slice大小
         * */
        final int level = slice[upto] & 15;
        final int newLevel = NEXT_LEVEL_ARRAY[level];
        final int newSize = LEVEL_SIZE_ARRAY[newLevel];
        // Maybe allocate another block
        if (byteUpto > BYTE_BLOCK_SIZE-newSize) {
          nextBuffer();
        }
        final int newUpto = byteUpto;
        final int offset = newUpto +byteOffset;
        byteUpto += newSize;
        // Copy forward the past 3 bytes (which we are about
        // to overwrite with the forwarding address):
        /*@xh 简单翻译就是:把当前Slice结束标志位前面的存储的内容移到下一层Slice的前三个位置
         * */
        buffer[newUpto] = slice[upto-3];
        buffer[newUpto+1] = slice[upto-2];
        buffer[newUpto+2] = slice[upto-1];
        /*@xh 然后用当前Slice空出来的三个位置连同结束标志位,一共4个Byte,来存储下一层Slice在buffer中起始位置。
         * 这样的话就可以通过当前Slice定位到下一层的Slice
         * */
        // Write forwarding address at end of last slice:
        slice[upto-3] = (byte) (offset >>> 24);
        slice[upto-2] = (byte) (offset >>> 16);
        slice[upto-1] = (byte) (offset >>> 8);
        slice[upto] = (byte) offset;
        // Write new level:
        //@xh 把下一层的结束标志写入。
        buffer[byteUpto-1] = (byte) (16|newLevel);
        //@xh 返回下一层可用的起始位置(由于下一层的前三个位置已经被占用<参看上面的代码>,所以需要+3)
        return newUpto+3;
      }

这种以链表的方式管理内存空间,是充分考虑了数据的特点。在文档集中的词分布是zipf分布。只有少量的词频很高,大量的词词频其实很低。所以最小的Slice是5byte,但是如果所有的Slice都是5byte的话,对于高频词汇,又太浪费空间,所以最大的Slice是200byte。而且有9种不同的Slice,满足了不同词频的存储需求。

如果要从Slice中读取数据,怎么知道里面的byte是数据信息还是下一层的地址信息呢?通过ByteSliceReader就很容易了.在写入数据到Slice时记录ByteBlockPool.buffer中代表Slice块链表的startIndex和endIndex。接下来我们肯定是从5_Bytes_Slice块开始读取.如果startIndex+5>=endIndex,那么就可以确定当前块中存储的内容只是整个Slice链表的一部分.就很自然得到5_Bytes_Slice中数据信息的终结位置limit.接下来用同样的方法确定出下一层的limit就OK啦。

具体的实现细节可以参考ByteSliceReader类,代码很容易读懂。

根据前面描述的ByteBlockPool存储term的方式,如果document如下:

则在ByteBlockPool中,存储的结构如下:

可以看到每个term后面都跟了两个5_Bytes_Slice,米***的块用来存储docDelta和docFreq信息;蓝色的块用来存储position信息。这就需要为每一个term分配两个int来保存Slice的起始位置,IntBlockPool则正好实现了上面的要求。接下来就会出现新的问题了,IntBlockPool中的哪两个位置是分配给给定termID的呢?IntStarts[termID]就正好指明了分配给term的位置起点。(注:两个位置是连续的)。所以ParallelPostingsArray和IntBlockPool可以视为整个倒排索引的藏宝路线图,而ByteBlockPool则可视为宝藏所在地。

还有就是Lucene存储在索引中的并非真正的docId,而是docDelta,即两个docId的差值.这样存储能够起到节约空间的作用.

正向信息在内存中的存储

正向信息在Lucene中只有docId-document的映射,由CompressingStoredFieldsWriter类来完成。

Lucene的正向信息存储比较简单,按Field依次把内容写入到bufferedDocs中,然后把偏移量写入到endOffsets中就OK了。

当满足flush条件或者执行了IndexWriter.commit()方法,则会进行一次flush操作,把内存中缓存的document及倒排信息flush到硬盘中。

时间: 2025-01-04 03:31:27

Lucene索引过程中的内存管理与数据存储的相关文章

[linux内存]系统启动过程中的内存管理

内核启动过程的内存管理1,memblock机制 kernel/arm/mm/memblock.c arm_memblock_init()函数 系统刚启动的时候不是所有的内存都是可以作为分配使用的,比如有些内存是默认给rootfs或者kernel使用的,memblock机制 作用就是决定哪些内存是可以分配的,哪些是默认已经被使用的. 涉及的三个主要API是memblock_init() memblock_reserve()  memblock_add()函数 2,bootmem机制 http://

启动期间的内存管理之初始化过程概述----Linux内存管理(九)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理 在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. 而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. 1 前景回顾 1.1

cocos2dx中的内存管理机制及引用计数

1.内存管理的两大策略: 谁申请,谁释放原则(类似于,谁污染了内存,最后由谁来清理内存)--------->适用于过程性函数 引用计数原则(创建时,引用数为1,每引用一次,计数加1,调用结束时,引用计数减1,当引用计数为0时,才会真正释放内存) --------->适用于注册性函数(消息处理,中断等场合) 2.cocos2dx中的内存管理采用引用计数和内存托管的原则 spr->retainCount();//获取对象的引用计数值 spr->retain();//引用计数加1 spr

Android中的内存管理机制以及正确的使用方式

概述 从操作系统的角度来说,内存就是一块数据存储区域,属于可被操作系统调度的资源.现代多任务(进程)的操作系统中,内存管理尤为重要,操作系统需要为每一个进程合理的分配内存资源,所以可以从两方面来理解操作系统的内存管理机制. 第一:分配机制.为每一个进程分配一个合理的内存大小,保证每一个进程能够正常的运行,不至于内存不够使用或者每个进程占用太多的内存. 第二:回收机制.在系统内存不足打的时候,需要有一个合理的回收再分配的机制,以保证新的进程可以正常运行.回收的时候就要杀死那些正在占有内存的进程,操

Java面向对象(二):成员变量—OOP中的内存管理—构造函数

第一节 成员变量 1.1成员变量与局部变量 成员变量:声明在类下面,方法外面:作用于整个类中: 局部变量:声明在方法下面,作用于方法下面. 1.2 成员变量默认值  成员变量的默认值和数组的默认值一样: 整数类型:0 浮点类型:0.0 字符类型:\u0000 布尔类型:false 字符串类型:null 1.3 成员变量和局部变量的区别  1.从声明的位置: 局部变量声明在方法中,成员变量声明在类下面. 2.从默认值方面: 局部变量没有默认值,成员变量有默认值. 3.从生命周期来说: 局部变量随着

Cocos2d-x开发中C++内存管理

由于开始并没有介绍C++语言,C++的内存管理当然也没进行任何的说明,为了掌握Cocos2d-x中的内存管理机制,是有必要先了解一些C++内存管理的知识.C++内存管理非常复杂,如果完全地系统地介绍可能需要一本书的篇幅才能解释清楚.这里只给大家介绍C++内存管理最为基本的用法. 内存分配区域创建对象需要两个步骤:第一步,为对象分配内存,第二步,调用构造函数初始化内存.在第一步中对象分配内存时候,我们可以选择几个不同的分配区域,这几个区域如下:栈区域分配.栈内存分配运算内置于处理器的指令集中,效率

关于OC中得内存管理问题,alloc,retain,release,copy,dealloc

我们都知道,一个手机,它的内存是有限的,而每一个手机应用都是需要一定空间,当应用所占空间过大时,系统就会发出警告,怎样在有限的空间中,做到更高效实用美观的效果呢? 这时候就牵涉到OC中得内存管理了. 在OC这门语言中,是不存在垃圾回收机制的,但是它采用了另外一种形式或者说方法,实现这一个空间回收的效果,那就是引用计数器. 别看-引用计数器,这个名字很高大上,实际是它就是一个整数. 所以OC中分配4个字节才存储它. 引用计数的值只有两种:0和非0,我们知道,计算机其实是很笨的,结果只有这两种时,它

Unity游戏开发中的内存管理_资料

内存是手游的硬伤——Unity游戏Mono内存管理及泄漏http://wetest.qq.com/lab/view/135.html 深入浅出再谈Unity内存泄漏http://wetest.qq.com/lab/view/150.html 这一次,我优化了37%的内存http://wetest.qq.com/lab/view/147.html Unity项目资源加载与管理http://wetest.qq.com/lab/view/124.html Android应用内存泄露分析.改善经验总结h

C++中的内存管理

在C++中也是少不了对内存的管理,在C++中只要有new的地方,在写代码的时候都要想着delete. new分配的时堆内存,在函数结束的时候不会自动释放,如果不delete我分配的堆内存,则会造成内存泄露.所以我们要学会内存管理,不要内存泄露.在C++中的内存管理机制和OC中的还不太一样,在OC中的ARC机制会给程序员的内存管理省不少事,但在C++中没有ARC所以我们要自己管理好自己开辟的内存.Java中也有自己相应的内存管理机制,比如JDBC里的获取的各种资源在finally里进行close等