Lucene源码分析 - FST

本文详细分析了Lucene的FST(Finite State Transducer)实现,从源码层面探讨了FIXED ARRAY存储方式、Arc的反向存储原因、FST遍历优化及压缩策略等内容,旨在帮助读者深入了解Lucene中FST的数据结构和操作原理。
摘要由CSDN通过智能技术生成

本文的代码以lucene-core 6.3.0为准,包含构建空FST的addNode函数,pack压缩函数等整个类所有代码的解析。转载请注明出处。

0 基本信息

   lucene的FST的二进制存储和查询方式都是由FST这个类来实现。如果要读这些代码,首先需要了解这个类里面的一些基本的东西。

  • 整个FST存到了BytesStore类的bytes对象里,bytes相当于一个大的字节数组,提供正向遍历和反向遍历数组的方式,bytes中存的是Node的Arc的内容,包括label,output,nextFinalOutput,target等。
  • Arc中,target是指Arc指向的Node,这个值可以是Node的ID,也就是ord,或者Node的Address。
  • nodeAddress存的是Node ID->Address的映射,inCounts存的是Node ID -> Node的入度(指向该Node的Arc数量),这两个类都是GrowableWriter,包了一下PackedInts,关于PackedInts可以参考下作者@iteye_14612的PackedInts源码分析
  • cachedRootArcs缓存一部分从根节点出发的Arc,INPUT_TYPE是Arc的label所占用的字节数。
  • 文中多次提到Node的Arc会按照label的字典序存,这与Builder构建FST有关,可以看下我的另一篇Lucene源码分析 - FST-Builder

1 源码分析

   先看下构建空的FST。addNode函数把Builder中的UnCompiledNode写入FST的bytes对象,写入的这个Node的ID就是nodeCount,这里把Node的Arc顺序写到字节数组中,由于FST的Builder是按照label的字典序调用构建的FST,所以这里的Arc也是按照label排序的,代码如下:

	// 处理没有Arc的空节点
    if (nodeIn.numArcs == 0) {
   
      if (nodeIn.isFinal) {
   
        return FINAL_END_NODE;
      } else {
   
        return NON_FINAL_END_NODE;
      }
    }
	// 记录当前Node在Address
    final long startAddress = builder.bytes.getPosition();
    // 判断是否用FIXED_ARRAY方式存Arc的信息
    final boolean doFixedArray = shouldExpand(builder, nodeIn);
    if (doFixedArray) {
   
      if (builder.reusedBytesPerArc.length < nodeIn.numArcs) {
   
      	//reusedBytesPerArc 用来存下每个Arc的字节长度
        builder.reusedBytesPerArc = new int[ArrayUtil.oversize(nodeIn.numArcs, 1)];
      }
    }
1.1 什么是 FIXED ARRAY

   如果这里的doFixedArraytrue,那么每个Arc都用一样的字节长度(后文都用长度代替),由于有的Arc可能有outputnextFinalOutput,而有的却没有,所以每个Arc的长度可能是不一样。FIXED_ARRAY方式下,每个Arc的存储长度都用最长的那个Arc的长度。
   很明显,这种FIXED_ARRAY方式会导致Arc与Arc之间存在一些内存碎片,好处就是能快速定位Node下的第N个Arc的位置,所以在查询Node下是否有Arc的label匹配一个确定的值的时候,可以用二分查找,这样就是空间换了时间。
   当然用这种模式也是有条件的,shouldExpand函数中给出了条件,比如Node下Arc数量要超过10,Arc数量太少的话,二分的效果比起遍历没有优势,而且还浪费了一些内存,反而亏了。

1.2 存储Node的Arc

   接下来是遍历这个Node的所有的Arc,并且把每个Arc的信息等写入bytes,前后两个Arc紧挨着存的,没有内存空隙:

