一,信息检索的过程简介
全文检索和数据库应用最大的不同在于:让最相关的头100条结果满足98%以上用户的需求
1,构建文本库
在开发功能前,一个信息检索系统需要做些准备工作,首先,必须要构建一个文本数据库,这个文本数据库用来保存所有用户可能检索的信息。在这些信息的基础上,确定索引中
的文本类型,文本类型是被系统所认可的一种信息格式,这种格式应当具有可识别,冗余程度低的特点。一旦文本模型确定下来后,就不应当对其进行大的行动。
2,建立索引
有了这种文本模型后,就应该根据数据库内的文本建立索引。索引可以大大的提高信息检索的速度。目前,有许多索引的建立方式。采用哪种方式取决于信息检索系统的规模。大型信息检索系统(百度,google这样的搜索)均采用倒排的方式来建立索引。
3,进行搜索
在文档建立索引之后,就可以开始对其进行搜索。这时,通常都是由用户提交一个检索请求,请求将被分析,然后利用文本操作进行处理。对于真实的信息检索系统,在真正处理请求前,还可以对请求进行一些预处理,然后再将请求送到后台,并返回用户需要的信息。
4,对结果进行过滤
通常,在信息检索系统检索到用户需要的信息后,还要做一步操作,就是将信息以一定的规则进行排序或过滤,再返回给用户。这一步实际上关乎到最终用户的体验。
二,Lucene 索引
1,使用索引提高检索速度常用的3种索引方式为
(1)倒排
倒排是一种面向单词的索引机制。通常它由(关键字)和出现情况两部分组成。对于索引中的每个词(关键字),都跟随一个列表(位置表),用来记录单词在所有文档中出现的位置。
倒排的特点:
在倒排索引中,关键字的数量并非随着文本内容的增长也线性增长。这是因为无论多大数量的文本数据库,总能够规范出一个关键字表。这种关键字受到实际语言因素的限制,他的增长率在文本数据库达到一定规模后可以忽略不计。
(2)后缀数组
(3)签名文件
2,索引的Segment
每 个segment代表lucene的一个完整索引段,通常,一个索引中,包含有多个segment,每个segment都有一个统一的前缀,这个前缀是根 据当前索引的document的数量而确立的,前缀名是document 数量转成36进制后,在前面加上“_”而构成的。
segment的格式
(1).fnm格式
包含了document中的所有field的名称。
(2).fdx 和.fdt格式
.fdx 和.fdt是综合使用的两个文件,其中.fdt类型文件用于存储具有store.YES属性的field的数据。而.fdx类型文件则是一个索引,用于存储document在.fdt中的位置。
(3).tii 与.tis格式
.tis 文件用于存储分词的词条(Term),而.tii就是它的索引文件,它标明了每个.tis文件的词条的位置。
(4).cfs复合格式
在indexWriter 中有一个属性:useCompoundFile,它的默认值为True,这个属性的含义是:是否使用复合索引格式来保存索引。索引的内容可能非常大,文件 的数量可能非常多,如果遇到这种情况,系统打开文件数量巨大将会极大地耗费系统资源。因此, lucene提供了一种简单文件索引格式,也就是所谓的复合索引格式。
3,索引的优化
(1)合并因子mergeFactor
当mergeFactor取比较小的 值时,内存中注入的文档数量少,向磁盘写入segment的操作比较多,故此时将占用较少的内存,但是索引的建立由于i/o操作频繁所以会比较慢.而当 mergeFactor取较大的值时,内存中驻留的document数量比较多.向磁盘写入segment的操作较少,故此时将占用较多的内存,但索引的 建立速度比较快.
maxMergeDocs
对索引的合并的最多文档数量.
mixMergeDocs(maxBufferedDocs)
(2)索引的合并与索引的优化
FSDirectory 和 RAMDirectory目录文件
FSDirectory 是与文件系统目录有关的,而RAMDirectory则是与内存相关的。
对 于lucene 来说,两中目录都可以作为索引的存储路径。在初始化indexwriter的时候需要传入一个directory类型的对象作为参数之一,当 indexwriter接收这样的参数时(无论是fsdirectory还是ramdirectory),它都会在指定的位置下将索引进行存储。但是文件 系统目录就会直接将索引写到磁盘上。而ramdirectory则是在内存中一个区域,虽然向其中添加document的过程与使用 fsdirectory一样,但是由于它是内存中的一块区域,因此如果不将ramdirectory中的内存写入磁盘,当虚拟机退出后,里面的内容也会随 之消失。因此,需要将ramdirectory中的内容转到fsdirectory中。
(3)使用indexWriter来合并索引
document 可以被放置在 ramdirectory中,使用它的优点就是索引的速度很快。当document被加入到ramdirectory中后, ramdirectory在逻辑上就是一个完整的索引了,它在逻辑上就应当包括如前所说的所有索引格式的文件(但是不能被持久的保存起来)。
indexwriter的addindexs()方法,可以实现索引的合并。addindexs()方法的参数是一个directory类型的数组,因此,可以同时合并多个目录下的索引,只要分别为这些目录创建其对应的directory类型的对象就可以了。
(4)索引的优化
indexwriter 的optimize()方法正是为了这个目的而设置的,该方法能够对当前indexwriter所制定的索引目录以及其所使用的缓存目录下的所有 segment 进行优化,使所有的segments合并成一个完整的segement,即整个索引目录内出现一种文件前缀。
对于系统的优化会有 什么性能上的损失呢?由于优化时需要对已有的索引内的文件进行操作,因此需要耗费更多的内存和磁盘空间,索引优化采用的策略是建立新的segment来取 代那些被合并的segements,所以在旧的segement还未被删除之前,索引内的磁盘空间消耗将会非常大,甚至可能使原来索引的两倍。同理,在进 行优化时的磁盘i/o也会非常多,所以这是一个耗费资源的过程。
2,索引中删除文档
索引的读取工具 IndexReader,IndexReader中的getVersion方法可以查看当前索引的版本,这个version是索引建立时的精确到毫秒的时 间,IndexReader的indexReader.numDosc()方法,可以查看当前索引内总共有多少个document, IndexReader.document(int)方法可以从索引中取出相应的document.
(1)使用文档的id号来删除特定文档
在创建索引的过程中 lucene会为每一个加入索引的document赋予一个id号,这个id号将唯一的标识每个文档.reader.deleteDocument (int),int参数为id 号删除完毕后需要执行reader.close()方法关闭.使删除操作写入索引的deletable文件中,如果不关闭怎没有删除掉,实际上 lucene的删除机制为回收站机制,删除操作没有真正删除文件,而是做了一标记,可以进行还原;reader.undeleteall()方法可以帮助 实现反删除(当使用indexwriter对索引optimize一次时,lucene 为每个document重新分配id,这样那些被标记为已删除的document真正的被物理删除了).
(2)使用field信息来删除批量文档
reader.deleteDocuments (term)该方法是一个能够批量删除索引的方法,它删除索引是按照词条来进行的. term类是用于表示词条的一个工具,它能够将词条表示成<field,value>(例如: 词条为<bookename,男>也就是indexReader就会删除所有在"bookname"这个field中含有"男"这个term 的document).
3, lucene的同步问题
writer.lock
出现在向索引中添加文档时,或是将文档从索引中删除时,在indexwriter的close()方法被调用时被释放
commit.lock
主要是与segment合并和读取的操作相关.
indexModifier类
三,lucene 的搜索
1,indexSearcher进行搜索
(1)indexSearcher的简单使用:
indexSearcher searcher=new IndexSearcher("索引路径");
//构建一个term对象
Term term=new Term("name","女")
//构建一个query对象
Query q =new TermQuery (term);
//检索
Hits hits=searcher.search(q);
//显示结果
for(int i=0;i<hits.lengtth();i++){
system.out.println(hits.doc(i));
}
上面的例子中介绍了indexsearcher的search方法,search方法是整个检索系统的核心。
indexSearcher有多种重载search方法,这些方法有些在于indexSearcher的父类Search中,有些在本身,Search类实现了一个接口Searchable,该接口提供了可以搜索的功能。
(2)Hits对象是搜索结果的集合 主要有下面几个方法 [list=1]
length() ,这个方法记录有多少条结果返回(lazy loading)
doc(n) 返回第n个记录
id(in) 返回第n个记录的Document ID
score(n) 第n个记录的相关度(积分)
由于搜索的结果一般比较大,从性能上考虑,Hits对象并不会真正把所有的结果全部取回,默认情况下是保留前100个记录(对于一般的搜索引擎,100个记录足够了).
hits类,在上面的例子中使用了hits,从Hits的doc(int n)方法来研究Hits的工作原理.
doc(int n)方法用于搜索索引的返回结果中取出相应的文档。参数n代表结果中的第n个文档。而doc(int n)方法的第一步就是使用hitDoc(int n)方法从缓存中取去相应的文档。
在hitDoc(int n)方法中,会先判断当前用户需要取出的文档是不是已经超过了缓存的大小。如果是,则先调用getMoreDocs(int min)方法来扩大缓存,然后再从缓存中返回需要的文档。
2,搜索结构的评分
(1)文档的得分算法公式:略
搜索的结果可以按照分数来排序。
3,lucene内建的query对象
(1) 内建的query对象主要包括:
(2)TermQuery词条搜索
(3) BooleanQuery布尔搜索
(4)RangeQuery范围搜索
(5) PrefixQuery前缀搜索
(6) PhraseQuery短语搜索
(7)MultiPhraseQuery多短语搜索
(8)FuzzyQuery模糊搜索
(9)WildcardQuery通配符搜索
(10)SpanQuery跨度搜索
(11)还有第三方提供的Query对象:RegexQuery
上面的这些内建的query对象都是可以用来做根据不同的情况来进行搜索。(具体略)
4,Lucene查询总结:
Lucene 面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果 (TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录 内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这 么多的结果的,头100条已经可以满足90%以上的检索需求。
如果首批缓存结果数用完后还要读取更 后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条 结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推, 400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下 以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。
Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处.
四,排序、过滤、分页
1, 自然排序
相关度排序是一种最简单的排序方式,所谓相关度,其实就是文档的得分。
searcher的explain方法可以每一个文档的得分是怎么样的算出来的,他们的idf,tf,lengthNorm的值得情况。如:searcher.explain(q,hits.id(i).toString());
通过改变boost值来改变文档的得分在进行相关度排序的时候,如果想人为的增加某个文档的相关度,使其在搜索的结构中排在考前的位置上,可以使用boost。
如:索引写入document 的时,在写入之前,使用document方法(document.setBoost(3f))
原理:在lucene中,文档的boost的值一般情况默认为1.0,但当某个文档的boost值大于1.0后,所有的文档boost值均会除以这个最大值,以此来为每个文档获取一个小于1.0的数作为新的boost值。
2,使用Sort来排序
Sort是lucene自带的一个排序工具,通过它,可以方便地对检索结果进行排序。
Sort 所提供的排序功能是以field为基础的,也就是说,最终的排序准则,总是以某个field(或多个)的值为基础,经过这样的处理,最终的排序就转变成对 所有文档中同一个field(或多个field)的值的排序。方法:Sort(String field,boolean reverse),field表示参照制定的field排序,第二个参数reverse 表示排序的顺序,升序还是降序(reverse的默认值为false,升序排序)。
SortField是一个包装类型,通过它的包装,可以使Sort类清楚地了解要进行排序的field的各种信息。
构造函数(略)
按文档的内部id号来排序
如:Hits hits=searcher.search(q,Sort.INDEXORDER);
这个内部需要是在建立索引的时候自动创建的。
按一个或多个Field来排序
如:Sort sort=new Sort();//定义一个Sort对象
SortField f=new SortField("bookno",SortField.INT,false);//定义SortField对象,同时是按照bookno升序来排序的。
sort.setSort(f);
//下面就可以查找排序了.
3,搜索的过滤器
lucene 中有两种过滤器,一个是搜索时的过滤器,一个是分析的过滤.
搜索时的过滤是一种减小搜索范围的方式.同时也可以实现一种安全机制,即保护某些文档无法被检索.
搜索时的过滤器来自于一个抽象基类Filter,它定义了过滤器的基本行为
public abstract BitSet bits(IndexReader reader);可以看到,这个方法返回一个bitSet类型的对象,filter是一种过滤行为,这种过滤行为在搜索时的表现就是"视而不见" ,即遇到该文档时,发现它被"过滤"了,于是就忽略它,BitSet是一种"位集合"队列,这个队列中的每个元素都只有两种取值,即true或 false,这俩种值代表文档是否被过滤,也就是说,返回结果时,会首先遍历BitSet尽将那些对应值为true的文档返回。在BitSet集合中,将 其索引号看作是文档的内部id。
lucene中内置了几个Filter,
RangeFilter(范围过滤,详细略)
QueryFilter(重要)在结果中查询
实际应用:在filter的行为可以看到,它总是在搜索前,首先对索引进行一次遍历,然后返回一个被业务逻辑处理好的BitSet对象,这种做法无可厚非,但是却存在很严重的性能
问题,这相当于对索引进行了两次遍历,这样会降低性能。
CachingWrapppeFilter将一个Filter作为构造函数的参数传入,在需要使用原Filter的地方,将这个CachingWrapppeFilter的对象传入,就可以在原来的filter进行过滤了。
CachingWrapppeFilter的原理: 其中使用了缓存,在调用的时候,查看缓存中是否存在处理的结构,如果存在,则直接取出后返回,如果没有执行被注入的Filter.
4,Lucene翻页
(1)依赖于session的翻页
是指将搜索的结果存储于session中,用户翻页的时候就从session中取出hits集合,这种方式简单不需要什么算法,一次查询就可以获得结果,但是这样很容易造成服务器的内存溢出。
(2)多次查询
使用完全无状态保持的开发方式,即用户每次翻页,都对索引进行重新检索,然后取得当前页的结果并返回。
(3)缓存+多次查询
使 用session方式的查询有内存问题,但是如果采用完全无状态的查询方式,又会出现磁盘i/o太过频繁的问题,以致降低了效率。可以采用在 session或者内存中其他空间,另外在缓存一部分结果,比如后5页10页等,这样当进行翻页的时候,就可以从缓存中取出内容,不用重新查索引,如果没 有缓存,则重新查询,更新缓存。
(4)缓存+多次查询+数据库
这种方式是在上面的基础上增加的,如果索引的量很大可以考虑把内容多的东西放在数据库中,索引中的id和数据库库中的id同步。
搜索时的过滤器可以自己定义.
5,lucene的分析器
信息检索所要处理的主要对象就是信息,在实际应用中,大部分时候信息是以一种文本的方式呈现的。而信息检索的第一件事,就是要对这种文本进行分析,以便能够继续下面的处理。
(1) 分词
分词就是将一段文本拆分成多个词。(需要注意的一点是,在建立索引时使用的分词工具,与在分析用户的检索请求时使用的分词工具应当是同一个)。
(2)分词器的结构
一 个标准的分词器是由两部分组成,一部分是分词器,被称为Tokenizer;另一部分是过滤器,被称为TokenFilter。一个分析器往往是由一个分 词器和多个过滤器组成。这里所说的过滤器,与前面所说的检索时使用的过滤器完全是不同的两个概念。此处的Filter主要是用于对用户切出来的词进行一些 处理,如去掉一些敏感词、转换大小写、转换单复数等。
lucene内部提供了过滤器,StandarFilter,StopFilter,LowerCaseFilter