lucene索引结构(三)-词项向量(TermVector)索引文件结构分析

时间:2022-12-17 03:09:50
0. 事先对代码进行的一点修改
     当我准备开始分析此项向量索引文件的时候,突然发现我的索引程序生成的索引文件里没有.tvx,.tvd,.tvf这三个文件。看了看lucene文档,才知道了"Term Vector support is an optional on a field by field basis. "。
     
     喔!原来是个可选的。那么意思就是说不生成这种索引,一次检索过程也能正常的完成,获取我们想要的信息。那它有啥用呢,反正肯定是有用的,要不Lucene让我们选了作甚。具体的作用可参见本文1.1节。

     将生成索引的代码改了下,
      doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED,Field.TermVector.WITH_POSITIONS_OFFSETS ));

      doc.add(new Field("modified",
        DateTools. timeToString(f.lastModified(), DateTools.Resolution.MINUTE ),
        Field.Store. YES, Field.Index.NOT_ANALYZED ,Field.TermVector.WITH_POSITIONS_OFFSETS));

      BufferedReader br = new BufferedReader(read);
    doc.add(new Field( "contents", br));

为"path"和"modified"这2个域存储了TermVector信息。注意代码中的Field.TermVector. WITH_POSITIONS_OFFSETS。
     它表示既保存这两个域的词项的位置信息,又保存偏移信息。

     执行程序,发现生成的索引中包含了.tvx,.tvd,.tvf文件,下面就对他们进行分析。

1. 词项向量索引文件(.tvx,.tvd,.tvf)结构分析
1.1 作用

     还是和之前一样,再介绍词项向量索引文件结构之前,先来介绍一下它的作用也即存在的必要性。

     一个文档(Document)是含有多个域(Field),而每个域是可以含有1个或多个Term的。所谓Term,也就是Filed里的文本字符串经过分析(比如分词)后得到的一系列词项信息。

     而倒排索引里,记录的是由词项-->文档x,文档y,...文档z的这么一种关系,是一种反向的信息。

     而当你想知道某篇文档的某个域有多少个词项,这些词项(Term)在这个域包含的文本中出现的频率(TermFreq),出现的位置(Positions),每次出现对应的偏移(Offsets)时,这时候就需要TermVector索引出马了(PS:显然,这是一种正向索引)。

     什么?还是不太明白?那我举个例子。 (Note:我不太清楚具体的search engine是不是按我说的这样实现的,但我觉得应该是)
     
      如下图,搜索"尤文图斯"的时候,搜索结果的标题中对"尤文图斯"(或者同义词,juventus)都采取了飘红处理。

     标题是文档的一个域(field:title),这个域所含的文本字符串,是可以被lucene保存的(Field.Store.YES),当搜索完毕需要展示的时候,对标题所含的关键词做飘红可以明显提高UX。
     
     如果没有TermVector索引,那我们只能根据文档的id取出这个文档的title域的值,然后,在这个值里搜索"尤文图斯",显然,这是相当brute-force的策略。

     当我们有了TermVector之后,可以根据文档id查出这个文档对应的域的词项信息(偏移,频率等),如果这些词项里含有"尤文图斯"四个字的话,那我们就把它的偏移拿出来,然后在title中直接做飘红就好啦。

     正如Lucene in Action上面所说,提高标题飘红功能的性能,是TermVector索引一个重要的作用。
lucene索引结构(三)-词项向量(TermVector)索引文件结构分析

1.2. TermVector索引文件结构分析

     TermVector索引由三个文件构成:

     1).tvx文件: Document Index File。它的主要作用是索引"该文档->该文档的域在tvd文件中的偏移"以及"该文档->该文档第0个域在tvf文件中的偏移"。
     2).tvd文件: Document File。它的主要作用是保存每一个文档的域的ID递增和第1到...第NumFields-1域在tvf中相对于第0个域的偏移。
     3).tvf文件: Filed File。它的主要作用是保存每一个文档的所有域的所有TermVector的具体信息。
     
     相信你一定被1)和2)中的各种偏移绕晕了吧。
     其实是这样的,由于.tvf文件是把所有的文档的域的词项向量都一个一个的排在文件中,每一篇文档的域是集中排在一块的(请见下图tvf文件中专门用红色勾画的地方)。因此要想知道文档i的第j个域的词项向量存在哪,必须知道
     (1) 文档i的第0个域在tvf中的起始偏移地址,这个地址是从.tvx文件中取到的FieldPosition

     (2) 文档i的第j(j>0)个域在.tvx中相对于第0个域地址的相对偏移,这个地址是tvd中的FieldPositionDelta。

     最后就可以利用 "FieldPostion + 第j个域相对于第0个域的偏移" 得到 "文档i的第j个域" 在.tvf中的偏移地址了,从这个地址开始读取文件,就可以取出文档i的第j个域的所有信息了。这也是TermVector索引的关键所在,其实说白了就是"tvx存储文档i第0域的绝对偏移,tvd存储文档i的第j域的相对偏移。最后绝对+相对得到第j域在tvf中的绝对地址,从而实现索引"。

