Lucene简单介绍(该部分摘自网络)
Lucene是一个高效的,基于Java的全文检索库。
所以在了解Lucene之前要费一番工夫了解一下全文检索。
那么什么叫做全文检索呢?这要从我们生活中的数据说起。
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
- 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
- 非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
当然有的地方还会提到第三种,半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。
非结构化数据又一种叫法叫全文数据。
按照数据的分类,搜索也分为两种:
- 对结构化数据的搜索:如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows搜索对文件名,类型,修改时间进行搜索等。
- 对非结构化数据的搜索:如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。
对非结构化数据也即对全文数据的搜索主要有两种方法:
一种是顺序扫描法(Serial Scanning):所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用windows的搜索也可以搜索文件内容,只是相当的慢。如果你有一个80G硬盘,如果想在上面找到一个内容包含某字符串的文件,不花他几个小时,怕是做不到。Linux下的grep命令也是这一种方式。大家可能觉得这种方法比较原始,但对于小数据量的文件,这种方法还是最直接,最方便的。但是对于大量的文件,这种方法就很慢了。
有人可能会说,对非结构化数据顺序扫描很慢,对结构化数据的搜索却相对较快(由于结构化数据有一定的结构可以采取一定的搜索算法加快速度),那么把我们的非结构化数据想办法弄得有一定结构不就行了吗?
这种想法很天然,却构成了全文检索的基本思路,也即将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。
这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。
这种说法比较抽象,举几个例子就很容易明白,比如字典,字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。
这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。
下面这幅图来自《Lucene in action》,但却不仅仅描述了Lucene的检索过程,而是描述了全文检索的一般过程。
全文检索大体分两个过程,索引创建(Indexing)和搜索索引(Search)。
- 索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
- 搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程
详情可以参考http://forfuture1978.iteye.com/blog/546771
Lucene3.5 建立索引总结
Lucene的索引结构是有层次结构的,主要分以下几个层次:
- 索引(Index):
- 在Lucene中一个索引是放在一个文件夹中的。
- 如上图,同一文件夹中的所有的文件构成一个Lucene索引。
- 段(Segment):
- 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
- 如上图,具有相同前缀文件的属同一个段,图中共两个段 "_0" 和 "_1"。
- segments.gen和segments_5是段的元数据文件,也即它们保存了段的属性信息。
- 文档(Document):
- 文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
- 新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
- 域(Field):
- 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。
- 不同域的索引方式可以不同,在真正解析域的存储的时候,我们会详细解读。
- 词(Term):
- 词是索引的最小单位,是经过词法分析和语言处理后的字符串。
(一)Field相当于数据库表的字段
1.其中的Field(字段或域)的构造方法有以下几种
Field(String name, boolean internName, String value, Field.Store store, Field.Index index, Field.TermVector termVector) 常用的
Field(String name, byte[] value)
Field(String name, byte[] value, int offset, int length)
Field(String name, Reader reader)
Field(String name, Reader reader, Field.TermVector termVector) 该Reader当在值是从文件中读取很长的数据时用,单身磁盘I/O操作太多效率太低了
Field(String name, String value, Field.Store store, Field.Index index)
Field(String name, String value, Field.Store store, Field.Index index, Field.TermVector termVector)
Field(String name, TokenStream tokenStream)
Field(String name, TokenStream tokenStream, Field.TermVector termVector)
l 其中 Field.Store表示是否存储,值分别为YES ,NO
l Field.Index 表示是否索引(允许检索),它已经包括了是否分词,其值有以下几种
ANALYZED 分词并索引
Index the tokens produced by running the field's value through an Analyzer.
ANALYZED_NO_NORMS 分词索引 但是禁用存储规范
Expert: Index the tokens produced by running the field's value through an Analyzer, and also separately disable the storing of norms.
NO 不索引,当然肯定不会去分词
Do not index the field value.
NOT_ANALYZED 索引但不会分词
Index the field's value without using an Analyzer, so it can be searched.
NOT_ANALYZED_NO_NORMS 不索引 同时禁用存储规范
l Field.TermVector 表示对Field词条向量是否进行存储,(具体什么意思我也还没弄明白)其值有以下几种
NO 不存储词条向量
Do not store term vectors.
WITH_OFFSETS 存储词条和其偏移量
Store the term vector + Token offset information
WITH_POSITIONS 存储词条和其位置
Store the term vector + token position information
WITH_POSITIONS_OFFSETS 存储词条和 词条偏移量及其位置
Store the term vector + Token position and offset information
YES 存储每个document的词条向量
Store the term vectors of each document.
2.document:简单的说就像数据库中的一条记录,它的每个字段就是Field。注意:数据库中你建立了字段,所有的记录具有相同记录,即数据库是以字段为主导的;而Lucene是以document为主导的,同一个索引(相当于数据库中一张表)中不同的document是可以不一样的。下面介绍docuemnt常用几种方法:
add(Fieldable field) 将一个Field 加入document,注意Field是实现了Fieldable接口的类
get(String name) 返回当前docuement指定Field名的Field值
getBoost() 返回索引时设置的加权因子(具体到底是什么没搞明白)
Returns, at indexing time, the boost factor as set by setBoost(float).
getFieldable(String name) 看英语吧,这个很简单
Returns a field with the given name if any exist in this document, or null.
getFieldables(String name) 看英语吧,这个很简单,只不过返回的是多个
Returns an array of Fieldables with the given name.
getFields()
Returns a List of all the fields in a document.
getFields(String name)
Deprecated. use getFieldable(java.lang.String) instead and cast depending on data type.
getValues(String name) 返回指定Field名的Field值的数组
Returns an array of values of the field specified as the method parameter.
removeField(String name) 移除当前document的指定的Field
Removes field with the specified name from the document.
removeFields(String name) 移除多个Field
Removes all fields with the given name from the document.
setBoost(float boost)
Sets a boost factor for hits(词条) on any field of this document.
toString()
Prints the fields of a document for human consumption.
(三)、Analyzer是Lucene的分析工具,无论是建立索引还是搜索过程,该类是一个抽象类,所以网上常用适合Lucene的分词器都是它的继承,例如用Lucene自带的StandardAnalyzer;如 Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_35)
(四)IndexWriter 他是建立索引的类,至于该类如何实现建立索引请参考前面的文章
IndexWriter.setMaxFieldLength(int)表示索引数据时对数据源的最大长度进行现在,如设置为100,则表示 取数据源前100个长度进行索引分析,当然存储是全都存储,当然长度不足100的就全都索引。
l 、一个索引可以有多个segment(段),但一个索引目录里只有一个segments(该文件记录了所有的segmet);一个segment有多个document(可以设置最大值),同一个segment在磁盘上各种物理的文件的前缀是相同的:如图
其中
l .frq 记录的是词条频率信息,
l .prx记录的词条的未知信息,
l .fnm记录了所有Field的信息,
l .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文件中找到对应的存储域的信息。
l 词典文件(tis)
- TermCount:词典中包含的总的词数
- IndexInterval:为了加快对词的查找速度,也应用类似跳跃表的结构,假设IndexInterval为4,则在词典索引(tii)文件中保存第4个,第8个,第12个词,这样可以加快在词典文件中查找词的速度。
- SkipInterval:倒排表无论是文档号及词频,还是位置信息,都是以跳跃表的结构存在的,SkipInterval是跳跃的步数。
- MaxSkipLevels:跳跃表是多层的,这个值指的是跳跃表的最大层数。
- TermCount个项的数组,每一项代表一个词,对于每一个词,以前缀后缀规则存放词的文本信息(PrefixLength + Suffix),词属于的域的域号(FieldNum),有多少篇文档包含此词(DocFreq),此词的倒排表在frq,prx中的偏移量(FreqDelta, ProxDelta),此词的倒排表的跳跃表在frq中的偏移量(SkipDelta),这里之所以用Delta,是应用差值规则。
l 词典索引文件(tii)
- 词典索引文件是为了加快对词典文件中词的查找速度,保存每隔IndexInterval个词。
- 词典索引文件是会被全部加载到内存中去的。
- IndexTermCount = TermCount / IndexInterval:词典索引文件中包含的词数。
- IndexInterval同词典文件中的IndexInterval。
- SkipInterval同词典文件中的SkipInterval。
- MaxSkipLevels同词典文件中的MaxSkipLevels。
- IndexTermCount个项的数组,每一项代表一个词,每一项包括两部分,第一部分是词本身(TermInfo),第二部分是在词典文件中的偏移量(IndexDelta)。假设IndexInterval为4,此数组中保存第4个,第8个,第12个词...
l 标准化因子文件(nrm)
为什么会有标准化因子呢?从第一章中的描述,我们知道,在搜索过程中,搜索出的文档要按与查询语句的相关性排序,相关性大的打分(score)高,从而排在前面。相关性打分(score)使用向量空间模型(Vector Space Model),在计算相关性之前,要计算Term Weight,也即某Term相对于某Document的重要性。在计算Term Weight时,主要有两个影响因素,一个是此Term在此文档中出现的次数,一个是此Term的普通程度。显然此Term在此文档中出现的次数越多,此Term在此文档中越重要。
这种Term Weight的计算方法是最普通的,然而存在以下几个问题:
- 不同的文档重要性不同。有的文档重要些,有的文档相对不重要,比如对于做软件的,在索引书籍的时候,我想让计算机方面的书更容易搜到,而文学方面的书籍搜索时排名靠后。
- 不同的域重要性不同。有的域重要一些,如关键字,如标题,有的域不重要一些,如附件等。同样一个词(Term),出现在关键字中应该比出现在附件中打分要高。
- 根据词(Term)在文档中出现的绝对次数来决定此词对文档的重要性,有不合理的地方。比如长的文档词在文档中出现的次数相对较多,这样短的文档比较吃亏。比如一个词在一本砖头书中出现了10次,在另外一篇不足100字的文章中出现了9次,就说明砖头书应该排在前面码?不应该,显然此词在不足100字的文章中能出现9次,可见其对此文章的重要性。
由于以上原因,Lucene在计算Term Weight时,都会乘上一个标准化因子(Normalization Factor),来减少上面三个问题的影响。
标准化因子(Normalization Factor)是会影响随后打分(score)的计算的,Lucene的打分计算一部分发生在索引过程中,一般是与查询语句无关的参数如标准化因子,大部分发生在搜索过程中,会在搜索过程的代码分析中详述。
标准化因子(Normalization Factor)在索引过程总的计算如下:
它包括三个参数:
- Document boost:此值越大,说明此文档越重要。
- Field boost:此域越大,说明此域越重要。
- lengthNorm(field) = (1.0 / Math.sqrt(numTerms)):一个域中包含的Term总数越多,也即文档越长,此值越小,文档越短,此值越大。
从上面的公式,我们知道,一个词(Term)出现在不同的文档或不同的域中,标准化因子不同。比如有两个文档,每个文档有两个域,如果不考虑文档长度,就有四种排列组合,在重要文档的重要域中,在重要文档的非重要域中,在非重要文档的重要域中,在非重要文档的非重要域中,四种组合,每种有不同的标准化因子。
(五)索引优化,合并策略,以前的版本中通过合并因子Mergefactor以及maxMergegeDocs都是通过IndexWriter来设置的,在3.5版本中这些方法都过时了,现在这些优化方法都集成到MergePolicy(一个抽象类),所以索引优化时可以用如下方法来设置
MergePolicy me = new LogMergePolicy()该类继承MergePolicy; 然后通过IndexWriterConfig调用setMergePolicy(MergePolicy mergePolicy) 来设置,然后通过3.5版本中IndexWriter的唯一推荐的构造方法IndexWriter(Directory d, IndexWriterConfig conf) 来设置,其他构造方法都过时了。
(六)、IndexReader类,该类主要负责索引的操作,如读取,修改,删除操作,具体可以参考http://xiaozu.renren.com/xiaozu/258210/336375170 ,很代码已经详细说明了。