全文检索引擎Lucene
1. 信息检索的概念
从信息集合中找出与用户需求相关的信息。被检索的信息包括文本、图像、音频、视频等多媒体信息。
2. 信息检索的分类
全文检索:把用户的查询请求和全文的每一个词进行比较,不考虑查询请求与文本语义上的匹配。在信息检索工具中,全文检索是最具通用性和实用性的。(只搜索文本)
数据检索
知识检索
3. 全文检索流程
*从信息源中拷贝到本地进行加工,生成的信息集合称为索引表,搜索时从本地的信息集合中搜索
*文本在建立索引和搜索时都会先分词(使用分词器),且使用同一个分词器,不同的语言有不同的分词器
全文检索与数据库查询的比较:
1.匹配效果:如搜索ant,在sql中用like %ant% 会搜出planting 但它不应该出现
2.查出的结果没有相关度排序,不知道有用的结果在哪一页。
3.搜索速度慢,达不到毫秒级的要求
Lucene介绍与初步使用
Lucene是一个高性能、可伸缩的全文检索工具包。可以使用它为你的应用程序添加索引和搜索功能。
Lucene的作者Doug Cutting 是资深的全文检索专家。最初,Lucene以开源的形式出现在SourceForget上。2001年Lucene加入了Apache旗下的Jakarta项目。2005年,Lucene正式脱离Jakarta成为Apache旗下的顶级项目。现在,Lucene的主页为http://lucene.apache.org/
目前有很多应用程序使用Lucene来提供全文检索的功能,如Eclipse的帮助子系统就是使用Lucene实现的。
Lucene中对应的类:
Directory:索引库的位置,一般为一个目录
Document:信息集合中的每条数据。是field的集合
Field:是感兴趣的内容.
f1 = new Field(“title”,”xxxx”);//值为文本,如日期,数字要转型
f2 = new Field(“body”,”xxxx”);
doc.add(f1);
doc.add(f2);
Analyzer:分词器
IndexWriter:用来操作索引库的(增、删、改)
IndexSearcher:是用来在索引库中进行查询的
Query:查询对象,解析查询条件
分词器
分词器,对文本资源进行切分,将文本按规则切分为一个个可以进行索引的最小单位(关键词)
英文分词
分词流程:
输入文本à关键字切分à去除停用词à形态还原à转为小写
停用词:对文本所携带的信息基本不产生影响的词。如英文的“a, an,of,the”,中文的“的,了,着”。
例:对“IndexWriter addDocument’s a javadoc.txt”进行分词
1,切分词
IndexWriter addDocument’s a javadoc.txt
2,去除停用词
IndexWriter addDocument’s javadoc.txt
3,形态还原
IndexWriter addDocument javadoc.txt
4,转为小写
indexwriter adddocument javadoc.txt
中文分词
中文分词比较复杂,如“帽子和服装”。对于中文分词,通常有3种方式:单字分词(StandarAnalyzer)、二分法分词(CJKAnalyzer)、词典分词(MMAnalyzer)
例:net.hlj.lucene.analyzer.AnalyzerTest.java
下面写一个简单的 索引,检索程序 net.hlj.lucene.test.Test1.java
用到的jar包:
lucene-core-2.4.0.jar
lucene-analyzers-2.4.0.jar
je-analysis-1.5.3.jar
lucene-highlighter-2.4.0.jar
下载地址:http://archive.apache.org/dist/lucene/java/
IndexDao
完成对索引的增删改查
工具类DateTools、NumberTools
DateTools:转换日期的工具类,Resolution参数可以指定日期的精度。
NumberTools:转换数字的工具类,把数字转换为36进制的字符串
Highligher:高亮器
指定要高亮信息的前缀和后缀
查询:net.hlj.lucene.query.QueryTest.java
TermQuery:关键词查询
RangeQuery:范围查询
WildcardQuery:通配符查询
PhraseQuery:短语查询
BooleanQuery:布尔查询
排序
1相关度排序
Document的boost属性:Document.setBoost(float boost)
Field的boost属性:Field.setBoost(float boost)
查询时指定:MultFieldQueryParser(String[] fields,Analyzer analyzer,Map boosts)
2按指定字段排序
Public Sort(SortField field)
Public Sort(SortField[] fields)
过滤器
RangeFilter ,可以对搜索出来的结果进行过滤
性能优化
从优化创建索引性能和优化搜索性能两方面介绍
1 优化创建索引性能
IndexWriter提供了一些接口可以控制建立索引的操作,另外我们可以先将索引写入RAMDirectory,再批量写入FSDirectory,不管怎样,目的都是尽量少的文件IO,因为创建索引的最大瓶颈在于磁盘IO。另外选择一个较好的分析器也能提高一些性能。
通过设置IndexWriter的参数优化索引建立
setMaxBufferedDocs(int maxBufferedDocs)
控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度,默认为10。
setMaxMergeDocs(int maxMergeDocs)
控制一个segment中可以保存的最大document数目,值较小有利于追加索引的速度,默认Integer.MAX_VALUE,无需修改。
setMergeFactor(int mergeFactor)
控制多个segment合并的频率,值较大时建立索引速度较快,默认是10,可以在建立索引时设置为100。
通过RAMDirectory缓写提高性能
我们可以先把索引写入RAMDirectory,达到一定数量时再批量写进FSDirectory,减少磁盘IO次数。
FSDirectory fsDir = FSDirectory.getDirectory("/data/index", true);
RAMDirectory ramDir = new RAMDirectory();
IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);
IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
Int i=0;
while (there are documents to index)
{
... create Document ...
ramWriter.addDocument(doc);
if (i==100)
{
fsWriter.addIndexes(new Directory[] { ramDir });
ramWriter.close();
ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);
i=0;
}
i++;
}
选择较好的分词器
相同测试数据下由600M减少到380M,但是耗时长,时间换空间。
2 优化搜索性能
将索引放入内存
这是一个最直观的想法,因为内存比磁盘快很多。Lucene提供了RAMDirectory可以在内存中容纳索引:
Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);
Directory ramDir = new RAMDirectory(fsDir);
Searcher searcher = new IndexSearcher(ramDir);
优化时间范围限制
既然载入内存并不能提高效率,一定有其它瓶颈,经过测试发现最大的瓶颈居然是时间范围限制,那么我们可以怎样使时间范围限制的代价最小呢?
当需要搜索指定时间范围内的结果时,可以:
1、用RangeQuery,设置范围,但是RangeQuery的实现实际上是将时间范围内的时间点展开,组成一个个BooleanClause加入到 BooleanQuery中查询,因此时间范围不可能设置太大,经测试,范围超过一个月就会抛BooleanQuery.TooManyClauses,可以通过设置 BooleanQuery.setMaxClauseCount(int maxClauseCount)扩大,但是扩大也是有限的,并且随着maxClauseCount扩大,占用内存也扩大
2、用RangeFilter代替RangeQuery,经测试速度不会比RangeQuery慢,但是仍然有性能瓶颈,查询的90%以上时间耗费在 RangeFilter,研究其源码发现RangeFilter实际上是首先遍历所有索引,生成一个BitSet,标记每个document,在时间范围内的标记为true,不在的标记为false,然后将结果传递给Searcher查找,这是十分耗时的。
3、进一步提高性能,这个又有两个思路:
a、缓存Filter结果。既然RangeFilter的执行是在搜索之前,那么它的输入都是一定的,就是IndexReader,而 IndexReader是由Directory决定的,所以可以认为RangeFilter的结果是由范围的上下限决定的,也就是由具体的 RangeFilter对象决定,所以我们只要以RangeFilter对象为键,将filter结果BitSet缓存起来即可。lucene API已经提供了一个CachingWrapperFilter类封装了Filter及其结果,所以具体实施起来我们可以cache CachingWrapperFilter对象,需要注意的是,不要被CachingWrapperFilter的名字及其说明误导, CachingWrapperFilter看起来是有缓存功能,但的缓存是针对同一个filter的,也就是在你用同一个filter过滤不同 IndexReader时,它可以帮你缓存不同IndexReader的结果,而我们的需求恰恰相反,我们是用不同filter过滤同一个 IndexReader,所以只能把它作为一个封装类。
b、降低时间精度。研究Filter的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。
下面针对上面的两个思路演示一下优化结果(都采用800线程随机关键词随即时间范围):
第一组,时间精度为秒:
方式 直接用RangeFilter 使用cache 不用filter
平均每个线程耗时 10s 1s 300ms
第二组,时间精度为天
方式 直接用RangeFilter 使用cache 不用filter
平均每个线程耗时 900ms 360ms 300ms
由以上数据可以得出结论:
1、 尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用cache还好,最好不使用filter。
2、 在不能降低时间精度的情况下,使用cache能带了10倍左右的性能提高。
3、使用更好的分词器