本文的代码以lucene-core 6.3.0为准,包含CompressingStoredFieldsWriter具体实现。转载请注明出处。
0 - 基本信息
CompressingStoredFieldsWriter
类是Lucene将每个Document的所有Field写入文件的具体实现类,在DefaultIndexingChain类中被调用,调用顺序是startDocument
-> writeField
-> finishDocument
,过程比较简单。
通过构造函数中可以了解到,Document需要存两部分数据,.fdt中存的是Document的每个Field的数据,.fdx存的是Document的索引数据。这里需要了解到的一个背景是,.fdx文件中,将16K(1 << 14)大小的或者数量为128的Document组成一个chunk
,每个chunk
是用LZ4函数压缩存储的。在整个.fdx中快速定位一个文档,需要先定位到文档的chunk
位置,然后解压缩这个chunk
的数据,最后找到文档在chunk
中的位置才能找到文档的所有数据。
在构造函数中fieldsStream
和indexWriter
分别可以写入.fdt和.fdx文件,了解背景,再看源码相对比较简单。
1 - CompressingStoredFieldsWriter 源码分析
writeField
函数实现了将一个Field的内容写入到内存中。
public void writeField(FieldInfo info, IndexableField field)
throws IOException {
++numStoredFieldsInDoc;
int bits = 0;
final BytesRef bytes;
final String string;
// 判断Field的类型,数字,字符串或者二进制,并计算长度
Number number = field.numericValue();
if (number != null) {
if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
bits = NUMERIC_INT;
} else if (number instanceof Long) {
bits = NUMERIC_LONG;
} else if (number instanceof Float) {
bits = NUMERIC_FLOAT;
} else if (number instanceof Double) {
bits = NUMERIC_DOUBLE;
} else {
throw new IllegalArgumentException("cannot store numeric type " + number.getClass());
}
string = null;
bytes = null;
} else {
bytes = field.binaryValue();
if (bytes != null) {
bits = BYTE_ARR;
string = null;
} else {
bits = STRING;
string = field.stringValue();
if (string == null) {
throw new IllegalArgumentException("field " + field.name() + " is stored but does not have binaryValue, stringValue nor numericValue");
}
}
}
final long infoAndBits = (((long) info.number) << TYPE_BITS) | bits;
bufferedDocs.writeVLong(infoAndBits);
// 不同的数据类型,都要写入到内存buffer中
if (bytes != null) {
bufferedDocs.writeVInt(bytes.length);
bufferedDocs.writeBytes(bytes.bytes, bytes.offset, bytes.length);
} else if (string != null) {
bufferedDocs.writeString(string);
} else {
if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
bufferedDocs.writeZInt(number.intValue());
} else if (number instanceof Long) {
writeTLong(bufferedDocs, number.longValue());
} else if (number instanceof Float) {
writeZFloat(bufferedDocs, number.floatValue());
} else if (number instanceof Double) {
writeZDouble(bufferedDocs, number.doubleValue());
} else {
throw new AssertionError("Cannot get here");
}
}
}
数字类型的Field写入内存Buffer的时候进行了优化,都存的是可变长度,对于整形数字还用了ZigZag编码,目的就是要让值比较小的数字存储的时候占用的空间尽可能少。
当一个Document的多个Field都写入内存之后,调用finishDocument
记录Field的元数据,如果内存的Buff写满了需要刷到磁盘。
public void finishDocument() throws IOException {
// numBufferedDocs数组用于记录Document的Field的数量
// 数组长度不够的话扩容
if (numBufferedDocs == this.numStoredFields.length) {
final int newLength = ArrayUtil.oversize(numBufferedDocs + 1, 4);
this.numStoredFields = Arrays.copyOf(this.numStoredFields, newLength);
endOffsets = Arrays.copyOf(endOffsets, newLength);
}
// 记录Document的Field的数量
this.numStoredFields[numBufferedDocs] = numStoredFieldsInDoc;
// 当前Document已经结束,所以重置Field计数器
numStoredFieldsInDoc = 0;
// 记录当前Document在文件中的偏移量
endOffsets[numBufferedDocs] = bufferedDocs.length;
++numBufferedDocs;
// 判断是否需要刷新到磁盘,内存满了就需要刷
if (triggerFlush()) {
flush();
}
}
如果内存Buff不够,或者所有的文档写入完毕,调用finish
将所有Field的数据写入到文件中,.fdt文件中存的数据要用LZ4压缩算法进行压缩,.fdx中只存了。
private void flush() throws IOException {
// 记录当前chunk在 .fdt 文件中的起始位置
indexWriter.writeIndex(numBufferedDocs, fieldsStream.getFilePointer());
// 将每个文档的 position 值转换一下,让除了第一个值以外的每个值变得更小
// 相当于是优化存储,细节可以不管
final int[] lengths = endOffsets;
for (int i = numBufferedDocs - 1; i > 0; --i) {
lengths[i] = endOffsets[i] - endOffsets[i - 1];
assert lengths[i] >= 0;
}
// 如果出现文档比较小,导致 16k(chunkSize) 内存中有超过 2*chunkSize 数量的文档
// 那么把这个 chunk 的文档切成多个部分,分别存储
// 实际上这种情况是不存在的,因为内存中最多存在 maxDocsPerChunk=128 个文档
final boolean sliced = bufferedDocs.length >= 2 * chunkSize;
// 将 docBase 写入到 chunk 的header中
writeHeader(docBase, numBufferedDocs, numStoredFields, lengths, sliced);
// 压缩存储
if (sliced) {
// 如果内存中的文档数量太多,那么分成多个部分存储
for (int compressed = 0; compressed < bufferedDocs.length; compressed += chunkSize) {
compressor.compress(bufferedDocs.bytes, compressed, Math.min(chunkSize, bufferedDocs.length - compressed), fieldsStream);
}
} else {
// 正常的压缩存储
compressor.compress(bufferedDocs.bytes, 0, bufferedDocs.length, fieldsStream);
}
// 重置所有变量
docBase += numBufferedDocs;
numBufferedDocs = 0;
bufferedDocs.length = 0;
numChunks++;
}
2 - CompressingStoredFieldsIndexWriter 说明
以上可以了解到.fdt文件中的数据是怎么存的,这里分析下.fdx文件中的数据如何存。上述已知.fdt文件中,所有文档被划分成多个chunk
存到文件中,那么.fdx中的索引数据就是要能快速定位chunk
,所以需要存docBase和文件的startPosition,这部分内容相对简单,就不详细说明了。