您的位置:首页 > 其它

Lucene的索引文件格式

2012-12-06 18:14 441 查看
这是我学习lucene的笔记,完全参考《lucene原理与代码分析》,作者的博客地址:

http://blog.csdn.net/forfuture1978

之前也是尝试着看了的,但是发现一深入源码我就懵了,犯了只见树木不见森林的错误。

如何有效阅读源代码,蔡学镛的经验:

1) 先阅读架构文档 2) 根据架构,将源码文件以模块(或上下层级)分类 3) 从最独立(依赖性最小)的模块代码读起 4) 阅读该模块功能文档 5) 阅读该模块源代码 6) 一边阅读一边整理「调用关系表」(很久以前我的某条微博提过)7) goto 3

Lucene中最重要的是部分是索引和搜索,整个都是围绕这两个中心展开的,下面我们来学习索引的文件格式,lucene的索引结构是有层次结构的,主要分以下几个层次:

1. 索引(index):

一个文件夹中的所有文件构成一个lucene索引

2. 段(segment):

一个索引可以包含多个段,段之间是独立的,添加新文档可以生产新的段,不同的段可以合并。上图中,具有相同前缀文件的属于同一个段,图中共两个段“_0”“_1”,segments.gen和segments_5是段的元数据文件,也即他们保存了段得属性信息。

3. 文档(document):

文档是我们建索引的基本单位,不同的文档是保存在不同的段中,一个段可以保含多篇文档,新添加的文档是单独保存在一个新生成的段中,随着段得合并,不同的文档合并到同一个段中。

4. 域(field):

一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域中,不同域的索引方式可以不同。

5. 词(term):

词是索引的最小单位,是经过词法分析和语言处理的字符串。

Lucene索引中即保存了正向信息,也保存了反向信息。

正向信息:

按层次保存了从索引一直到词的包含关系:索引(index)—> 段(segment)—>文档(document)—>域(field)—>词(term)也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了那些词,既然是层次结构,则每个层次都保存了本层次的信息以及下一层次的元信息也即属性信息。如上图,包含正向信息的文件有:



反向信息:

保存了词典到倒排表的映射:词(term)—>文档(document)

如上图,包含反向信息的文件有:



基本规则:

1. 前缀后缀规则(prefix+suffix)

所谓前缀后缀规则,即当某个词和前一个词有共同的前缀的

时候,后面的词仅仅保存前缀在词中的偏移(offset),以及除前缀以外的字符串(称为后缀)。



2. 差值规则(delta)

所谓差值规则(Delta)就是先后保存两个整数的时候,后面的整数仅仅保存和前面整数的差即可。



3. 或然跟随规则(A,B?)

在Lucene中,采取以下的方式:A的值左移一位,空出最后一位,作为标志位,来表示后

面是否跟随B,所以在这种情况下,A/2是真正的A原来的值。



4. 跳跃表规则(skip list)

跳跃表(Skip List)是如图的一种数据结构,有以下几个基本特征:

1)元素是按顺序排列的,在Lucene中,或是按字典顺序排列,或是按从小到大顺序排列。

2)跳跃是有间隔的(Interval),也即每次跳跃的元素数,间隔是事先配置好的,如图跳跃表的间隔为3。

3)跳跃表是由层次的(level),每一层的每隔指定间隔的元素构成上一层,如图跳跃表共有2层。



具体格式:

1. 正向信息

Index –> Segments (segments.gen, segments_N) –> Field(fnm, fdx, fdt) –> Term (tvx, tvd, tvf)

上面的层次结构不是十分的准确,因为segments.gen 和segments_N 保存的是段(segment)

的元数据信息(metadata),其实是每个Index 一个的,而段的真正的数据信息,是保存在域

(Field)和词(Term)中的。





上面这些都是作者为了说明方便只是列出了其中主要的步骤。

段在形成的过程中总是后一个段得域和词向量信息存储到前一个段中。段讲完了现在来讲域。

一个段(segment)包含多个域,每个域都有一些元数据信息,保存在.fnm文件中,.fnm文件格式如下:



FNMVersion 是fnm文件的版本号,对于Lucene 2.9为-2

· FieldsCount :

