Lucene入门的基本知识(四)

刚才在写创建索引和搜索类的时候发现非常多类的概念还不是非常清楚,这里我总结了一下。

1 lucene简单介绍 

1.1 什么是lucene 
Lucene是一个全文搜索框架,而不是应用产品。因此它并不像www.baidu.com 或者google Desktop那么拿来就能用,它仅仅是提供了一种工具让你能实现这些产品。 

1.2 lucene能做什么 
要回答这个问题,先要了解lucene的本质。实际上lucene的功能非常单一。说究竟,就是你给它若干个字符串。然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出如今哪里。知道了这个本质,你就能够发挥想象做不论什么符合这个条件的事情了。你能够把站内新闻都索引了,做个资料库;你能够把一个数据库表的若干个字段索引起来,那就不用再操心由于“%like%”而锁表了;你也能够写个自己的搜索引擎…… 

1.3 你该不该选择lucene 
以下给出一些測试数据,假设你认为能够接受,那么能够选择。

 
測试一:250万记录。300M左右文本,生成索引380M左右。800线程下平均处理时间300ms。 
測试二:37000记录,索引数据库中的两个varchar字段,索引文件2.6M,800线程下平均处理时间1.5ms。 

2 lucene的工作方式 
lucene提供的服务实际包括两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除。所谓出是读出。即向用户提供全文搜索服务,让用户能够通过关键词定位源。 

2.1写入流程 
源字符串首先经过analyzer处理,包含:分词,分成一个个单词。去除stopword(可选)。

 
将源中须要的信息增加Document的各个Field中。并把须要索引的Field索引起来,把须要存储的Field存储起来。

 
将索引写入存储器。存储器能够是内存或磁盘。 

2.2读出流程 
用户提供搜索关键词,经过analyzer处理。 
对处理后的关键词搜索索引找出相应的Document。 
用户依据须要从找到的Document中提取须要的Field。 

3 一些须要知道的概念 
lucene用到一些概念。了解它们的含义,有利于以下的解说。 

3.1 analyzer 
Analyzer 是分析器。它的作用是把一个字符串按某种规则划分成一个个词语,并去除当中的无效词语。这里说的无效词语是指英文中的“of”、 “the”,中文中的 “的”、“地”等词语,这些词语在文章中大量出现,可是本身不包括什么关键信息。去掉有利于缩小索引文件、提高效率、提高命中率。 
分词的规则千变万化。但目的仅仅有一个:按语义划分。这点在英文中比較easy实现。由于英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法以下再具体介绍,这里仅仅需了解分析器的概念就可以。 

3.2 document 
用户提供的源是一条条记录,它们能够是文本文件、字符串或者数据库表的一条记录等等。

一条记录经过索引之后,就是以一个Document的形式存储在索引文件里的。用户进行搜索,也是以Document列表的形式返回。

 

3.3 field 
一个Document能够包括多个信息域,比如一篇文章能够包括“标题”、“正文”、“最后改动时间”等信息域,这些信息域就是通过Field在Document中存储的。 
Field有两个属性可选:存储和索引。通过存储属性你能够控制是否对这个Field进行存储。通过索引属性你能够控制是否对该Field进行索引。这看起来似乎有些废话。其实对这两个属性的正确组合非常重要,以下举例说明: 
还是以刚才的文章为样例,我们须要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同一时候我们希望能直接从搜索结果中提取文章标题。所以我们把标题域的存储属性设置为真。可是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当须要时再直接读取文件;我们仅仅是希望能从搜索解果中提取最后改动时间,不须要对它进行搜索。所以我们把最后改动时间域的存储属性设置为真,索引属性设置为假。

上面的三个域涵盖了两个属性的三种组合。另一种全为假的没实用到,其实Field不同意你那么设置,由于既不存储又不索引的域是没有意义的。 

3.4 term 
term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现的field。 

3.5 tocken 
tocken是term的一次出现,它包括trem文本和对应的起止偏移。以及一个类型字符串。一句话中能够出现多次同样的词语,它们都用同一个term表示。可是用不同的tocken,每一个tocken标记该词语出现的地方。 

