美文网首页
Lucene Point/Range类型存储流程与文件格式

Lucene Point/Range类型存储流程与文件格式

作者: ni_d58f | 来源:发表于2019-09-21 04:02 被阅读0次

    Point/Range 类型写入详解

    (本文示例都是简单的一维IntPoint)

    1. 写入主要流程

    说明: 对Point来说,在DefaultIndexingChainprocessField方法中,只走到了indexPoint方法, 其中主要步骤有:

    • 根据field name构造PerField, PerField保存了每一个filed基本上所有的元数据, PerField数组组织形式如下:
    PerField Array 组织形式

    主要算法: 根据fieldName hash取模得到数组下标index, 然后在对应的PerFiled中查找对应的PerFiled(如果index所对应PerField值不为null), 这个实现很类似于java的Map中根据key查找value过程。

    关于为什么不直接使用Java 的Map, 作者在Java doc说根据测试表明采用数组 + 链表然后手动扩容方式相比于Map来说有3%的性能优势

    PerField对应的数据结构:

    PerField结构
    PerField为 DefaultIndexingChain的内部类, 其中有关数据数据结构如图:

    其中需要注的的有:

    1. fieldInfo //field元数据
    2. PointValuesWriter //写PointValue类
      像InvertState, TermHashPerFiled、docValuesWriter等对于Point类型来说,这个不需要关心。
    • 根据PerField信息,构造维度信息;接着构造PointValuesWriter对象, 这个对象负责写Point数据,并将这个对象赋值给PerField的pointValuesWriter成员变量

    • 将文档的id与point值存入PointValuesWriter, 其中Point值会以ByteRef形式存储,这实际上是byte[]的一个封装。
      Doc id会存入docID的int数组,下标为该point的序号,即第几个point值
      Point会存入bytes的ByteBlockPool, 这个对象是一个可变的byte[][], 每个point 会转化成byte[]存入bytes, 因为point是固定字节,因此不需要设置offset与length。

    byte[][]数组

    具体代码如下:

    public void addPackedValue(int docID, BytesRef value) {
        if (value == null) {
          throw new IllegalArgumentException("field=" + fieldInfo.name + ": point value must not be null");
        }
        if (value.length != packedBytesLength) {
          throw new IllegalArgumentException("field=" + fieldInfo.name + ": this field's value has length=" + value.length + " but should be " + (fieldInfo.getPointDataDimensionCount() * fieldInfo.getPointNumBytes()));
        }
    
        if (docIDs.length == numPoints) {
          docIDs = ArrayUtil.grow(docIDs, numPoints+1);
          iwBytesUsed.addAndGet((docIDs.length - numPoints) * Integer.BYTES);
        }
        bytes.append(value);
        docIDs[numPoints] = docID;
        if (docID != lastDocID) {
          numDocs++;
          lastDocID = docID;
        }
    
        numPoints++;
      }
    

    现在将Point值存储内存的过程就结束了,是不是看起来很简单呢? 确实, 将数据保存在内存中的逻辑相对简单,复杂的过程在最后flush中程中

    Flush过程

    flush操作一般是由于用户显示调用commit()方法, 其它可能的情况包括内存缓存的数据过多,内存不足、合并段文件等,下面以用户显示调用IndexWriter.commit() 来说明Point类型说明对应的过程。

    flush操作总入口在DocumentsWriterPerThreadflush方法中, 主要方法为

         //... 
         sortMap = consumer.flush(flushState);
         // ...
    
    • DefaultIndexingChainflush函数进去后,对于point类型来说,主要会调用
      1. writeNorm() //没有显示设置的话,这个不起作用, 主要作用是写入Norm值,关于Lucene中Norm作用请google
      2. writeDocValues() //对于point类型来说这个不起作用, 主要记录像String, Filed docId到value的正向索引、NumericType与binary 对应的值。
      3. writepoint() 这个函数会将所有的point filed数据写入文件, 包括数据与索引
      4. 写入String, text field类型的数据、索引
      5. 构造segment文件中关于所有field信息的元数据

    现在重点关注第3步过程

    • 后面调用链路如下:
      DefaultIndexingChain.writePoints() --> PointValuesWriter.flush() --> Lucene60PointsWriter.writeField() --> BKDWriter.writeField()

    BKDWriter.writeField()会调用BKDWriter.add()BKDWriter.finish()方法。 代码链路比较长,直接贴最后的文件格式,后面会对文件格式具体内容进行说明

    笔者测试代码如下:

     for (int i = 0; i < 10240; i++) {
           Document d = new Document();
           IntPoint p = new IntPoint("age", i);
           d.add(p);
    
           writer.addDocument(d);
      }
      writer.commit();
    

    最终的存储形式如下图:

    Point索引元数据文件组织形式

    Point Filed的文件组织形式
    Point索引元数据文件组织形式 Point值的BKD索引

    dii文件保存的是block数据在dim文件的偏移量,查询的时候首先加载dii文件,根据dii文件同时加载dim文件的packedIndex区,拿到BKD的索引数据以构造BKD查询树。

    查询的时候访问BKD(Block K Dimension)查询树,确定Block下标,比如查询1025这个值,可以很明确的知道在以1024为起始值的block中,通这该block的下标在dii文件中获取block在dim文件的地址,最后采用二分的方式找到对应的point

    以上三张图大致展示Point field的存储结构及细节,关于Point field需要特别掌握BKD算法思想与实现
    这是实现Point/Range类型数据存储与索引的核心。

    关于具体代码流程与细节地方读者如果有兴趣,有可以自行参考上图去阅读。

    相关文章

      网友评论

          本文标题:Lucene Point/Range类型存储流程与文件格式

          本文链接:https://www.haomeiwen.com/subject/cvatyctx.html