§ 域的数目
· 一个数组的域(Fields) :
§FieldName:域名,如"title","modified","content"等。
§ FieldBits:一系列标志位,表明对此域的索引方式
§ 最低位:1表示此域被索引,0则不被索引。所谓被索引,也即放到倒排表中去。
§ 仅仅被索引的域才能够被搜到。
§ Field.Index.NO则表示不被索引。
§ Field.Index.ANALYZED则表示不但被索引,而且被分词,比如索引"hello world"后,无论是搜"hello",还是搜"world"都能够被搜到。
§ Field.Index.NOT_ANALYZED表示虽然被索引,但是不分词,比如索引"hello world"后,仅当搜"hello world"时,能够搜到,搜"hello"和搜"world"都搜不到。
§ 一个域出了能够被索引,还能够被存储,仅仅被存储的域是搜索不到的,但是能通过文档号查到,多用于不想被搜索到,但是在通过其它域能够搜索到的情况下,能够随着文档号返回给用户的域。
§ Field.Store.Yes则表示存储此域,Field.Store.NO则表示不存储此域。
§ 倒数第二位:1表示保存词向量,0为不保存词向量。
§ Field.TermVector.YES表示保存词向量。
§ Field.TermVector.NO表示不保存词向量。
§ 倒数第三位:1表示在词向量中保存位置信息。
§ Field.TermVector.WITH_POSITIONS
§ 倒数第四位:1表示在词向量中保存偏移量信息。
§ Field.TermVector.WITH_OFFSETS
§ 倒数第五位:1表示不保存标准化因子
§ Field.Index.ANALYZED_NO_NORMS
§ Field.Index.NOT_ANALYZED_NO_NORMS
§ 倒数第六位:是否保存payload
· payload的使用

§ 我们知道,索引是以倒排表形式存储的,对于每一个词,都保存了包含这个词的一个链表,当然为了加快查询速度,此链表多用跳跃表进行存储。
§
Payload信息就是存储在倒排表中的,同文档号一起存放,多用于存储与每篇文档相关的一些信息。当然这部分信息也可以存储域里(stored Field),两者从功能上基本是一样的,然而当要存储的信息很多的时候,存放在倒排表里,利用跳跃表,有利于大大提高搜索速度。
§ Payload的存储方式如下图:



Payload主要有以下几种用法:
§ 存储每个文档都有的信息:比如有的时候,我们想给每个文档赋一个我们自己的文档号,而不是用Lucene自己的文档号。于是我们可以声明一个特殊的域(Field)"_ID"和特殊的词(Term)"_ID",使得每篇文档都包含词"_ID",于是在词"_ID"的倒排表里面对于每篇文档又有一项,每一项都有一个payload,于是我们可以在payload里面保存我们自己的文档号。每当我们得到一个Lucene的文档号的时候,就能从跳跃表中查找到我们自己的文档号。

//声明一个特殊的域和特殊的词public static final String ID_PAYLOAD_FIELD = "_ID";
public static final String ID_PAYLOAD_TERM = "_ID";
public static final Term ID_TERM = new Term(ID_PAYLOAD_TERM, ID_PAYLOAD_FIELD);
//声明一个特殊的TokenStream,它只生成一个词(Term),就是那个特殊的词,在特殊的域里面。
static class SinglePayloadTokenStream extends TokenStream {
private Token token;
private boolean returnToken = false;

SinglePayloadTokenStream(String idPayloadTerm) {
char[] term = idPayloadTerm.toCharArray();
token = new Token(term, 0, term.length, 0, term.length);
}
void setPayloadValue(byte[] value) {
token.setPayload(new Payload(value));
returnToken = true;
}
public Token next() throws IOException {
if (returnToken) {
returnToken = false;
return token;
} else {
return null;
}
}
}
//对于每一篇文档,都让它包含这个特殊的词,在特殊的域里面
SinglePayloadTokenStream singlePayloadTokenStream = new SinglePayloadTokenStream(ID_PAYLOAD_TERM);
singlePayloadTokenStream.setPayloadValue(long2bytes(id));
doc.add(new Field(ID_PAYLOAD_FIELD, singlePayloadTokenStream));
//每当得到一个Lucene的文档号时,通过以下的方式得到payload里面的文档号long id = 0;
//	TermPositions tp = reader.termPositions(ID_PAYLOAD_TERM);
//	boolean ret = tp.skipTo(docID);tp.nextPosition();
//	int payloadlength = tp.getPayloadLength();
//	byte[] payloadBuffer = new byte[payloadlength];
//	tp.getPayload(payloadBuffer, 0);
//	id = bytes2long(payloadBuffer);
//	tp.close();