3.6 segment 
加入索引时并非每一个document都立即加入到同一个索引文件,它们首先被写入到不同的小文件。然后再合并成一个大索引文件。这里每一个小文件都是一个segment。 

4 lucene的结构 
lucene包括core和sandbox两部分,当中core是lucene稳定的核心部分,sandbox包括了一些附加功能,比如highlighter、各种分析器。 
Lucene core有七个包:analysis,document。index,queryParser。search。store。util。 
4.1 analysis 
Analysis包括一些内建的分析器,比如按空白字符分词的WhitespaceAnalyzer。加入了stopwrod过滤的StopAnalyzer,最经常使用的StandardAnalyzer。 
4.2 document 
Document包括文档的数据结构。比如Document类定义了存储文档的数据结构。Field类定义了Document的一个域。 
4.3 index 
Index 包括了索引的读写类。比如对索引文件的segment进行写、合并、优化的IndexWriter类和对索引进行读取和删除操作的 IndexReader类。这里要注意的是不要被IndexReader这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完毕, IndexWriter仅仅关心怎样将索引写入一个个segment。并将它们合并优化;IndexReader则关注索引文件里各个文档的组织形式。

 
4.4 queryParser 
QueryParser 包括了解析查询语句的类。lucene的查询语句和sql语句有点类似,有各种保留字,依照一定的语法能够组成各种查询。 Lucene有非常多种 Query类。它们都继承自Query,运行各种特殊的查询,QueryParser的作用就是解析查询语句,按顺序调用各种 Query类查找出结果。

 
4.5 search 
Search包括了从索引中搜索结果的各种类,比如刚才说的各种Query类。包括TermQuery、BooleanQuery等就在这个包里。 
4.6 store 
Store包括了索引的存储类,比如Directory定义了索引文件的存储结构,FSDirectory为存储在文件里的索引。RAMDirectory为存储在内存中的索引。MmapDirectory为使用内存映射的索引。

 
4.7 util 
Util包括一些公共工具类,比如时间和字符串之间的转换工具。 

5 怎样建索引 
5.1 最简单的能完毕索引的代码片断 

IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true); 
Document doc = new Document(); 
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED)); 
doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED)); 
writer.addDocument(doc); 
writer.optimize(); 
writer.close(); 

以下我们分析一下这段代码。 
首先我们创建了一个writer,并指定存放索引的文件夹为“/data/index”。使用的分析器为StandardAnalyzer。第三个參数说明假设已经有索引文件在索引文件夹下。我们将覆盖它们。 
然后我们新建一个document。 
我们向document加入一个field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。

 
再加入一个名字是“content”的field,内容是“lucene works well”。也是存储并索引。

 
然后我们将这个文档加入到索引中。假设有多个文档,能够反复上面的操作,创建document并加入。 
加入全然部document,我们对索引进行优化,优化主要是将多个segment合并到一个,有利于提高索引速度。 
随后将writer关闭,这点非常重要。 

对。创建索引就这么简单! 
当然你可能改动上面的代码获得更具个性化的服务。

 

5.2 将索引直接写在内存 
你须要首先创建一个RAMDirectory,并将其传给writer,代码例如以下: 

Directory dir = new RAMDirectory(); 
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true); 
Document doc = new Document(); 
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED)); 
doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED)); 
writer.addDocument(doc); 
writer.optimize(); 
writer.close(); 

5.3 索引文本文件 
假设你想把纯文本文件索引起来,而不想自己将它们读入字符串创建field,你能够用以下的代码创建field: 

Field field = new Field("content", new FileReader(file)); 

这里的file就是该文本文件。该构造函数实际上是读去文件内容。并对其进行索引,但不存储。 

6 怎样维护索引 
索引的维护操作都是由IndexReader类提供。

 

6.1 怎样删除索引 
lucene提供了两种从索引中删除document的方法。一种是 

void deleteDocument(int docNum) 

