综述
Lucene中有个比较特殊的索引文件,叫做DocValues。从字面意思看,就是文档的值,在lucene中,文档是由字段组成的,所以文档的值其实就是字段的值。说起字段的值,前面我们已经介绍过了正排索引,就是用来存储文档中字段的值,那它和我们接下来要介绍的DocValues有什么区别呢?
在正排索引中,同一个文档中所有的字段的值都存储在一起,以存储的专业术语就是行式存储。而DocValues是同一字段所有doc的值都存储在一起,也就是列式存储。
行式存储我们可以通过docId很容易拿到docId所有字段的值,更注重的是根据docID来获取所有字段的值,因此lucene中正排数据是用来在返回召回结果时,根据docID填充字段信息返回给用户。而列式存储我们可以容易拿到某个一个字段在所有文档中的值,用列式存储来做排序聚合类操作性能更高。
Lucene中针对不同使用场景,一共设计了5种DocValues。
-
NumericDocValues
数值类型的docValues,最终都是会转成long来处理,并且一个doc中的字段只能有一个NumericDocValues。
-
SortedNumericDocValues
同NumericDocValues一样,也是数值类型的docValues,只是一个doc中的字段可以有多个SortedNumericDocValues,对于同一个字段来说,这些数值是有序的。
-
BinaryDocValues
字节数组类型的docValues,并且一个doc中的字段只能有一个BinaryDocValues。
-
SortedDocValues
字节数组类型的docValues,并且一个doc中的字段只能有一个SortedDocValues,但是在存储的时候是全局有序的。
-
SortedSetDocValues
多值且有序的字节数组
例子
下面的例子中,包含了所有DocValues类型的使用示例:
public class DocValueDemo {
public static void main(String[] args) throws IOException {
Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath());
WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer();
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer);
indexWriterConfig.setUseCompoundFile(false);
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
Document document = new Document();
// NumericDocValuesField最多只能一个,存储数值类型
document.add(new NumericDocValuesField("age", 20));
// SortedNumericDocValuesField可以有多个,存储数值类型
document.add(new SortedNumericDocValuesField("level", 4));
document.add(new SortedNumericDocValuesField("level", 3));
document.add(new SortedNumericDocValuesField("level", 0));
// BinaryDocValuesField最多只能一个,存储二进制
document.add(new BinaryDocValuesField("name", new BytesRef("zjc".getBytes(StandardCharsets.UTF_8))));
// SortedSetDocValuesField可以有多个,存储二进制
document.add(new SortedSetDocValuesField("address", new BytesRef("hangzhou".getBytes(StandardCharsets.UTF_8))));
document.add(new SortedSetDocValuesField("address", new BytesRef("beijing".getBytes(StandardCharsets.UTF_8))));
document.add(new SortedSetDocValuesField("address", new BytesRef("shanghai".getBytes(StandardCharsets.UTF_8))));
// SortedDocValuesField只能有一个,存储二进制
document.add(new SortedDocValuesField("nickname", new BytesRef("alpha".getBytes(StandardCharsets.UTF_8))));
indexWriter.addDocument(document);
indexWriter.flush();
indexWriter.commit();
indexWriter.close();
}
}
文件格式
dvm
整体结构
dvm存储的是每个DocValues字段的元信息,是读取的时候使用。它是分字段存储的,如下图所示:
字段详解
- FieldMeta:DocValue字段的元信息
- FieldNumber:字段编号
- DocValueType:字段的DocValue类型编号
- DocValueTypeRelated:跟DocValue类型相关的其他元信息,后面的文章会一一介绍。
dvd
整体结构
dvd存储的是每个DocValues字段的数据,它也是分字段存储的,如下图所示,每个字段的数据根据docValues的类型不同,有不同的存储结构,后面我们会一一介绍。
源码解析
本文中的源码解析主要是对DocValues在整体上的构建和读取的代码逻辑做解析,没有具体到各个不同的DocValues类型,具体的DocValues的相关源码我们后面会详细介绍。
构建
数据收集
在文档索引的过程中,如果文档中有DocValues相关的字段,会先使用DocValues类型对应的的DocValuesWriter# addValue实现临时存储起来,在flush的时候执行真正的持久化逻辑。
// IndexingChain#indexDocValue
private void indexDocValue(int docID, PerField fp, DocValuesType dvType, IndexableField field) {
switch (dvType) {
case NUMERIC:
if (field.numericValue() == null) {
throw new IllegalArgumentException(
"field=\"" + fp.fieldInfo.name + "\": null value not allowed");
}
((NumericDocValuesWriter) fp.docValuesWriter)
.addValue(docID, field.numericValue().longValue());
break;
case BINARY:
((BinaryDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
break;
case SORTED:
((SortedDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
break;
case SORTED_NUMERIC:
((SortedNumericDocValuesWriter) fp.docValuesWriter)
.addValue(docID, field.numericValue().longValue());
break;
case SORTED_SET:
((SortedSetDocValuesWriter) fp.docValuesWriter).addValue(docID, field.binaryValue());
break;
case NONE:
default:
throw new AssertionError("unrecognized DocValues.Type: " + dvType);
}
}
持久化
持久化的入口是使用DocValue对应的DocValuesWriter#flush来开始的:
private void writeDocValues(SegmentWriteState state, Sorter.DocMap sortMap) throws IOException {
DocValuesConsumer dvConsumer = null;
boolean success = false;
try {
for (int i = 0; i < fieldHash.length; i++) {
PerField perField = fieldHash[i];
while (perField != null) {
if (perField.docValuesWriter != null) {
// 。。。去掉一些判断逻辑
// 使用的是对应的DocValues类型的DocValuesWriter中的flush方法来持久化
perField.docValuesWriter.flush(state, sortMap, dvConsumer);
perField.docValuesWriter = null;
} else if (perField.fieldInfo != null
&& perField.fieldInfo.getDocValuesType() != DocValuesType.NONE) {
throw new AssertionError(
"segment="
+ state.segmentInfo
+ ": field=\""
+ perField.fieldInfo.name
+ "\" has docValues but did not write them");
}
perField = perField.next;
}
}
success = true;
} finally {
if (success) {
IOUtils.close(dvConsumer);
} else {
IOUtils.closeWhileHandlingException(dvConsumer);
}
}
}
而真正持久化的核心逻辑在DocValuesConsumer中,其中对应了5中DocValues的处理逻辑,在lucene-9.1.0版本中,实现类是Lucene90DocValuesConsumer,我们后面会一一介绍不同DocValues的持久化逻辑。
public abstract class DocValuesConsumer implements Closeable {
// 持久化NumericDocValues
public abstract void addNumericField(FieldInfo field, DocValuesProducer valuesProducer)
throws IOException;
// 持久化BinaryDocValues
public abstract void addBinaryField(FieldInfo field, DocValuesProducer valuesProducer)
throws IOException;
// 持久化SortedDocValues
public abstract void addSortedField(FieldInfo field, DocValuesProducer valuesProducer)
throws IOException;
// 持久化SortedNumericDocValues
public abstract void addSortedNumericField(FieldInfo field, DocValuesProducer valuesProducer)
throws IOException;
// 持久化SortedSetDocValues
public abstract void addSortedSetField(FieldInfo field, DocValuesProducer valuesProducer)
throws IOException;
}
读取
读取的接口是DocValuesProducer:
public abstract class DocValuesProducer implements Closeable {
public abstract NumericDocValues getNumeric(FieldInfo field) throws IOException;
public abstract BinaryDocValues getBinary(FieldInfo field) throws IOException;
public abstract SortedDocValues getSorted(FieldInfo field) throws IOException;
public abstract SortedNumericDocValues getSortedNumeric(FieldInfo field) throws IOException;
public abstract SortedSetDocValues getSortedSet(FieldInfo field) throws IOException;
}
DocValuesProducer在lucene-9.1.0版本中的实现类是Lucene90DocValuesProducer,在读取的开始,最重要的是解析元信息,
// 按DocValues类型解析所有字段的元信息,具体逻辑我们后面一一介绍。
private void readFields(IndexInput meta, FieldInfos infos) throws IOException {
for (int fieldNumber = meta.readInt(); fieldNumber != -1; fieldNumber = meta.readInt()) {
FieldInfo info = infos.fieldInfo(fieldNumber);
if (info == null) {
throw new CorruptIndexException("Invalid field number: " + fieldNumber, meta);
}
byte type = meta.readByte();
if (type == Lucene90DocValuesFormat.NUMERIC) {
numerics.put(info.name, readNumeric(meta));
} else if (type == Lucene90DocValuesFormat.BINARY) {
binaries.put(info.name, readBinary(meta));
} else if (type == Lucene90DocValuesFormat.SORTED) {
sorted.put(info.name, readSorted(meta));
} else if (type == Lucene90DocValuesFormat.SORTED_SET) {
sortedSets.put(info.name, readSortedSet(meta));
} else if (type == Lucene90DocValuesFormat.SORTED_NUMERIC) {
sortedNumerics.put(info.name, readSortedNumeric(meta));
} else {
throw new CorruptIndexException("invalid type: " + type, meta);
}
}
}
总结
本文主要是对DocValues整体的一个介绍,并没有深入到某一种具体的DocValues,因为每种DocValues的文件格式都是不一样的,后面的文章我们来一一介绍。