Lucene系列(七)索引格式之fdx文件

2021-02-02 14:36:27 浏览数 (1)

前言

这篇文章介绍.fdx文件格式.

.fdx文件整体格式

看起来比较简单, 实际写入代码是fdt,fdm,fdx三个文件中最复杂的.

其中内容包括:

  1. IndexHeader. 索引文件头,前面说过,就不细说了.
  2. Footer: 索引文件脚, 不细说.
  3. ChunkDocsNum: 一个数组,含义是: 每个Chunk中的doc数量.
  4. ChunkStartPoint: 一个数组,含义是: 每个chunk的内容在fdt文件中文件地址.

鉴于存储方式比较复杂, 我们就直接快进到源代码.

写入代码分析

CompressingStoredFieldsWriter类的构造函数中, 初始化了FieldsIndexWriter类的实例, 由它来进行fdx文件的写入,看看他的构造函数.

代码语言:javascript复制
  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文件,而是创建了两个临时文件, docsOutfilePointOut. 分别用于存储前面提到的两份数据. 每个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文件. 该方法比较复杂, 让我们一步步捋一下.

代码语言:javascript复制
  /**
   * 在这里生成的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数据已经写入. 进行文件的转换操作而已.

  1. 向两个临时文件写入Footer, 之后将其关闭.
  2. 打开真正的fdx文件,写入Header.
  3. 向之前介绍过的fdm文件中,写入部分元数据.不是这篇文章重点,就不详细解释了.
  4. 打开刚才的临时文件DocsOut, 把数据读出来. 使用DirectMonotonicWriter来将数据写入fdx文件. 对DirectMonotonicWriter类不熟悉的话, 可以阅读 DirectMonotonicWriter源码解析. 之后将Docs的临时文件删除.
  5. 打开刚才的临时文件filePointOut, 把数据读出来, 调用DirectMonotonicWriter进行写入fdx文件. 之后将临时文件删除.
  6. 向fdx文件写入Footer. 关闭文件.

如何索引?

从名字上可以看出来, fdx文件是用来作为fdt文件的索引的. 作用就是: 能够方便快速查询到指定的doc的field信息.

那么它是如何作为索引的呢, 三个field相关文件的对应关系是怎样的.

以下内容为猜想内容, 如果你看到这条红字, 不要相信. 未来的某一天,我看到代码且确认了下面的内容, 我会回来删掉这行红字.

当我们拿到一个DocId, 该如何通过这三个文件拿到该doc的具体field信息呢?

首先, fdx及fdm文件都比较小,可以全部加载到内存中.

  1. 根据fdm中的ChunkDocsNumIndex, 可以找到在fdx文件中, 存储Chunk中doc数量的起始文件地址.
  2. 读出每个Chunk的doc数量, 用docId, 即可以算出 该DocId位于第几个Chunk的第几个Doc.
  3. 根据fdx文件中ChunkDocsNum 和 ChunkStartPoint 文件时平行数据的关系, 即可以求出, DocId所在的chunk, 其field信息在fdt文件中的起始文件位置.
  4. 将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文件

0 人点赞