这样的方法是依据document在索引中的编号来删除,每一个document加进索引后都会有个唯一编号,所以依据编号删除是一种精确删除。可是这个编号是索引的内部结构,一般我们不会知道某个文件的编号究竟是几,所以用处不大。

还有一种是 

void deleteDocuments(Term term) 

这样的方法实际上是首先依据參数term运行一个搜索操作。然后把搜索到的结果批量删除了。

我们能够通过这种方法提供一个严格的查询条件,达到删除指定document的目的。

 
以下给出一个样例: 

Directory dir = FSDirectory.getDirectory(PATH, false); 
IndexReader reader = IndexReader.open(dir); 
Term term = new Term(field, key); 
reader.deleteDocuments(term); 
reader.close(); 

6.2 怎样更新索引 
lucene并没有提供专门的索引更新方法,我们须要先将对应的document删除,然后再将新的document增加索引。比如: 

Directory dir = FSDirectory.getDirectory(PATH, false); 
IndexReader reader = IndexReader.open(dir); 
Term term = new Term(“title”, “lucene introduction”); 
reader.deleteDocuments(term); 
reader.close(); 

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true); 
Document doc = new Document(); 
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED)); 
doc.add(new Field("content", "lucene is funny", Field.Store.YES, Field.Index.TOKENIZED)); 
writer.addDocument(doc); 
writer.optimize(); 
writer.close(); 


7 怎样搜索 
lucene 的搜索相当强大,它提供了非常多辅助查询类,每一个类都继承自Query类,各自完毕一种特殊的查询,你能够像搭积木一样将它们随意组合使用。完毕一些复杂操作。另外lucene还提供了Sort类对结果进行排序。提供了Filter类对查询条件进行限制。

你也许会不自觉地拿它跟SQL语句进行比較: “lucene能运行and、or、order by、where、like ‘%xx%’操作吗?”回答是:“当然没问题!” 

7.1 各种各样的Query 
以下我们看看lucene究竟同意我们进行哪些查询操作: 

7.1.1 TermQuery 
首先介绍最主要的查询,假设你想运行一个这种查询:“在content域中包括‘lucene’的document”,那么你能够用TermQuery: 

Term t = new Term("content", " lucene"; 
Query query = new TermQuery(t); 

7.1.2 BooleanQuery 
假设你想这么查询:“在content域中包括java或perl的document”,那么你能够建立两个TermQuery并把它们用BooleanQuery连接起来: 

TermQuery termQuery1 = new TermQuery(new Term("content", "java"); 
TermQuery termQuery 2 = new TermQuery(new Term("content", "perl"); 
BooleanQuery booleanQuery = new BooleanQuery(); 
booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD); 
booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD); 

7.1.3 WildcardQuery 
假设你想对某单词进行通配符查询,你能够用WildcardQuery。通配符包含’?’匹配一个随意字符和’*’匹配零个或多个随意字符,比如你搜索’use*’,你可能找到’useful’或者’useless’: 

Query query = new WildcardQuery(new Term("content", "use*"); 

7.1.4 PhraseQuery 
你可能对中日关系比較感兴趣,想查找‘中’和‘日’挨得比較近(5个字的距离内)的文章,超过这个距离的不予考虑。你能够: 

PhraseQuery query = new PhraseQuery(); 
query.setSlop(5); 
query.add(new Term("content ", “中”)); 
query.add(new Term(“content”, “日”)); 

那么它可能搜到“中日合作……”、“中方和日方……”。可是搜不到“中国某高层领导说日本欠扁”。

 

7.1.5 PrefixQuery 
假设你想搜以‘中’开头的词语,你能够用PrefixQuery: 

PrefixQuery query = new PrefixQuery(new Term("content ", "中"); 

7.1.6 FuzzyQuery 
FuzzyQuery用来搜索相似的term,使用Levenshtein算法。如果你想搜索跟‘wuzza’相似的词语,你能够: 

Query query = new FuzzyQuery(new Term("content", "wuzza"); 

你可能得到‘fuzzy’和‘wuzzy’。

 

7.1.7 RangeQuery 
还有一个经常使用的Query是RangeQuery。你或许想搜索时间域从20060101到20060130之间的document,你能够用RangeQuery: 

RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true); 

最后的true表示用闭合区间。 

7.2 QueryParser 
看了这么多Query。你可能会问:“不会让我自己组合各种Query吧。太麻烦了!

”当然不会,lucene提供了一种类似于SQL语句的查询语句。我们姑且叫它lucene语句,通过它,你能够把各种查询一句话搞定,lucene会自己主动把它们查分成小块交给相应Query运行。

以下我们相应每种 Query演示一下: 
TermQuery能够用“field:key”方式,比如“content:lucene”。 
BooleanQuery中‘与’用‘+’。‘或’用‘ ’,比如“content:java contenterl”。 
WildcardQuery仍然用‘?’和‘*’。比如“content:use*”。 
PhraseQuery用‘~’,比如“content:"中日"~5”。 
PrefixQuery用‘*’,比如“中*”。 
FuzzyQuery用‘~’。比如“content: wuzza ~”。 
RangeQuery用‘[]’或‘{}’,前者表示闭区间。后者表示开区间,比如“time:[20060101 TO 20060130]”,注意TO区分大写和小写。 
你能够随意组合query string,完毕复杂操作,比如“标题或正文包含lucene,而且时间在20060101到20060130之间的文章”能够表示为:“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代码例如以下: 

Directory dir = FSDirectory.getDirectory(PATH, false); 
IndexSearcher is = new IndexSearcher(dir); 
QueryParser parser = new QueryParser("content", new StandardAnalyzer()); 
Query query = parser.parse("+(title:lucene content:lucene) +time:[20060101 TO 20060130]"; 
Hits hits = is.search(query); 
for (int i = 0; i < hits.length(); i++) 

Document doc = hits.doc(i); 
System.out.println(doc.get("title"); 

is.close(); 

首先我们创建一个在指定文件文件夹上的IndexSearcher。 
然后创建一个使用StandardAnalyzer作为分析器的QueryParser,它默认搜索的域是content。

 
接着我们用QueryParser来parse查询字串,生成一个Query。

 
然后利用这个Query去查找结果,结果以Hits的形式返回。

 
这个Hits对象包括一个列表,我们挨个把它的内容显示出来。 

7.3 Filter 
filter 的作用就是限制仅仅查询索引的某个子集,它的作用有点像SQL语句里的where。但又有差别,它不是正规查询的一部分,仅仅是对数据源进行预处理。然后交给查询语句。

注意它运行的是预处理,而不是对查询结果进行过滤,所以使用filter的代价是非常大的,它可能会使一次查询耗时提高一百倍。 
最经常使用的filter是RangeFilter和QueryFilter。RangeFilter是设定仅仅搜索指定范围内的索引;QueryFilter是在上次查询的结果中搜索。

 
Filter的使用很easy,你仅仅需创建一个filter实例。然后把它传给searcher。

继续上面的样例。查询“时间在20060101到20060130之间的文章”除了将限制写在query string中,你还能够写在RangeFilter中: 

Directory dir = FSDirectory.getDirectory(PATH, false); 
IndexSearcher is = new IndexSearcher(dir); 
QueryParser parser = new QueryParser("content", new StandardAnalyzer()); 
Query query = parser.parse("title:lucene content:lucene"; 
RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true); 
Hits hits = is.search(query, filter); 
for (int i = 0; i < hits.length(); i++) 

Document doc = hits.doc(i); 
System.out.println(doc.get("title"); 

is.close(); 

7.4 Sort 
有时你想要一个排好序的结果集,就像SQL语句的“order by”,lucene能做到:通过Sort。 
Sort sort = new Sort(“time”); //相当于SQL的“order by time” 
Sort sort = new Sort(“time”, true); // 相当于SQL的“order by time desc” 
以下是一个完整的样例: 

Directory dir = FSDirectory.getDirectory(PATH, false); 
IndexSearcher is = new IndexSearcher(dir); 
QueryParser parser = new QueryParser("content", new StandardAnalyzer()); 
Query query = parser.parse("title:lucene content:lucene"; 
RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true); 
Sort sort = new Sort(“time”); 
Hits hits = is.search(query, filter, sort); 
for (int i = 0; i < hits.length(); i++) 

Document doc = hits.doc(i); 
System.out.println(doc.get("title"); 

is.close(); 

8 分析器 
在前面的概念介绍中我们已经知道了分析器的作用。就是把句子依照语义切分成一个个词语。英文切分已经有了非常成熟的分析器: StandardAnalyzer,非常多情况下StandardAnalyzer是个不错的选择。

甚至你会发现StandardAnalyzer也能对中文进行分词。

 
可是我们的焦点是中文分词。StandardAnalyzer能支持中文分词吗?实践证明是能够的,可是效果并不好。搜索“假设” 会把“牛奶不假设汁好喝”也搜索出来,并且索引文件非常大。

那么我们手头上还有什么分析器能够使用呢?core里面没有,我们能够在sandbox里面找到两个: ChineseAnalyzer和CJKAnalyzer。可是它们相同都有分词不准的问题。

相比之下用StandardAnalyzer和 ChineseAnalyzer建立索引时间差点儿相同,索引文件大小也差点儿相同,CJKAnalyzer表现会差些。索引文件大且耗时比較长。 
要解决这个问题,首先分析一下这三个分析器的分词方式。StandardAnalyzer和ChineseAnalyzer都是把句子按单个字切分。也就是说 “牛奶不假设汁好喝”会被它们切分成“牛 奶 不 如 果 汁 好 喝”。而CJKAnalyzer则会切分成“牛奶 奶不 不如 假设 果汁 汁好好喝”。这也就解释了为什么搜索“果汁”都能匹配这个句子。

 
以上分词的缺点至少有两个:匹配不准确和索引文件大。我们的目标是将上面的句子分解成 “牛奶 不如 果汁好喝”。

这里的关键就是语义识别,我们怎样识别“牛奶”是一个词而“奶不”不是词语?我们非常自然会想到基于词库的分词法,也就是我们先得到一个词库。里面列举了大部分词语。我们把句子按某种方式切分,当得到的词语与词库中的项匹配时,我们就觉得这样的切分是正确的。

这样切词的过程就转变成匹配的过程,而匹配的方式最简单的有正向最大匹配和逆向最大匹配两种。说白了就是一个从句子开头向后进行匹配,一个从句子末尾向前进行匹配。

基于词库的分词词库非常重要,词库的容量直接影响搜索结果,在同样词库的前提下,据说逆向最大匹配优于正向最大匹配。

 
当然还有别的分词方法,这本身就是一个学科。我这里也没有深入研究。回到详细应用,我们的目标是能找到成熟的、现成的分词工具。避免又一次发明车轮。经过网上搜索。用的比較多的是中科院的 ICTCLAS和一个不开放源代码可是免费的JE-Analysis。ICTCLAS有个问题是它是一个动态链接库, java调用须要本地方法调用,不方便也有安全隐患,并且口碑也确实不大好。JE-Analysis效果还不错,当然也会有分词不准的地方,相比比較方便放心。 

9 性能优化 
一直到这里。我们还是在讨论怎么样使lucene跑起来,完毕指定任务。利用前面说的也确实能完毕大部分功能。

可是測试表明lucene的性能并非非常好,在大数据量大并发的条件下甚至会有半分钟返回的情况。另外大数据量的数据初始化建立索引也是一个十分耗时的过程。那么怎样提高lucene的性能呢?以下从优化创建索引性能和优化搜索性能双方面介绍。 

9.1 优化创建索引性能 
这方面的优化途径比較有限,IndexWriter提供了一些接口能够控制建立索引的操作,另外我们能够先将索引写入RAMDirectory,再批量写入FSDirectory。无论如何,目的都是尽量少的文件IO,由于创建索引的最大瓶颈在于磁盘IO。另外选择一个较好的分析器也能提高一些性能。

 

9.1.1 通过设置IndexWriter的參数优化索引建立 
setMaxBufferedDocs(int maxBufferedDocs) 
控制写入一个新的segment前内存中保存的document的数目。设置较大的数目能够加快建索引速度,默觉得10。

 
setMaxMergeDocs(int maxMergeDocs) 
控制一个segment中能够保存的最大document数目,值较小有利于追加索引的速度,默认Integer.MAX_VALUE。无需改动。

 
setMergeFactor(int mergeFactor) 
控制多个segment合并的频率,值较大时建立索引速度较快,默认是10,能够在建立索引时设置为100。 

9.1.2 通过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); 
while (there are documents to index) 

... create Document ... 
ramWriter.addDocument(doc); 
if (condition for flushing memory to disk has been met) 

fsWriter.addIndexes(new Directory[] { ramDir }); 
ramWriter.close(); 
ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true); 



9.1.3 选择较好的分析器 
这个优化主要是对磁盘空间的优化,能够将索引文件减小将近一半,同样測试数据下由600M降低到380M。可是对时间并没有什么帮助,甚至会须要更长时间。由于较好的分析器须要匹配词库,会消耗很多其它cpu。測试数据用StandardAnalyzer耗时133分钟;用MMAnalyzer耗时150分钟。

 

9.2 优化搜索性能 
尽管建立索引的操作很耗时。可是那毕竟仅仅在最初创建时才须要,平时仅仅是少量的维护操作,更何况这些能够放到一个后台进程处理,并不影响用户搜索。我们创建索引的目的就是给用户搜索,所以搜索的性能才是我们最关心的。

以下就来探讨一下怎样提高搜索性能。

 

9.2.1 将索引放入内存 
这是一个最直观的想法。由于内存比磁盘快非常多。Lucene提供了RAMDirectory能够在内存中容纳索引: 

Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false); 
Directory ramDir = new RAMDirectory(fsDir); 
Searcher searcher = new IndexSearcher(ramDir); 

可是实践证明RAMDirectory和FSDirectory速度差点儿相同,当数据量非常小时两者都非常快。当数据量较大时(索引文件400M)RAMDirectory甚至比FSDirectory还要慢一点,这确实让人出乎意料。 
并且lucene的搜索很耗内存,即使将400M的索引文件加载内存,在执行一段时间后都会out of memory,所以个人觉得加载内存的作用并不大。 

9.2.2 优化时间范围限制 
既然加载内存并不能提高效率,一定有其他瓶颈,经过測试发现最大的瓶颈竟然是时间范围限制,那么我们能够如何使时间范围限制的代价最小呢? 
当须要搜索指定时间范围内的结果时,能够: 
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倍左右的性能提高。

 

9.2.3 使用更好的分析器 
这个跟创建索引优化道理差点儿相同,索引文件小了搜索自然会加快。

当然这个提高也是有限的。较好的分析器相对于最差的分析器对性能的提升在20%下面。 

10 一些经验 

10.1关键词区分大写和小写 
or AND TO等关键词是区分大写和小写的。lucene仅仅认大写的,小写的当做普通单词。

 

10.2 读写相互排斥性 
同一时刻仅仅能有一个对索引的写操作,在写的同一时候能够进行搜索 

10.3 文件锁 
在写索引的过程中强行退出将在tmp文件夹留下一个lock文件。使以后的写操作无法进行。能够将其手工删除 

10.4 时间格式 
lucene仅仅支持一种时间格式yyMMddHHmmss。所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的 

10.5 设置boost 
有些时候在搜索时某个字段的权重须要大一些。比如你可能觉得标题中出现关键词的文章比正文中出现关键词的文章更有价值。你能够把标题的boost设置的更大。那么搜索结果会优先显示标题中出现关键词的文章(没有使用排序的前题下)。用法: 
Field. setBoost(float boost);默认值是1.0,也就是说要添加权重的须要设置得比1大。

posted @ 2016-04-02 10:12  phlsheji  阅读(492)  评论(0编辑  收藏  举报