for(int arcIdx=0;arcIdx<nodeIn.numArcs;arcIdx++) {
   
	  ...  //省略flags处理逻辑
	  // 存标志位信息
      builder.bytes.writeByte((byte) flags);
      writeLabel(builder.bytes, arc.label);
      
	  // 这里可以看到,每个Arc的长度不一样,主要是下面这三个write的内容不一样
      if (arc.output != NO_OUTPUT) {
   
        outputs.write(arc.output, builder.bytes);
      }

      if (arc.nextFinalOutput != NO_OUTPUT) {
   
        outputs.writeFinalOutput(arc.nextFinalOutput, builder.bytes);
      }

      if (targetHasArcs && (flags & BIT_TARGET_NEXT) == 0) {
   
        builder.bytes.writeVLong(target.node);
      }
	  
      if (doFixedArray) {
   
        // 记录下每个Arc的长度
        builder.reusedBytesPerArc[arcIdx] = (int) (builder.bytes.getPosition() - lastArcStart); 
        lastArcStart = builder.bytes.getPosition();
        // 记录最长的Arc的长度
        maxBytesPerArc = Math.max(maxBytesPerArc, builder.reusedBytesPerArc[arcIdx]);   
      }
    }
1.3 将Arc紧邻而存转变成 FIXED ARRAY 存储

   所有Arc存下之后,如果doFixedArraytrue,由于前面Arc内容写入bytes的时候是紧挨着写入的,所以还需要调整一下,每个Arc的长度要调整为最长的Arc的长度,所以从最后一个Arc开始,基本上每个Arc都需要向后移动到正确的位置上。代码如下:

    if (doFixedArray) {
   
      final int MAX_HEADER_SIZE = 11;    // header(byte) + numArcs(vint) + numBytes(vint)
      
      byte header[] = new byte[MAX_HEADER_SIZE]; 
      ByteArrayDataOutput bad = new ByteArrayDataOutput(header);
      bad.writeByte(ARCS_AS_FIXED_ARRAY);	   // 写入标志位,代表ARC是FIXED_ARRAY
      bad.writeVInt(nodeIn.numArcs);      // 写入Arc的数量
      bad.writeVInt(maxBytesPerArc);		// 写入最大的边的长度
      int headerLen = bad.getPosition();
      
      final long fixedArrayStart = startAddress + headerLen;
	
	  //记录最后一个Arc的Address,注意这里是反向读取
      long srcPos = builder.bytes.getPosition();	
      //记录FIXED_ARRAY方式下,最后一个Arc调整之后的Address
      long destPos = fixedArrayStart + nodeIn.numArcs*maxBytesPerArc;		

      if (destPos > srcPos) {
   
      	// bytes 移动到调整的位置
        builder.bytes.skipBytes((int) (destPos - srcPos));
        for(int arcIdx=nodeIn.numArcs-1;arcIdx>=0;arcIdx--) {
   		// 从最后一个Arc开始调整
          // 计算应该调整后的位置
          destPos -= maxBytesPerArc;
          srcPos -= builder.reusedBytesPerArc[arcIdx];		

          if (srcPos != destPos) {
   
            // 如果Arc调整后的位置与当前所处位置相比发生了变化,那么就移动Arc到调整之后的位置
            builder.bytes.copyBytes(srcPos, destPos, builder.reusedBytesPerArc
Lucene是一个全文检索引擎,它的核心数据结构包括倒排索引和正排索引。其中,倒排索引是Lucene最重要的数据结构之一,它通过将文档中的每个词都映射到包含该词的文档列表来实现快速的文本搜索。 Lucene中的Term Dictionary和Term Index是倒排索引中的两个重要组成部分。Term Dictionary用于存储所有唯一的词项(term),而Term Index则用于快速定位某个词项的位置。 在Lucene中,Term Dictionary和Term Index通常存储在磁盘上。Term Dictionary通常使用一种称为Trie树的数据结构来实现。Trie树是一种树形数据结构,它可以快速地查找某个字符串是否存在,以及在字符串集合中查找前缀匹配的字符串。 Term Index则通常存储在一个称为倒排索引表(Inverted Index Table)的结构中。倒排索引表是由一系列的倒排索引条目(Inverted Index Entry)组成的,每个倒排索引条目包含了一个词项及其在倒排索引中的位置信息,例如该词项在文档列表中出现的次数、该词项在哪些文档中出现等。 当进行文本搜索时,Lucene会首先在Term Dictionary中查找搜索关键词是否存在,然后通过Term Index快速定位到包含该词的文档列表,最后根据文档列表中的文档ID查找正排索引中具体的文档内容。这种基于倒排索引的搜索方式可以实现非常高效的文本搜索,是Lucene等全文检索引擎的核心技术之一。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值