影响词的评分
§ 在Similarity抽象类中有函数public float scorePayload(byte [] payload, int offset, int length) 可以根据payload的值影响评分。
·读取域元数据信息的代码如下:
FieldInfos.read(IndexInput, String)
int firstInt = input.readVInt();
size = input.readVInt();
for (int i = 0; i < size; i++)
String name = input.readString();
byte bits = input.readByte();
boolean isIndexed = (bits & IS_INDEXED) != 0;
boolean storeTermVector = (bits & STORE_TERMVECTOR) != 0;
boolean storePositionsWithTermVector = (bits & STORE_POSITIONS_WITH_TERMVECTOR) != 0;
boolean storeOffsetWithTermVector = (bits & STORE_OFFSET_WITH_TERMVECTOR) != 0;
boolean omitNorms = (bits & OMIT_NORMS) != 0;
boolean storePayloads = (bits & STORE_PAYLOADS) != 0;
boolean omitTermFreqAndPositions = (bits & OMIT_TERM_FREQ_AND_POSITIONS) != 0;

域(Field)的数据信息(.fdt,.fdx)



域数据文件(fdt):
§ 真正保存存储域(stored field)信息的是fdt文件
§ 在一个段(segment)中总共有segment size篇文档,所以fdt文件中共有segment size个项,每一项保存一篇文档的域的信息
§ 对于每一篇文档,一开始是一个fieldcount,也即此文档包含的域的数目,接下来是fieldcount个项,每一项保存一个域的信息。
§ 对于每一个域,fieldnum是域号,接着是一个8位的byte,最低一位表示此域是否分词(tokenized),倒数第二位表示此域是保存字符串数据还是二进制数据,倒数第三位表示此域是否被压缩,再接下来就是存储域的值,比如new Field("title", "lucene in action", Field.Store.Yes, …),则此处存放的就是"lucene in action"这个字符串。
域索引文件(fdx)
§ 由域数据文件格式我们知道,每篇文档包含的域的个数,每个存储域的值都是不一样的,因而域数据文件中segment size篇文档,每篇文档占用的大小也是不一样的,那么如何在fdt中辨别每一篇文档的起始地址和终止地址呢,如何能够更快的找到第n篇文档的存储域的信息呢?就是要借助域索引文件。
§ 域索引文件也总共有segment size个项,每篇文档都有一个项,每一项都是一个long,大小固定,每一项都是对应的文档在fdt文件中的起始地址的偏移量,这样如果我们想找到第n篇文档的存储域的信息,只要在fdx中找到第n项,然后按照取出的long作为偏移量,就可以在fdt文件中找到对应的存储域的信息。
读取域数据信息的代码如下:

Document FieldsReader.doc(int n, FieldSelector fieldSelector)
· long position = indexStream.readLong();//indexStream points to ".fdx"
· fieldsStream.seek(position);//fieldsStream points to "fdt"
· int numFields = fieldsStream.readVInt();
· for (int i = 0; i < numFields; i++)
§ int fieldNumber = fieldsStream.readVInt();
§ byte bits = fieldsStream.readByte();
§ boolean compressed = (bits & FieldsWriter.FIELD_IS_COMPRESSED) != 0;
§ boolean tokenize = (bits & FieldsWriter.FIELD_IS_TOKENIZED) != 0;
§ boolean binary = (bits & FieldsWriter.FIELD_IS_BINARY) != 0;
§ if (binary)
§ int toRead = fieldsStream.readVInt();
§ final byte[] b = new byte[toRead];
§ fieldsStream.readBytes(b, 0, b.length);
§ if (compressed)
§ int toRead = fieldsStream.readVInt();
§ final byte[] b = new byte[toRead];
§ fieldsStream.readBytes(b, 0, b.length);
§ uncompress(b),
§ else
§ fieldsStream.readString()
词向量(Term Vector)的数据信息(.tvx,.tvd,.tvf)



