源码系列类
原文地址: https://www.amazingkoala.com.cn/Lucene/gongjulei/2018/1209/24.html
IntBlockPool 类
在索引阶段,使用 IntBlockPool 来存储 term(域值)的信息,在 MemoryIndex 中,使用此类对 term 在文档中的位置、payload 数据进行存储,它即 MemoryIndex 中的倒排表,它的数据就是用这个类的对象存储的。在介绍 IntBlockPool 类时,我们根据这个类在 MemoryIndex 中实际使用过程来讲解。
如何介绍 IntBlockPool 类
首先会贴出这个类中几个重要的方法,详细的说明方法中的逻辑过程,在讲解 MemoryIndex 类时,会使用图文方法来演示一个例子。IntBlockPool 类的详细代码注释在这里: https://github.com/luxugang/Lucene-7.5.0/blob/master/solr-7.5.0/lucene/core/src/java/org/apache/lucene/util/IntBlockPool.java
IntBlockPool 的方法和变量
几个基本的变量说明
// 存储term的位置,词频,payload,默认是由10个大小为 INT_BLOCK_SIZE大小的一维数组组成的。
public int[][] buffers = new int[10][];
public final static int INT_BLOCK_SHIFT = 13;
// 一维数组的大小。
public final static int INT_BLOCK_SIZE = 1 << INT_BLOCK_SHIFT;
public final static int INT_BLOCK_MASK = INT_BLOCK_SIZE - 1;
// 用来表示我们正在使用第几个一维数组(前面定义了一个包含10个一维数组的二维数组)。
private int bufferUpto = -1;
// 用来表示一维数组中可以使用的位置(偏移)。
public int intUpto = INT_BLOCK_SIZE;
// 同一时刻只选取二维数组中的一个一维数组进行数据的存储跟读取,buffer用来表示我们正在使用的那个一维数组。
// 这个buffer又称为 head buffer。
public int[] buffer;
// 用来表示在二维数组中可以使用的位置(偏移)。
public int intOffset = -INT_BLOCK_SIZE;
// 分层的级别,代表下次分配新的分片(slice)的级别(层级),数组元素越大,分片大小越大,数组元素用来作为LEVEL_SIZE_ARRAY[]数组的下标值。
private final static int[] NEXT_LEVEL_ARRAY = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};
// 数组元素表示新的分片的大小。
private final static int[] LEVEL_SIZE_ARRAY = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024};
获得新的分片(slice)
在 MemoryIndex 中,对于有相同域名的不同域值,这些域值的倒排表信息都是存放到同一个二维数组中,每个域值都会被分配一个固定大小的分片,用来存储域值(即 term)的信息,并且相同的域值会被存储多次(因为在文档中的的位置不同导致偏移值和 payload 的不同)。另外多个相同的域值,他们的数据不是连续存放的,通过分层的方式分布在二维数组的不同位置(稍后会作出解释)。下面获取一个新的分片的方法。
// 需要分配size大小的分片。
private int newSlice(final int size) {
// 判断当前的head buffer空间是否充足,如果不够的话,就用分配新的head buffer来存储。
if (intUpto > INT_BLOCK_SIZE-size) {
// 空间不足的话,分配一个新的一维数组。
nextBuffer();
assert assertSliceBuffer(buffer);
}
final int upto = intUpto;
// 分配size个大小的数组空间给这次的存储,然后intUpto更新。
intUpto += size;
// 指定下次分片的级别。
buffer[intUpto-1] = 1;
return upto;
}
public void nextBuffer() {
// 判断二维数组是否存储已满,那么就扩容,并且扩容结束后,迁移数据。
if (1+bufferUpto == buffers.length) {
int[][] newBuffers = new int[(int) (buffers.length*1.5)][];
System.arraycopy(buffers, 0, newBuffers, 0, buffers.length);
buffers = newBuffers;
}
// 生成一个新的一维数组。
buffer = buffers[1+bufferUpto] = allocator.getIntBlock();
// 更新bufferUpto,这个值描述了我们当前正在使用第bufferUpto个一维数组。
bufferUpto++;
// head buffer数组的可使用位置(下标值)置为0。
intUpto = 0;
// 更新intOffset的值,表示在二维数组中可用的位置。
intOffset += INT_BLOCK_SIZE;
}
往分片(slice)中写数据
通过 SliceWriter 对象将数据写入到分片中。
SliceWriter 类的部分方法和变量
// 描述在二维数组中的位置。
private int offset;
将 value 值写入到分片中,如果分片剩余空间不足,那么重写分配一个新的分片。
public void writeInt(int value) {
// 获得head buffer这个一维数组, offset >> INT_BLOCK_SHIFT的值就是head buffer在二维数组中的行数。
int[] ints = pool.buffers[offset >> INT_BLOCK_SHIFT];
assert ints != null;
// 获得在head buffer这个一维数组组内的偏移。
int relativeOffset = offset & INT_BLOCK_MASK;
// if语句为真,说明分片剩余空间不足,我们需要分配新的分片(slice), 当前数组位置存储的是分片的层级。
if (ints[relativeOffset] != 0) {
// 分配一个新的分片,并且返回可以存放value的下标值。
relativeOffset = pool.allocSlice(ints, relativeOffset);
// 更新下ints变量和offset变量,因为调用pool.allocSlice()后,head buffer发生了变化。
ints = pool.buffer;
offset = relativeOffset + pool.intOffset;
}
// 存储value的值。
ints[relativeOffset] = value;
offset++;
}
private int allocSlice(final int[] slice, final int sliceOffset) {
// 取出分片的层级。
final int level = slice[sliceOffset];
// 获得新的分片的层级。
final int newLevel = NEXT_LEVEL_ARRAY[level-1];
// 根据新的分片的层级,获得新分片的大小。
final int newSize = LEVEL_SIZE_ARRAY[newLevel];
// 判断是否需要分配一个新的一维数组。
if (intUpto > INT_BLOCK_SIZE-newSize) {
nextBuffer();
assert assertSliceBuffer(buffer);
}
// 获得当前在head buffer中下一个可以使用的位置。
final int newUpto = intUpto;
// 记录下一个数组下标位置(这个位置的值在二维数组中的位置)。
final int offset = newUpto + intOffset;
// 更新head buffer中下一个可以使用的位置。
intUpto += newSize;
// Write forwarding address at end of last slice:
// 将sliceOffset位置的数组值替换为offset, 目的就是在读取数据时,被告知应该跳转到数组的哪一个位置继续找这个term的其他偏移值(在文本中的偏移量)跟payload值。
// 换句话如果一篇文档的某个term出现多次,那么记录这个term的在文本中的所有偏移值跟payload并不是连续存储的。
slice[sliceOffset] = offset;
// 记录新的分片层级。
buffer[intUpto-1] = newLevel;
// 返回可以写入数据的位置。
return newUpto;
}
存储一个新的域值时候需要调用下面的方法来分配一个分片(仅在 MemoryIndex 中会调用此方法,Lucene 7.5.0)。
public int startNewSlice() {
// offset的值是 head buffer这个一维数组组内的偏移量 + head buffer这个一维数组在二维数组中的偏移量。
return offset = pool.newSlice(FIRST_LEVEL_SIZE) + pool.intOffset;
}
写入的域值跟上一个处理的域值不一样,并且已经在分片存储过,那么写入之前需要调用下面方法另 offset 的值为这个域值上次在分片中的结束位置
public void reset(int sliceOffset) {
this.offset = sliceOffset;
}
从分片(slice)中读数据
通过 SliceReader 对象从分片中读取数据
SliceReader 类的部分方法和变量
// 当前读取的位置。
private int upto;
// 指定了二维数组的某个一维数组。
private int bufferUpto;
// 一维数组的第一个元素在二维数组中的偏移量(位置)。
private int bufferOffset;
// 当前正在读取数据的一维数组。
private int[] buffer;
// limit作为下标描述了下一个存储当前term信息的位置。
private int limit;
// 获得分片的层级。
private int level;
// 这个term的最后一个信息的位置。
private int end;
从当前分片中读取一个 term 的数据,如果没有完全读取,那么跳转到其他分片中继续读取。
public int readInt() {
assert !endOfSlice();
assert upto <= limit;
// if语句为真,那么去下一个分片中继续读取。
if (upto == limit)
nextSlice();
return buffer[upto++];
}
private void nextSlice() {
// Skip to our next slice
// 找到下一个存储term信息的位置。
final int nextIndex = buffer[limit];
// 获得分片的层级。
level = NEXT_LEVEL_ARRAY[level-1];
// 获得分片的大小。
final int newSize = LEVEL_SIZE_ARRAY[level];
// 计算出在二维数组中的第几层。
bufferUpto = nextIndex / INT_BLOCK_SIZE;
// 当前的一维数组的第一个元素在二维数组中的位置。
bufferOffset = bufferUpto * INT_BLOCK_SIZE;
// 取出 head buffer。
buffer = pool.buffers[bufferUpto];
// 计算出在head buffer中的起始位置。
upto = nextIndex & INT_BLOCK_MASK;
// 更新limit的值。
if (nextIndex + newSize >= end) {
// We are advancing to the final slice
// 已经读到最后一个slice。
assert end - nextIndex > 0;
limit = end - bufferOffset;
} else {
// This is not the final slice (subtract 4 for the
// forwarding address at the end of this new slice)
// 还有其他的slice没有读取到, 将limit的值置为下一个slice的起始位置。
limit = upto+newSize-1;
}
}
例子
在 MemoryIndex 的 storeTerms(…)的方法中,其逻辑就是通过 IntBlockPool 类依次存储一篇文档中的每一个域值的在文档中的分词位置 posIncr,域值的起始位置 startOffset,结束为止 endOffset,负载值 payload,下面是名词解释。
文档 A 中包含的内容: “Tales of Tales James”, 分词器:WhiteSpaceAnalyzer,分词后的
tokenStream 中包含了下面 4 个 term: “Tales”,“of”,“Tales”,“James”。
posIncr
term 是 tokenStream 的第几个 token。例如 ”of“ 的 posIncr 的值是 1,”Tales“的 posIncr 是 0 跟 2,”James“的 posIncr 是 3。
startOffset
term 的第一个字符的位置。例如 “of” 的 startOffset 的值是 6, ”James“的 startOffset 值是 15。
endOffset
term 的最后一个字符的位置的后一个位置。例如”of“的 endOffset 的值是 8,endOffset 与 startOffset 的差值就是 term 包含的字符个数。
payload
payload,负载值,这个值用来标注一个 term,你可以给这个 term 标注任意信息,比如可以将业务数据作为 payload。如果没有指定 payload 的值,那么 payload 的值为-1。在这个例子中,所有 term 的 payload 值都是-1。
文档 A 中的 term 需要保存的数据如下图
termposlncrstartOffsetendOffsetpayloadTales005-1of168-1Tales2914-1James31520-1
添加第一个 term:Tales
“Tales” 第一次添加到二维数组 buffers 中,那么先分配一个大小 2 的分片(slice),并将 posIncr 添加到 buffers 中,由于在 MemoryIndex 中,存储 startOffset,endOffset,payload 是可选项,所以当只要存放 posIncr 时,只要分配 2 个数组元素大小的分片即可,如下图:
![](https://img
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek/post/%E4%BA%92%E8%81%94%E7%BD%91/%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%E7%B1%BB/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com