1. 域(Field)的元数据信息(.fnm)文件分析
1.1 作用
我们在为文档建立索引的时候,会为文档添加不同的域(字段)来进行索引,使得索引结构能满足更多的查询语法。例如一个文档集被索引了author,modifydate字段,那么就能支持 'author:wangzhengnb AND modifydate>20120722' 这种Query语法。
更真实的例子就是搜索引擎普遍支持的site语法,也就是对网页索引时添加了site域,这样用户输入'意甲 site:sports.sina.com.cn' 时,搜索后端就能根据网页的site field来判断这个网页是不是新浪体育的,从而实现针对新浪体育频道域下的搜索。如下图,

而.fnm文件就是索引了这些域的元信息。比如一共有多少个域,某个域是否被存储、是否保存位置信息、是否保存偏移等等。
1.2 .fnm的物理结构分析
.fnm的物理结构如下图,

.fnm文件分为FNMVersion,FieldsCount, <FieldName, FieldBits>FieldsCount
FNMVersion, FieldsCount --> VInt
FieldName --> String
FieldBits --> Byte
FNMVersion对应版本号,对应Lucene-3.0.2为-2。
FieldsCount表示这个段中Field的总数。
<FieldName, FieldBits>元组是一个Field的信息,它一共重复FieldsCount次;其中的FieldName是Field名称,FieldBits表示这个域的属性,具体如下表。
1) 最低位如果为0,表示这个域不被索引(Field.Index.NO);为1,表示被索引(Field.Index.ANALYZED不仅索引而且分词,Field.Index.NOT_ANALYZED仅索引不分词),加入倒排表。
|
2) 次低位为0,表示不保存词向量(TermVector)(Field.TermVector.NO),为1表示保存词向量(Field.TermVector.YES) |
3) 倒数第三位如果为1表示在词向量中保存位置信息(Field.TermVector.WITH_POSITIONS) |
4) 倒数第四位如果为1表示在词向量中保存偏移量信息(Field.TermVector.WITH_OFFSETS) |
5) 倒数第五位如果为1表示不保存偏移量因子(Field.Index.NOT_ANALYZED_NO_NORMS) |
6) 倒数第六位如果为1表示保存Payload信息。 |
Notes:
1) 索引域(Indexed)和存储域(Stored)的区别
◦ 一个域为什么会被存储(store)而不被索引(Index)呢?在一个文档中的所有信息中,有这样一部分信息,可能不想被索引从而可以搜索到,但是当这个文档由于其他的信息被搜索到时,可以同其他信息一同返回。
◦ 举个例子,读研究生时,您好不容易写了一篇论文交给您的导师,您的导师却要他所第一作者而您做第二作者,然而您导师不想别人在论文系统中搜索您的名字时找到这篇论文,于是在论文系统中,把第二作者这个Field的Indexed设为false,这样别人搜索您的名字,永远不知道您写过这篇论文,只有在别人搜索您导师的名字从而找到您的文章时,在一个角落表述着第二作者是您 分信息,可能不想被索引从而可以搜索到,但是当这个文档于其他的信息被搜索到时,可以 同其他信息一同返回。
2) payload的使用
◦ 我们知道,索引是以倒排表形式存储的,对于每一个词,都保存了包含这个词的一个链表,当然为了加快查询速度,此链表多用跳跃表进行存储。
◦ Payload信息就是存储在倒排表中的,同文档号一起存放,多用于存储与每篇文档相关的一些信息。当然这部分信息也可以存储域里(stored Field),两者从功能上基本是一样的,然而当要存储的信息很多的时候,存放在倒排表里,利用跳跃表,有利于大大提高搜索速度。

利用Payload,我们可以为文档附加上一些自定义的信息。比如为文档增加一个自定义的Term "$$$_Internal_ID"以及声明特殊的Field "$$$_Internal_ID",用以为文档保存一份不同于Lucene生成的ID。这样在倒排里就可以有一个"$$$_Internal_ID"的词项指向有这些域的文档的倒排,从而查找这些文档的自定义的ID。
1.3 深入分析.fnm文件
用UE打开_0.fnm文件,下图是我用不同颜色标注的各个字段的信息。

1) FNMVersion, VInt FEFFFFFF0F, decode出来就是4294967294(126 + 127*128 + 127*128^2 + 127*128^3 + 15*128^4), 也就是-2了。
这和官方文档上叙述的"FNMVersion (added in 2.9) is always -2."是一致的。
不过让我略觉蛋疼的是官方文档上也清楚的写着"VInt - A variable-length format for positive integers ",尼玛这不是正数吗。靠int32溢出来转成-2。。。而且不就 是一个版本号么,搞个定长的int32数就完了呗,反正也就存一次,能耗多少空间嘛。。真是的,蛋疼。。
2) FieldsCount, 也是Vint, 0x03, 解码后就是3,和建索引的代码是一致的。一共加入了'path','modified'和'contents'三个域。
doc.add(new Field( "path", f.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED ));
doc.add(new Field("modified",DateTools. timeToString(f.lastModified(), DateTools.Resolution.MINUTE ),Field.Store. YES,Field.Index. NOT_ANALYZED));
BufferedReader br = new BufferedReader(read);
doc.add(new Field("contents", br));
后面跟着的就是FieldsCount个域。
3) 这里只展示第一个域。首先是FieldName,String类型,String也就是一个VInt表示长度len,后面跟着len个Byte。
这里的长度是04,解码也就是4。后面读出的4个Byte就是这个域的名字,也就是'path'了,从右侧也可以看到。
这之后跟着的就是下一个域,长度为08(也就是8),名字是'modified'。然后又是下一个。。
2. 域数据文件(.fdx和.fdt)分析
2.1 作用
前面提到的域元数据文件,是从一个整体上来对段中的域进行描述。
然而当你想查找某篇文档都有哪些Field,这篇文档的这些个Field都取些什么值,都有啥属性,那就得依靠域数据文件了。
PS:从上句话也可以看出来,这个域数据索引,也是一个典型的正向索引(文档ID--->文档域信息)。
域数据文件分成域数据索引(Field data index, .fdx)和域数据(Field data, .fdt)两部分,下面将对它们介绍。
2.2 域数据索引及域数据的物理结构分析
.fdx和.fdt文件的物理结构如下图所示:

坑爹的地方来了。这幅图原来在网上找到的时候是没有前面粉红色的Format Version字段的,连和代码一起下下来的官方文档里都没提这俩字段。
尼玛啊,这索引文件可是二进制的啊,用UE打开全是00 01 FF的这种有木有,怎么看都怎么不对,遂上网搜以及跟代码看。终于发现是Lucene2.9之后再.fdx和.fdt最前面加入了一个format version字段。。。可参见http://blog.youkuaiyun.com/a276202460/article/details/5650026。
上述索引和数据的关系大体如下:
段内检索了多少个文档,在.fdx中就有多少个FieldValuePosition,这是一个UInt64的定长数据,指向的是.fdt的一个绝对偏移地址。这个地址上保存的就是这篇文档的域信息。
显而易见,.fdx因为是一个由定长数据组成的记录,所以docID=n的文档的域数据地址,就被保存在索引文件.fdx的4+n*8的位置上,然后读出绝对偏移后,就可以读取.fdt的这个偏移上的数据,就是docID=n文档的域信息了。
一篇文档的域信息包括文档域的个数,域编号(文档内的编号,从0开始),域属性,域的值。
详细的解释如下:
域数据索引:
1) Format Version: Int32数据,Lucene-3.0.2的这个值对应的是2。
2) 之后就跟的是SegSize个(段的大小,也即文档总数,在segment索引可找到它的身影,见
http://blog.youkuaiyun.com/wangzhengnb/article/details/7771448) FieldValuesPosition,都是UInt64数据。表示.fdt文件中的绝对偏移。
域数据:
1) Format Version: Int32数据,Lucene-3.0.2的这个值对应的是2。
2) 之后跟着SegSize个DocFieldData。DocFieldData可不是定长数据,因为每个文档对应的某个域的取值都是不同的,文档也不都一定有相同数量的域等等。
3) DocField由FieldCount开头,是个Vint变长数据,标志这个文档一共有多少个域。
接下来就是1个Bits段,占1字节,解释见下表。
最低位为1表示这个域是分词的(tokenized) |
次低位为1表示这个域是二进制数据,否则是文本,也即字符串数据 |
倒数第三位为1表示这个域采用了压缩算法(ZLib) |
最后跟着的是Value段,Value可根据域的取值是二进制还是文本,或为BinaryValue类型或为String类型。
这里只讨论String类型的。同样String还是由VInt和Char bytes组成。
2.3 深入分析.fdx和.fdt文件
UE打开它俩,截图如下,因为文件很长,也没必要全部截完图,所以只截了开头的一部分。并把它俩贴到一副图里,方便叙述。

嗯哈?有点乱?不要怕,接下来一步步看,很简单。
首先看看.fdx文件:
1)由一个Format Version开头,占Int32空间,取值为0x02,也即2。
2) 紧跟着的是99个文档的域信息在.fdt中的绝对偏移。这里框取了前3个,它们的偏移分别是0x04,0x3F,0x7C。
看看下方的.fdt文件,3个红色的圈圈圈住的地方,就是这三个文档的域信息的开头的地方。
在看看.fdt文件:
1) 同样由一个Format Version开头,占Int32空间,取值为0x02,也即2。
2)接下来的是文档的域总数FieldCount,为2。等等!不是建立了path,modified和contents三个域嘛?!怎么只有俩!别着急,原来是contents只索引,不保存。如果每篇文档都保存全部的正文部分,那空间开销好大哦。
3) 接着的是这个域在文档中的编号,第一个绿圈里的数是00,第二个绿圈对应的是01,也即0号域和1号域。注意,这个编号是文档内的。到了下一篇文档,域编号又会从0开始啦。
4) 接下来是Bits了,取0。也即这个域不分词、为字符串类型、不压缩,这个和实际的索引建立程序是吻合的。
5) 之后是Value了,这里我把String类型拆成了Vint(紫色)和char bytes(棕色)来画,方便看些:)
可看到文档0的域0的value长度为0x28,也就是40。所以从接下来的0x08到0x2F就是文档0的域0的值,可以从右侧看到是它的路径,E:\lucene\....什么的。
如是分析文档0的域1的信息,都能和实际吻合。当域1的value在0x3E结束后,又开始了文档1的域信息,这印证了.fdx中文档1的偏移地址是0x3F的正确性。
3 Reference
[1] Apache Lucene - Index File Formats
http://lucene.apache.org/core/old_versioned_docs/versions/2_9_0/fileformats.html#File Naming
[2] forfuture1978的专栏
http://blog.youkuaiyun.com/forfuture1978
http://lucene.apache.org/core/old_versioned_docs/versions/2_9_0/fileformats.html#File Naming
[2] forfuture1978的专栏
http://blog.youkuaiyun.com/forfuture1978
[3] a276202460的专栏--边学边记(七) lucene索引结构四(_N.fdx,_N.fdt)