词向量信息是从索引(index)到文档(document)到域(field)到词(term)的正向信息,有了词向量信息,我们就可以得到一篇文档包含那些词的信息。
词向量索引文件(tvx)
§ 一个段(segment)包含N篇文档,此文件就有N项,每一项代表一篇文档。
§ 每一项包含两部分信息:第一部分是词向量文档文件(tvd)中此文档的偏移量,第二部分是词向量域文件(tvf)中此文档的第一个域的偏移量。
词向量文档文件(tvd)
§ 一个段(segment)包含N篇文档,此文件就有N项,每一项包含了此文档的所有的域的信息。
§ 每一项首先是此文档包含的域的个数NumFields,然后是一个NumFields大小的数组,数组的每一项是域号。然后是一个(NumFields - 1)大小的数组,由前面我们知道,每篇文档的第一个域在tvf中的偏移量在tvx文件中保存,而其他(NumFields - 1)个域在tvf中的偏移量就是第一个域的偏移量加上这(NumFields - 1)个数组的每一项的值。
词向量域文件(tvf)
§ 此文件包含了此段中的所有的域,并不对文档做区分,到底第几个域到第几个域是属于那篇文档,是由tvx中的第一个域的偏移量以及tvd中的(NumFields - 1)个域的偏移量来决定的。
§ 对于每一个域,首先是此域包含的词的个数NumTerms,然后是一个8位的byte,最后一位是指定是否保存位置信息,倒数第二位是指定是否保存偏移量信息。然后是NumTerms个项的数组,每一项代表一个词(Term),对于每一个词,由词的文本TermText,词频TermFreq(也即此词在此文档中出现的次数),词的位置信息,词的偏移量信息。
读取词向量数据信息的代码如下:
TermVectorsReader.get(int docNum, String field, TermVectorMapper)
· int fieldNumber = fieldInfos.fieldNumber(field);//通过field名字得到field号
· seekTvx(docNum);//在tvx文件中按docNum文档号找到相应文档的项
· long tvdPosition = tvx.readLong();//找到tvd文件中相应文档的偏移量
· tvd.seek(tvdPosition);//在tvd文件中按偏移量找到相应文档的项
· int fieldCount = tvd.readVInt();//此文档包含的域的个数。
· for (int i = 0; i < fieldCount; i++) //按域号查找域
§ number = tvd.readVInt();
§ if (number == fieldNumber)
§ found = i;
· position = tvx.readLong();//在tvx中读出此文档的第一个域在tvf中的偏移量
· for (int i = 1; i <= found; i++)
§ position += tvd.readVLong();//加上所要找的域在tvf中的偏移量
· tvf.seek(position);
· int numTerms = tvf.readVInt();
· byte bits = tvf.readByte();
· storePositions = (bits & STORE_POSITIONS_WITH_TERMVECTOR) != 0;
· storeOffsets = (bits & STORE_OFFSET_WITH_TERMVECTOR) != 0;
· for (int i = 0; i < numTerms; i++)
§ start = tvf.readVInt();
§ deltaLength = tvf.readVInt();
§ totalLength = start + deltaLength;
§ tvf.readBytes(byteBuffer, start, deltaLength);
§ term = new String(byteBuffer, 0, totalLength, "UTF-8");
§ if (storePositions)
§ positions = new int[freq];
§ int prevPosition = 0;
§ for (int j = 0; j < freq; j++)
§ positions[j] = prevPosition + tvf.readVInt();
§ prevPosition = positions[j];
§ if (storeOffsets)
§ offsets = new TermVectorOffsetInfo[freq];
§ int prevOffset = 0;
§ for (int j = 0; j < freq; j++)
§ int startOffset = prevOffset + tvf.readVInt();
§ int endOffset = startOffset + tvf.readVInt();
§ offsets[j] = new TermVectorOffsetInfo(startOffset, endOffset);
§ prevOffset = endOffset;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: