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索引一个重要的作用。
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中的绝对地址,从而实现索引"。
好!下面开始介绍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打开着三个索引文件后得到的内容,在图中已经对各个文件的各个字段用不同颜色的框框住了,方便看些。
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>。