6. http://forfuture1978.javaeye.com 1.1 Lucene学习总结之一:全文检索的基本原理
左边保存的是一系列字符串,称为词典。
每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。
有了索引,便使保存的信息和要搜索的信息一致,可以大大加快搜索的速度。
比如说,我们要寻找既包含字符串“lucene”又包含字符串“solr”的文档,我们只需要以下几步:
1. 取出包含字符串“lucene”的文档链表。
2. 取出包含字符串“solr”的文档链表。
3. 通过合并链表,找出既包含“lucene”又包含“solr”的文件。
看到这个地方,有人可能会说,全文检索的确加快了搜索的速度,但是多了索引的过程,两者加起来不一定比
顺序扫描快多少。的确,加上索引的过程,全文检索不一定比顺序扫描快,尤其是在数据量小的时候更是如
此。而对一个很大量的数据创建索引也是一个很慢的过程。
然而两者还是有区别的,顺序扫描是每次都要扫描,而创建索引的过程仅仅需要一次,以后便是一劳永逸的
了,每次搜索,创建索引的过程不必经过,仅仅搜索创建好的索引就可以了。
这也是全文搜索相对于顺序扫描的优势之一:一次索引,多次使用。
三、如何创建索引
全文检索的索引创建过程一般有以下几步:
第一步:一些要索引的原文档(Document)。
为了方便说明索引创建过程,这里特意用两个文件为例:
文件一:Students should be allowed to go out with their friends, but not allowed to drink beer.
文件二:My friend Jerry went to school to see his students but found them drunk which is not
allowed.
第 6 / 199 页
9. http://forfuture1978.javaeye.com 1.1 Lucene学习总结之一:全文检索的基本原理
their 1
friend 1
allow 1
drink 1
beer 1
my 2
friend 2
jerry 2
go 2
school 2
see 2
his 2
student 2
find 2
them 2
drink 2
allow 2
2. 对字典按字母顺序进行排序。
Term Document ID
allow 1
allow 1
allow 2
beer 1
drink 1
drink 2
find 2
friend 1
friend 2
go 1
第 9 / 199 页
10. http://forfuture1978.javaeye.com 1.1 Lucene学习总结之一:全文检索的基本原理
go 2
his 2
jerry 2
my 2
school 2
see 2
student 1
student 2
their 1
them 2
3. 合并相同的词(Term)成为文档倒排(Posting List)链表。
第 10 / 199 页
36. http://forfuture1978.javaeye.com 1.4 Lucene学习总结之三:Lucene的索引文件格式 (2)
• SegCount
◦ 段(Segment)的个数。
◦ 如上图,此值为2。
• SegCount个段的元数据信息:
◦ SegName
▪ 段名,所有属于同一个段的文件都有以段名作为文件名。
▪ 如上图,第一个段的段名为"_0",第二个段的段名为"_1"
◦ SegSize
▪ 此段中包含的文档数
▪ 然而此文档数是包括已经删除,又没有optimize的文档的,因为在optimize之前,
Lucene的段中包含了所有被索引过的文档,而被删除的文档是保存在.del文件中的,在
搜索的过程中,是先从段中读到了被删除的文档,然后再用.del中的标志,将这篇文档
过滤掉。
▪ 如下的代码形成了上图的索引,可以看出索引了两篇文档形成了_0段,然后又删除了其
中一篇,形成了_0_1.del,又索引了两篇文档形成_1段,然后又删除了其中一篇,形成
_1_1.del。因而在两个段中,此值都是2。
IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new
StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);
writer.setUseCompoundFile(false);
indexDocs(writer, docDir);//docDir中只有两篇文档
//文档一为:Students should be allowed to go out with their friends, but not allowed to
drink beer.
//文档二为:My friend Jerry went to school to see his students but found them drunk
which is not allowed.
writer.commit();//提交两篇文档,形成_0段。
writer.deleteDocuments(new Term("contents", "school"));//删除文档二
writer.commit();//提交删除,形成_0_1.del
indexDocs(writer, docDir);//再次索引两篇文档,Lucene不能判别文档与文档的不同,因而算两
篇新的文档。
writer.commit();//提交两篇文档,形成_1段
writer.deleteDocuments(new Term("contents", "school"));//删除第二次添加的文档二
writer.close();//提交删除,形成_1_1.del
第 36 / 199 页
62. http://forfuture1978.javaeye.com 1.5 Lucene学习总结之三:Lucene的索引文件格式 (3)
• 此文件包含TermCount个项,每一个词都有一项,因为每一个词都有自己的倒排表。
• 对于每一个词的倒排表都包括两部分,一部分是倒排表本身,也即一个数组的文档号及词频,另一部分
是跳跃表,为了更快的访问和定位倒排表中文档号及词频的位置。
• 对于文档号和词频的存储应用的是差值规则和或然跟随规则,Lucene的文档本身有以下几句话,比较
难以理解,在此解释一下:
For example, the TermFreqs for a term which occurs once in document seven and three
times in document eleven, with omitTf false, would be the following sequence of VInts:
15, 8, 3
If omitTf were true it would be this sequence of VInts instead:
7,4
首先我们看omitTf=false的情况,也即我们在索引中会存储一个文档中term出现的次数。
例子中说了,表示在文档7中出现1次,并且又在文档11中出现3次的文档用以下序列表示:15,
8,3.
那这三个数字是怎么计算出来的呢?
首先,根据定义TermFreq --> DocDelta[, Freq?],一个TermFreq结构是由一个DocDelta后面或
许跟着Freq组成,也即上面我们说的A+B?结构。
DocDelta自然是想存储包含此Term的文档的ID号了,Freq是在此文档中出现的次数。
所以根据例子,应该存储的完整信息为[DocID = 7, Freq = 1] [DocID = 11, Freq = 3](见全文检
索的基本原理章节)。
然而为了节省空间,Lucene对编号此类的数据都是用差值来表示的,也即上面说的规则2,Delta
规则,于是文档ID就不能按完整信息存了,就应该存放如下:
[DocIDDelta = 7, Freq = 1][DocIDDelta = 4 (11-7), Freq = 3]
然而Lucene对于A+B?这种或然跟随的结果,有其特殊的存储方式,见规则3,即A+B?规则,如果
DocDelta后面跟随的Freq为1,则用DocDelta最后一位置1表示。
如果DocDelta后面跟随的Freq大于1,则DocDelta得最后一位置0,然后后面跟随真正的值,从而
对于第一个Term,由于Freq为1,于是放在DocDelta的最后一位表示,DocIDDelta = 7的二进制
是000 0111,必须要左移一位,且最后一位置一,000 1111 = 15,对于第二个Term,由于Freq
第 62 / 199 页