上次随笔写的Lucene.Net 初学笔记 - 介绍,有许多前辈让我知道了Lucene.Net已经不再更新,最后的版本写到2.9.2,不过只更新在svn上。我上次下载是官方正式发布的版本,只有2.0。若有兴趣下载最新版本看,可以从chunk下载:https://svn.apache.org/repos/asf/lucene/lucene.net/trunk/C%23/
另外eaglet前辈的hubbledotnet,是一个基于数据库的索引和搜索的引擎,它数据库的概念和用类似于SQL的查询语句,感觉都很实用,再加上它较于Lucene.Net的性能更优,以后将会更有前途。强烈支持eaglet能继续更新。
学Lucene.Net是因为它学习资源多,java版本文档对它也有参考意义。既然开始了,想还是快速把Lucene.Net过一般,看一下它到底是如何工作的,对以后学习其他引擎也有帮助。
今天看一下Lucene.Net建立索引的过程。记得上次提到方法
IndexWriter.AddDocument
但没有说如何构造它的参数 Document 对象。下面这段代码来自DemoLib项目中FileDocument文件:
1: // make a new, empty document
2: Document doc = new Document();
3:
4: // Add the path of the file as a field named "path". Use a field that is
5: // indexed (i.e. searchable), but don't tokenize the field into words.
6: doc.Add(new Field("path", f.FullName, Field.Store.YES, Field.Index.UN_TOKENIZED));
7:
8: // Add the last modified date of the file a field named "modified". Use
9: // a field that is indexed (i.e. searchable), but don't tokenize the field
10: // into words.
11: doc.Add(new Field("modified", DateTools.TimeToString(f.LastWriteTime.Ticks, DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.UN_TOKENIZED));
12:
13: // Add the contents of the file to a field named "contents". Specify a Reader,
14: // so that the text of the file is tokenized and indexed, but not stored.
15: // Note that FileReader expects the file to be in the system's default encoding.
16: // If that's not the case searching for special characters will fail.
17: doc.Add(new Field("contents", new System.IO.StreamReader(f.FullName, System.Text.Encoding.Default)));
这里一共创建了3个字段(field)
1. path是文件路径,存储字段并索引,但是不会对它进行分词
2. modified是最后修改时间,存储字段并索引,也不会对它进行分词
3. contents是文件正文,不存储因为它太大,建立索引,对它进行分词
AddDocument方法其实做的事情就是对这三个字段建立索引,存储和建立逆向索引,就是从分词到文档的索引。
要了解索引,首先要知道下面几个概念:
index,就是索引,一个索引包含一连串的文档
document,文档,一个文档包含一连串的字段
field,字段,一个字段包含一连串的词(term)
term,词,就是一个字符串
inverted index, 逆向索引,就是根据term来索引document,类似于书籍最后的对词语的索引,映射到书中出现词的段落。
Segments,子索引,一个Lucene.Net的索引可以由多个segment组成。看Lucene.Net实现,每个segment是对应每个document的索引。
每个Segment index包含下面内容:
- 字段名字
- 存储的字段值
- 字典
- 词语出现的频率信息
- 词语映射到文档中的信息
- Normalization factors 用来计算字段的被查找到的分数
- 词向量
最终这些信息都将会以文件形式保存,下面简单介绍一下索引文件的结构:
Segments文件,是多个segment的索引,主要包含了Lucene.Net的版本信息,和每个segment的命名,大小。
segment的命名是用递增+1的名字:
1: return "_" + SupportClass.Number.ToString(segmentInfos.counter++, SupportClass.Number.MAX_RADIX);
以下其他后缀名的文件,都是以segment名字作为文件名。
Field Infos (.fnm)文件,是对多个field的索引,包含field的数量,以及各个field的名字和field的属性信息。
field属性信息是以bit来存储的,用它可以知道field是否建立索引,term向量相关信息, 以及其他一些属性
1: public void Write(IndexOutput output)
2: {
3: output.WriteVInt(Size());
4: for (int i = 0; i < Size(); i++)
5: {
6: FieldInfo fi = FieldInfo(i);
7: byte bits = (byte) (0x0);
8: if (fi.isIndexed)
9: bits |= IS_INDEXED;
10: if (fi.storeTermVector)
11: bits |= STORE_TERMVECTOR;
12: if (fi.storePositionWithTermVector)
13: bits |= STORE_POSITIONS_WITH_TERMVECTOR;
14: if (fi.storeOffsetWithTermVector)
15: bits |= STORE_OFFSET_WITH_TERMVECTOR;
16: if (fi.omitNorms)
17: bits |= OMIT_NORMS;
18: output.WriteString(fi.name);
19: output.WriteByte(bits);
20: }
21: }
FieldIndex (.fdx)文件,是field的index文件,存储了每个field值在field值文件中的位置信息
FieldData (.fdt)文件,field值文件,需要存储字段的数量,每个字段的编号,属性bits,和值数据
在说term字典文件之前,先看一下Lucene.Net是如何invert document的。具体可以看Lucene.Net.Index.DocumentWriter.InvertDocument(Document doc)方法。我把简化的代码贴在这里:
1: private void InvertDocument(Document doc)
2: {
3: foreach(Field field in doc.Fields())
4: {
5: // Get field information
6: if (length > 0)
7: position += analyzer.GetPositionIncrementGap(fieldName);
8:
9: if (field.IsIndexed())
10: {
11: if (!field.IsTokenized())
12: { // add field token information
13: }
14: else
15: {
16: System.IO.TextReader reader; // find or make Reader
17: if (field.ReaderValue() != null)
18: reader = field.ReaderValue();
19: else if (field.StringValue() != null)
20: reader = new System.IO.StringReader(field.StringValue());
21: else
22: throw new System.ArgumentException("field must have either String or Reader value");
23:
24: // Tokenize field and add to postingTable
25: TokenStream stream = analyzer.TokenStream(fieldName, reader);
26: try
27: {
28: Token lastToken = null;
29: for (Token t = stream.Next(); t != null; t = stream.Next())
30: {
31: // add token information
32: }
33: finally
34: {
35: stream.Close();
36: }
37: }
38: }
39: }
40: }
Analyzer的TokenStream就是用来把field value分词成token进而转化成term。具体分词算法根据不同的analyzer而不同,这里不详述了。
被分词以后的term将会以附带位置的信息存储在一个list中。在这个list将保存每个term和他们每次出现在document中position的信息,当然position的数量也表示term出现的频率。下面说到和term字典相关的文件就是把这个list的数组结构保存在文件中:
TermInfoFile (.tis) term信息文件, 是对所有term的一个索引,主要包含了term数量,以及每个term的一些信息,这个信息有些复杂,需要详述:
从文档中看来,它包含很多信息
TermInfo --> <Term, DocFreq, FreqDelta, ProxDelta, SkipDelta>
Term --> <PrefixLength, Suffix, FieldNum>
Suffix --> String
PrefixLength, DocFreq, FreqDelta, ProxDelta, SkipDelta
--> VInt
关于PrefixLength, Suffix, FieldNum,用代码解释比较清楚:
1: private void WriteTerm(Term term)
2: {
3: int start = StringHelper.StringDifference(this.lastTerm.text, term.text);
4: int length = term.text.Length - start;
5: this.output.WriteVInt(start);
6: this.output.WriteVInt(length);
7: this.output.WriteChars(term.text, start, length);
8: this.output.WriteVInt(this.fieldInfos.FieldNumber(term.field));
9: this.lastTerm = term;
10: }
11:
DocFreq, FreqDelta, ProxDelta, SkipDelta
主要是记录term指向到频度文件和位置文件的位置。
1: this.output.WriteVInt(ti.docFreq);
2: this.output.WriteVLong(ti.freqPointer - this.lastTi.freqPointer);
3: this.output.WriteVLong(ti.proxPointer - this.lastTi.proxPointer);
4: if (ti.docFreq >= this.skipInterval)
5: {
6: this.output.WriteVInt(ti.skipOffset);
7: }
FreqFile (.frq) 频度文件,存储了每个term出现在每个document的编号和出现的次数。
ProxFile (.prx) 位置信息文件,存储了term出现在document中出现次数和位置的信息。
最后是建立对词向量的索引文件:
DocumentIndex (.tvx) 文件向量索引文件,记录了对(.tvd)文件地址的索引。
Document (.tvd) 文件向量文件, 记录了每个document中每个field对应到.tvf文件中的地址。
Field (.tvf) 字段向量文件,记录了每个字段中词向量的信息,包括一个词列表和他们出现的次数和位置信息
今天先说到这里,还有一些概念并没有弄得很明白,包括文档中提到的Interval,SkipDelta, Normalization Factors, 以及词向量的具体应用,为什么它和前面的一些索引文件保存的信息是重复的。这些在以后的文章中再解释吧。
这篇文章主要内容来自:http://lucene.apache.org/java/2_1_0/fileformats.html