前言
这篇文章介绍.fdx文件格式.
.fdx文件整体格式
看起来比较简单, 实际写入代码是fdt,fdm,fdx三个文件中最复杂的.
其中内容包括:
- IndexHeader. 索引文件头,前面说过,就不细说了.
- Footer: 索引文件脚, 不细说.
- ChunkDocsNum: 一个数组,含义是: 每个Chunk中的doc数量.
- ChunkStartPoint: 一个数组,含义是: 每个chunk的内容在fdt文件中文件地址.
鉴于存储方式比较复杂, 我们就直接快进到源代码.
写入代码分析
在CompressingStoredFieldsWriter
类的构造函数中, 初始化了FieldsIndexWriter
类的实例, 由它来进行fdx文件的写入,看看他的构造函数.
FieldsIndexWriter(Directory dir, String name, String suffix, String extension,
String codecName, byte[] id, int blockShift, IOContext ioContext) throws IOException {
this.dir = dir;
this.name = name;
this.suffix = suffix;
this.extension = extension;
this.codecName = codecName;
this.id = id;
this.blockShift = blockShift;
this.ioContext = ioContext;
// docNum 的tmp文件
this.docsOut = dir.createTempOutput(name, codecName "-doc_ids", ioContext);
boolean success = false;
try {
CodecUtil.writeHeader(docsOut, codecName "Docs", VERSION_CURRENT);
// StartPoint 的tmp文件
filePointersOut = dir.createTempOutput(name, codecName "file_pointers", ioContext);
CodecUtil.writeHeader(filePointersOut, codecName "FilePointers", VERSION_CURRENT);
success = true;
} finally {
if (success == false) {
close();
}
}
}
在构造函数中, 没有创建fdx文件,而是创建了两个临时文件, docsOut
和filePointOut
. 分别用于存储前面提到的两份数据. 每个Chunk中的doc数量
及每个chunk的内容在fdt文件中文件地址
.
之后,每次向fdt文件中,写入一个chunk的内容, 同时会调用下方的方法, 写入当前chunk的doc数量,及fdt文件地址. 注意写入的是临时文件.
代码语言:javascript复制 void writeIndex(int numDocs, long startPointer) throws IOException {
assert startPointer >= previousFP;
// doc num
docsOut.writeVInt(numDocs);
// filepoint
filePointersOut.writeVLong(startPointer - previousFP);
previousFP = startPointer;
totalDocs = numDocs;
totalChunks ;
}
在所有数据写入完成后, 会调用FieldsIndexWriter
类的finish方法,来进行生成真正的fdx文件. 该方法比较复杂, 让我们一步步捋一下.
/**
* 在这里生成的fdx文件,从两个tmp文件里面找到每个chunk的doc数量,fdt文件中存储的字节数,
* 这两个内容,写到meta文件和fdx文件中,配合起来存储的
* <p>
* 这个类本身就是为了fdx文件搞的,就是为了写fdt的索引,写得少很正常
*/
void finish(int numDocs, long maxPointer, IndexOutput metaOut) throws IOException {
if (numDocs != totalDocs) {
throw new IllegalStateException("Expected " numDocs " docs, but got " totalDocs);
}
CodecUtil.writeFooter(docsOut);
CodecUtil.writeFooter(filePointersOut);
IOUtils.close(docsOut, filePointersOut);
// dataOut 是fdx文件,是用来对fdt文件做索引的文件,所以fdt文件写入内容,我这里记录每个chunk的doc数量,占用字节数即可
// 所以这里只能调用一次么,无论是多少个多大的field,都只能调用一次这里么
// 写fdx文件
try (IndexOutput dataOut = dir.createOutput(IndexFileNames.segmentFileName(name, suffix, extension), ioContext)) {
// 这个header,48个字节.
CodecUtil.writeIndexHeader(dataOut, codecName "Idx", VERSION_CURRENT, id, suffix);
metaOut.writeInt(numDocs);
metaOut.writeInt(blockShift);
metaOut.writeInt(totalChunks 1);
// 这个filePointer,此时只写了一个header的长度,48
long filePointer = dataOut.getFilePointer();
metaOut.writeLong(filePointer);
try (ChecksumIndexInput docsIn = dir.openChecksumInput(docsOut.getName(), IOContext.READONCE)) {
CodecUtil.checkHeader(docsIn, codecName "Docs", VERSION_CURRENT, VERSION_CURRENT);
Throwable priorE = null;
try {
// 这里做的配合是, meta里面存了min/斜率等,真实的数组偏移量在dataOut里面存储
final DirectMonotonicWriter docs = DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks 1, blockShift);
long doc = 0;
docs.add(doc);
// 注意,这里是每一chunk, 而不是per document
for (int i = 0; i < totalChunks; i) {
// 每个chunk的doc数量
doc = docsIn.readVInt();
docs.add(doc);
}
docs.finish();
if (doc != totalDocs) {
throw new CorruptIndexException("Docs don't add up", docsIn);
}
} catch (Throwable e) {
priorE = e;
} finally {
CodecUtil.checkFooter(docsIn, priorE);
}
}
dir.deleteFile(docsOut.getName());
docsOut = null;
long filePointer1 = dataOut.getFilePointer();
metaOut.writeLong(filePointer1);
try (ChecksumIndexInput filePointersIn = dir.openChecksumInput(filePointersOut.getName(), IOContext.READONCE)) {
CodecUtil.checkHeader(filePointersIn, codecName "FilePointers", VERSION_CURRENT, VERSION_CURRENT);
Throwable priorE = null;
try {
// 其实由于我测试的时候只有一两个doc,肯定在一个chunk,所以dataOut里面都没写入啥东西
final DirectMonotonicWriter filePointers = DirectMonotonicWriter.getInstance(metaOut, dataOut, totalChunks 1, blockShift);
long fp = 0;
// 这里存储的是每一个chunk的实际数据的字节长度
for (int i = 0; i < totalChunks; i) {
fp = filePointersIn.readVLong();
filePointers.add(fp);
}
if (maxPointer < fp) {
throw new CorruptIndexException("File pointers don't add up", filePointersIn);
}
filePointers.add(maxPointer);
filePointers.finish();
} catch (Throwable e) {
priorE = e;
} finally {
CodecUtil.checkFooter(filePointersIn, priorE);
}
}
dir.deleteFile(filePointersOut.getName());
filePointersOut = null;
// meta里面再搞个索引
long filePointer2 = dataOut.getFilePointer();
metaOut.writeLong(filePointer2);
metaOut.writeLong(maxPointer);
CodecUtil.writeFooter(dataOut);
}
}
需要注意, 此时所有的field数据已经写入. 进行文件的转换操作而已.
- 向两个临时文件写入Footer, 之后将其关闭.
- 打开真正的fdx文件,写入Header.
- 向之前介绍过的fdm文件中,写入部分元数据.不是这篇文章重点,就不详细解释了.
- 打开刚才的临时文件
DocsOut
, 把数据读出来. 使用DirectMonotonicWriter
来将数据写入fdx文件. 对DirectMonotonicWriter
类不熟悉的话, 可以阅读 DirectMonotonicWriter源码解析. 之后将Docs的临时文件删除. - 打开刚才的临时文件
filePointOut
, 把数据读出来, 调用DirectMonotonicWriter
进行写入fdx文件. 之后将临时文件删除. - 向fdx文件写入Footer. 关闭文件.
如何索引?
从名字上可以看出来, fdx文件是用来作为fdt文件的索引的. 作用就是: 能够方便快速查询到指定的doc的field信息.
那么它是如何作为索引的呢, 三个field相关文件的对应关系是怎样的.
以下内容为猜想内容, 如果你看到这条红字, 不要相信. 未来的某一天,我看到代码且确认了下面的内容, 我会回来删掉这行红字.
当我们拿到一个DocId, 该如何通过这三个文件拿到该doc的具体field信息呢?
首先, fdx及fdm文件都比较小,可以全部加载到内存中.
- 根据fdm中的ChunkDocsNumIndex, 可以找到在fdx文件中, 存储Chunk中doc数量的起始文件地址.
- 读出每个Chunk的doc数量, 用docId, 即可以算出 该DocId位于第几个Chunk的第几个Doc.
- 根据fdx文件中ChunkDocsNum 和 ChunkStartPoint 文件时平行数据的关系, 即可以求出, DocId所在的chunk, 其field信息在fdt文件中的起始文件位置.
- 将fdt文件中, 该chunk的数据读入, 即可获取到给定DocId的具体内容.
不用完整的遍历fdt文件,而是通过fdx及fdm做了一些索引操作. 比较高效.
总结
fdx文件中, 主要是存储以chunk为单位的doc数量, 对应chunk在fdt文件中的起始位置. 由这些数据可以对fdt文件进行随机方法而不用顺序访问,加快了读取速度.
为了对fdx文件中的数据进行压缩, 防止读取到内存中过大,需要fdm进行一些配合存储. 通过DirectMonotonicWriter
进行压缩写入.
完。
以上皆为个人所思所得,如有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文链接。
联系邮箱:huyanshi2580@gmail.com
更多学习笔记见个人博客或关注微信公众号 <呼延十 >——>呼延十
var gitment = new Gitment({ id: 'Lucene系列(七)索引格式之fdx文件', // 可选。默认为 location.href owner: 'hublanker', repo: 'blog', oauth: { client_id: '2297651c181f632a31db', client_secret: 'a62f60d8da404586acc965a2ba6a6da9f053703b', }, }) gitment.render('container')
- Previous Lucene系列(六)索引格式之fdt文件