lucene索引结构(三)-词项向量(TermVector)索引文件结构分析

     好!下面开始介绍TermVector索引的三个文件各个字段的格式及其代表的意义啦。

     1) .tvx文件:
     TVXVersion,<DocumentPosition,FieldPosition>(二元组重复NumDocs次)
           TVXVersion, Int, 记录版本号,值取TermVectorsReader.FORMAT_CURRENT
    
           <DocumentPosition,FieldPosition>  :有多少个文档,就有多少个这个二元组。

             DocumentPosition, UInt64, 指向这个文档对应的FiledInformation在tvd中的绝对偏移地址。

             FieldPosition, UInt64  , 指向这个文档的第0个域对应的TermInformation在tvf中的绝对偏移地址。

       非常重要的一点,.tvx里的数据都是定长的,所以非常容易根据docId来确定文档在这个文件中的地址,docId * 16L + FORMAT_SIZE。        

    2).tvd文件:

          TVDVersion<NumFields, FieldNums, FieldPositions> (三元组重复NumDocs次)
          
           TVDVersion, Int ,版本号,值取 TermVectorsReader.FORMAT_CURRENT 。      

           <NumFields, FieldNums, FieldPositions>,有多少个文档,就有多少个这个三元祖。

          NumFields,VInt, 即这个文档的域的个数。
     
          FieldNums --> <FieldNumDelta>(NumFields次)

               FieldNumDelta --> VInt

          FieldPositions --> <FieldPositionDelta> (重复NumFields-1次,因为不用记录第0个的相对偏移)。

               FieldPositionDelta --> VLong,域相对偏移的数字,采用了对间距进行变长编码(VB编码,Variable-Byte)的方式来进行压缩(这种索引压缩技术可以看看《信息检索导论》的5.3节,原理是一样一样的)。
          
    3).tvf文件:

           TVFVersion<NumTerms, Position/Offset, TermFreqs> (三元组重复NumFields次)。               

            TVFVersion,版本号,Int (TermVectorsReader.FORMAT_CURRENT)  。

          <NumTerms, Position/Offset, TermFreqs>,该文档有多少个域,这个三元组就重复多少次(NumFields次),每1个三元组就是1个域的词项向量信息 。

               NumTerms  ,VInt,这个域所包含的词项向量的个数。
               
               Position/Offset,Byte, 记录了是否记录这个term的位置信息和偏移信息(最低位标识是否保存位置信息,次低位标识是否保存偏移信息)。
     
               TermFreqs --> <TermText, TermFreq, Positions?, Offsets?>(四元组重复NumTerms次,即这个域有多少个词项向量,就重复多少次,每个四元组记录了这个域的一个词项向量的信息)      


2. 深入.tvx,.tvd,.tvf文件内部
     
     如下图,这就是用UE打开着三个索引文件后得到的内容,在图中已经对各个文件的各个字段用不同颜色的框框住了,方便看些。

lucene索引结构(三)-词项向量(TermVector)索引文件结构分析

a) 由TVXVersion字段开头,int类型,占4个Byte。图中取值为0x04,这和TermVectorsReader.FORMAT_CURRENT是一致的,
                    // Changed strings to UTF8 with length-in-bytes not length-in-chars
              static   final   int   FORMAT_UTF8_LENGTH_IN_BYTES   = 4;

              // NOTE: always change this if you switch to a new format!
                 static   final   int   FORMAT_CURRENT   =   FORMAT_UTF8_LENGTH_IN_BYTES  ;

  
     b) 接下来是<DocumentPosition,FieldPosition>元组了

              这是一个定长数据DocumentPosition和FieldPosition各占8Byte。
                
            图中可看到DocumentPosition取值为0x04,代表文档0的域信息存储在.tvd文件的0x04位置上,分析.tvd文件,发现的确如此。看到那根红色的虚线了吧,正好指向.tvd文件的0x04地址。

            FieldPosition的取值为0x04,表示文档0的第0个域的TermInformation位于.tvf文件的0x04地址上。看到那根绿色的虚线了吧,正是表示这个指向关系的。

 2).tvd文件

            a) 由TVDVersion开头,Int类型,占4Byte。取值为0x04,和 TermVectorsReader.FORMAT_CURRENT一致。

          b) NumFields, VInt类型,不定长,图中的取值为0x02,代表文档0有2个域。这是和我自己写的用于创建索引的demo代码是吻合的。

          c) FieldNumDelta Vint类型

          d) FieldPositionDelta,这也是个不定长的类型,这里是0x14。现在可以更清楚的看到,由于文档0的域0的地址已经由.tvx中的FieldPosition指定,这里就只需要指定剩下的域之于域0的相对偏移,而这里每个文档只有2个域被配置了存储TermVector,因此就只剩一个FieldPositonDelta啦。然后,我们就可以根据FieldPosition+FieldPositionDelta,即0x04+0x14=0x18来得到文档0的域1的TermInfomation在.tvf中的起始地址了。这个请见那根蓝色的虚线,注意它指向的地方即是.tvf的0x18位置。

3).tvf文件
           a) 由TVFVersion开头(图中写成了TVDVersion,懒得改了),Int类型,4Byte, 图中取值为0x04,和 TermVectorsReader.FORMAT_CURRENT是一致的 。   

           b) 从0x04开始就是文档0的域0的TermInfomation在.tvf中的起始地址。0x04这个地方存储的是NumTerms,VInt类型,表示这个域含有的Term数量。

           c) Position/Offset,Byte,可以看到它的最低位和次低位都是1,表示它的位置信息和偏移信息都被记录了。  

           d) PrefixLength,VInt前缀长度,这里是0。Lucene采用了前缀编码这种字符串前端编码压缩机制,具体可以参见《信息检索导论》的5.2.2节,基本思想是一样的。

           e) Suffix,这就是需要添加在前缀后的后缀,这里因为没有前缀,因此这个后缀就是这个词项的全部了。它的长度为0x0c,即12,后面的Byte取出来就是它的值,201103040708,这就是'modified'域的唯一词项(由于这是个整数时间,是不会被分词的)。
          
           f) TermFreqs本来要重复多次,但这里NumTerms为1,这也就只出现了1次。它表示对应词项在域中的出现频率,这里为1,也很好理解,确实就出现了一次。。

           g) 接下来就是Offset了,类型是<Vint,Vint>。