1. 提取搜索结果
Lucene 中,搜索结果保存在 Hits 对象中,提取搜索结果就是从 Hits 对象中提取出文档,然后再从文档中提取各个字段。
1.1 Hits 对象的方法
l Document doc(int n) 返回指定序号的 Document 。 Doc 对象的 getField(“”) 方法获取字段
l Int id(int n) 返回指定序号的 Document 的 id 属性
l Int length() 返回 Hits 对象的长度,即 Hits 对象中包含的 Document 的数量
l Float score(int n) 返回指定序号的 Document 的 score 属性
l Iterator iterator() 返回一个 Iterator 对象用于导航 Hits 。将 Hits 的 iterator 方法返回的 Iterator 对象转换成 HitIterator 对象后,就可以用 HitIterator 对象的 next 方法获得 Hit 对象,这个 hit 对象就是 Hits 的个体,具有 get(“ 字段名 ”) , getDocument(), getScore() 等方法,可以获得某 Document 的一切信息。
1.2 性能说明
Hits 对象有内部缓存,搜索时,如果 Hits 内部已经缓存了一些记录,如果需要返回的记录在其缓存里, Lucene 将直接从缓存中提取记录,从而加快搜索。
Hits 对象的缓存机制,使得 Lucene 的查询性能很高,因此在做分页显示的时候,可以对用户的每次请求进行新的查询,而不是把 Hits 对象保留。(点第几页都重新搜索)
为加快速度,还可以把文件索引读入内存,建立起内存索引,然后执行搜索。方法如下:
RAMDirectory(Directory dir)
RamDirectrory(File dir)
RamDirectory(String dir);
如:
//search
RAMDirectory rd = new RAMDirectory(indexPath);
//IndexSearcher
IndexSearcher searcher = new IndexSearcher(rd);
2. 过滤搜索结果
过滤方式有两种:在搜索结果提取出来后过滤;把过滤条件加在搜索条件中。看起来后者更具优势,但目前,后者的实现是基于前者的,所以更耗内存,不如使用前者。
2.1 简单的过滤
2.2 利用 Filter 类过滤
抽象类,自定义的过滤器只需要覆盖它的一个方法:
BitSet bits(IndexReader reader);
过滤器在搜索结果显示之前就得到了 IndexReader 对象,从而将数据进行拦截来完成过滤。其子类有: QueryFilter 、 RangeFilter 、 PrefixFilter 、 ChainedFilter 、 CachingWrapperFilter 等。 QueryFilter 类的结果被缓存,其它类都不被缓存。
2.3 QueryFilter 类过滤
应用最广,结果被缓存
//IndexSearcher
IndexSearcher searcher = new IndexSearcher(rd);
//Term & Query
String searchField = "text";
String searchPhrase = "love";
Term t = new Term(searchField, searchPhrase);
TermQuery q = new TermQuery(t);
//Filter ,从上面的结果中过滤出包含“ genius ”的结果
Term te = new Term(searchField,"genius");
TermQuery tq = new TermQuery(te);
QueryFilter filter = new QueryFilter(tq);
//Hits
Hits hs = searcher.search(q,filter);
使用 RangeQuery 构建 QueryFilter 过滤
//Filter , id 号在 0-2 之间
Term tb = new Term("id","0");
Term te = new Term("id","2");
RangeQuery rq = new RangeQuery(tb,te,false);
QueryFilter filter = new QueryFilter(rq);
使用 PrefixQuery 构建 QueryFilter
//Filter
Term te = new Term("text","people");
PrefixQuery pq = new PrefixQuery(te);
QueryFilter filter = new QueryFilter(pq);
2.4 PrefixFilter 前缀过滤
//Filter
Term te = new Term("text","people");
PrefixFilter filter = new PrefixFilter(te);
2.5 RangeFilter
结果没有被缓存
用于从搜索结果中得到满足某一范围要求的记录,上面的 RangeQuery 和 QueryFilter 的组合与单独使用 RangeFilter 的效果是一样的。
RangeFilter(String fieldname, String lowerTerm, String upperTerm, Boolean includedLower, Boolean includedUpper);
参数:字段名称,下边界,上边界,是否包含上边界,是否包含下边界
静态方法 Less 设定小于等于上边界的过滤器, More 设定大于等于下边界的过滤器。
Static RangeFilter Less(String filedName, String upperTerm);
Static RangeFilter More(String filedName, String upperTerm);
//Filter
RangeFilter filter = new RangeFilter("id","0","2",false,false);
使用 More 方法的例子:
//Filter
RangeFilter filter = RangeFilter.More("id","1");
2.6 ChainedFilter
用来将多个过滤器连接起来,从而达到多重过滤的效果,三个构造方法:
ChainedFilter(Filter[] chain)
ChainedFilter(Filter[] chain, int logic);
ChainedFilter(Filter[] chain, int[] logicArray);
参数:过滤器数组,逻辑关系: ChainedFilter.AND, ChainedFitler.OR 等常量来表示,默认为逻辑或。
BooleanQuery 和 QueryFilter 结合也可以达到 ChainedFilter 的效果。
使用 ChainedFilter 需要把 contrib/misc 目录下的 lucene-misc-2.1.0.jar 放入编译路径
例 1 :简单的组合
//Filter
RangeFilter rf = RangeFilter.More("id","1"); 范围过滤器
Term te = new Term("text","people");
PrefixFilter pf = new PrefixFilter(te); 前缀过滤器
ChainedFilter filter = new ChainedFilter(new Filter[]{rf,pf}); 默认或逻辑
ChainedFilter filter = new ChainedFilter(new Filter[]{rf,pf}, ChainedFilter.AND); 与
例二:使用 BooleanQuery 和 QueryFilter ,效果相当于 ChainedFilter
//IndexSearcher
IndexSearcher searcher = new IndexSearcher(rd);
//Term & Query
String searchField = "text";
String searchPhrase = "love";
Term t = new Term(searchField, searchPhrase);
TermQuery q = new TermQuery(t);
//Filter
Term te = new Term("text","you");
TermQuery tq = new TermQuery(te); // 普通词项搜索
Term tr = new Term("text","xiaoyue");
PrefixQuery pq = new PrefixQuery(tr); // 前缀搜索
BooleanQuery bq = new BooleanQuery(); // BooleanQuery
bq.add(tq,BooleanClause.Occur.MUST);
bq.add(pq,BooleanClause.Occur.MUST);
QueryFilter filter = new QueryFilter(bq); //QueryFilter
//Hits
Hits hs = searcher.search(q,filter);
2.7 CachingWrapperFilter 的使用
由于除了 QueryFilter 之外的其它 Filter 自身都没有缓存,所以, CachingWrapperFilter 被开发出来用于包装其它 Filter ,从而使之具有缓存功能。构造方法如下:
CachingWrapperFilter(Filter filter);
只需在其它过滤器语句后面,加上这样一句即可:
CachingWrapperFilter(Filter filter);
CachingWrapperFilter filter = new CachingWrapperFilter(pf);
2.8 FilterQuery 的使用
FilteredQuery 是一种带有过滤器的特殊 Query 类,它是一种包装器,将原始的 Query 对象和某个过滤器结合起来。构造方法如下:
FilterQuery(Query query, Filter fitler);
因此,我们有了两种方式过滤搜索结果:
(1) 使用 Query 加 Filter ,得到 Filter ,然后使用 search(Query 对象, Filter 对象 ) 的方法执行搜索,上面的示例大都如此;
(2) 使用 Query 加 Filter ,得到 Query ,然后使用 search(Query 对象 ) 的方法执行搜索。( FilterQuery )
//Term & Query 创建 Query
//original query
String searchField = "text";
String searchPhrase = "love";
Term t = new Term(searchField, searchPhrase);
TermQuery qo = new TermQuery(t);
//Filter 创建过滤器
Term te = new Term("text","people");
PrefixFilter filter = new PrefixFilter(te);
//new query 结合 Query 和 Filter
FilteredQuery q = new FilteredQuery (qo,filter);
//Hits
Hits hs = searcher.search(q);
2.9 综述
Lucece 为了过滤搜索结果,提供了许多过滤器类,如同那些 Query 类一样,许多类都是不必要的,只是为了编程的方便。
对于过滤器的一些建议:
l 过滤器消耗资源,慎用
l 如果需要过滤结果,最好是将搜索结果按某些自定义的条件显示出来,而不使用过滤器
l 如果一定要使用过滤器,应使用 QueryFilter ,因为它的结果被缓存了,再次执行过滤搜索时,速度更快
l 如果一定要使用过滤器,而且一定要使用 QueryFilter 之外的其它 Filter ,那么应使用 CachingWrapperFilter 为其封装(实现缓存)
评:貌似都可以用 Query 的那些类生成 Query ,然后再使用 QueryFilter 进行过滤。
3. 搜索结果排序
Lucene 默认是按文档得分进行排序的,我们可以按照其它规则进行排序。 Lucene 不进行链接结构分析。
3.1 按照文档得分排序
(1) 默认排序规则
//Hits
Hits hs = searcher.search(q);
int num = hs.length();
//view details
for(int i=0;i<num;i++)
{
//get document
doc = hs.doc(i);
//fields
id = doc.getField("id").stringValue();
//score
float score = hs.score(i); // 获取得分值
}
(2) 修改默认得分
最常用的修改得分的方法如下:
Void setBoost(float boost);
该方法修改 boost 因素,这是影响得分的主要因素之一。
Document 和 Field 类都有这个方法, Document 类的 setBoost 方法用来设定其所有 Field 的 boost 值; Field 类的 setBoost 方法用来设定该 Field 的 boost 值。对 boost 的设定是在建立索引的过程中进行的,也即对某个文档调用 addDocument 之前。
例一:为 Document 设置 boost
IndexWriter writer = new IndexWriter(rd,new StandardAnalyzer());
Document doc = null;
Field field = null;
String id = null;
String text = null;
//doc 0
doc = new Document();
id = "0";
field = new Field("id",id ,Field.Store.YES, Field.Index.UN_TOKENIZED);
doc.add(field);
text = "i love you, my mother land! ";
field = new Field("text", text ,Field.Store.YES, Field.Index.TOKENIZED);
doc.add(field);
doc.setBoost(2.0f);
writer.addDocument(doc);
例二:为 Field 设置 boost
Document doc = null;
Field field = null;
String id = null;
String text = null;
//doc 0
doc = new Document();
id = "0";
field = new Field("id",id ,Field.Store.YES, Field.Index.UN_TOKENIZED);
doc.add(field);
text = "i love you, my mother land! ";
field = new Field("text", text ,Field.Store.YES, Field.Index.TOKENIZED);
field.setBoost(2.0f); 为这一字段进行设置
doc.add(field);
writer.addDocument(doc);
最终的 boost 值未必和我们设置的一样,因为我们的设置只代表我们对文档排名的期待,最终值要通过进一步的计算才能转换成文档的真实 boost 值。
(3) 查看得分计算过程
Searcher 类提供了 explain 方法用来查看得分的计算过程。调用方法:
Explanation Explain(Query, int)
参数:被解释的 Query 对象,索引中文档序号,返回值是 Explanation 对象。该对象有 getDetails() 、 isMatch() 等方法。
//IndexSearcher
IndexSearcher searcher = new IndexSearcher(rd);
//Term & Query
String searchField = "text";
String searchPhrase = "love";
Term t = new Term(searchField, searchPhrase);
TermQuery q = new TermQuery(t);
//Hits
Hits hs = searcher.search(q);
//view details
int num = hs.length();
for(int i=0;i<num;i++)
{
//get document
doc = hs.doc(i);
//fields
id = doc.getField("id").stringValue();
text = doc.getField("text").stringValue();
//document id
int did = hs.id(i);
//boost
float boost = doc.getBoost();
//score
float score = hs.score(i);
String explaination = searcher.explain(q,i).toString();
sb.append("document " + did + ":" + doc + "/n");
sb.append("boost:" + boost + "/n");
sb.append("score:" + score + "/n");
sb.append("id:" + id + "/n");
sb.append("text:" + text + "/n");
sb.append("explaination:/n" + explaination + "/n");
sb.append("------------------- " + "/n/n");
}
searcher.close();
结果:
0.75 = (MATCH) fieldWeight(text:love in 0), product of:
1.0 = tf(termFreq(text:love)=1) 注释:在该文档中出现一次
1.0 = idf(docFreq=2) 注释:共两个文档,各出现一次
0.75 = fieldNorm(field=text, doc = 0)
要注意,使用 explain 方法是要付出代价的,这代价相当于执行对整个索引的查询。所以,除非要做改善得分算法一类的基础研究,否则不要使用该方法。
3.2 自定义排序规则
如按日期或多个字段组合排序,自定义排序规则需要使用 sort 类,构造方法如下:
(1) sort() :系统自己的排序规则
(2) sort(String field) :按照指定的某个 field 排序,参数是 field 名
(3) sort(String[] fields) :按照指定的多个 field 进行排序
(4) sort(SortField field) :按照指定的某个 field 排序,参数不是原始 field 名,而是 SortField 对象
(5) sort(SortField[] fields) :按照指定的多个 field 排序
(6) sort(String field, Boolean reverse) :按照指定的某个 field 排序, reverse 指明是降序还是升序,默认为 false 升序
两个静态属性:
( 1 ) Public static final Sort RELEVANCE
使用 RELEVANCE 属性时相当于没有使用 sort 方法,它将排序规则交给系统决定,默认就是,不需要专门设置
( 2 ) Public static final Sort INDEXORDER
使用 INDEXORDER 属性相当于按照索引内部的文档编号进行排序
在构造好 Sort 对象后,就可以用 search(Query 对象 , Sort 对象 ) 执行搜索了
注意:
l 被指定为排序依据的字段必须被索引且不能被分词(即使用 Field.Index.UN_TOKENIZED ),试图对分词了的字段作排序时,会抛出异常
l 被指定为排序依据的字段可以存储也可以不存储( Field.Store.YES 或 Field.Store.NO )
l 被指定为排序依据的字段的值类型必须是可排序的整型、浮点型或字符型。系统会根据第一个值自动判断字段类型。整型、浮点型效率高,字符型排序消耗资源多,意义不大。
3.3 让系统决定如何排序
Hits hs = searcher.search(q,Sort.INDEXORDER);
3.4 按照索引中的文档编号排序
Hits hs = searcher.search(q,Sort.INDEXORDER);
3.5 按照文本字段排序
//Hits sort(String field, Boolean reverse)
Sort sort = new Sort("time",true);
Hits hs = searcher.search(q,sort);
3.6 指定字段的数据类型
指定数据类型,需要用到 SortField 类。
l SortField(String field)
l SortField(String field, Boolean reverse)
l SortField(String field, int type); type 是字段类型,值为六个静态属性
Ø AUTO: 系统自己猜
Ø CUSTOM :用户自己指定某个排序类型
Ø FLOAT :指定排序字段为 float 类型
Ø INT :
Ø STRING :
Ø SCORE :指定按照文档得分排序
l SortField(String field, int type, Boolean reverse)
示例:
//Hits
SortField sf = new SortField("id",SortField.INT,true); id 字段,整型,逆序
Sort sort = new Sort(sf);
Hits hs = searcher.search(q,sort);
3.7 按多个字段排序
只需将作为排序依据的字段合起来,以数组形式传递给 sort 方法即可。
String[] fields = new String[]{"id","age"};
Sort sort = new Sort(fields);
Hits hs = searcher.search(q,sort);
说明:按照{ ”id”, “age” }排序,以 id 字段为主, id 字段相同时,以 age 排序。
没有指明升降顺序,以默认升序排列
如果要让排序所依据的多个字段按照不同的升降顺序排列,就需要独立的 SortField 对象,在 SortField 对象中指定字段的排序规则,然后使用 sort(SortField[] fields) 构造方法。
//Hits
SortField sf1 = new SortField("id",false);
SortField sf2 = new SortField("age",true);
SortField[] fields = new SortField[]{sf1,sf2};
Sort sort = new Sort(fields);
Hits hs = searcher.search(q,sort);
3.8 综述
排序是很耗资源的,越复杂消耗越大,通常按照得分排序就足够了
另外, Lucene 提供了对于复杂自定义字段的排序接口,例如希望将三维坐标值以字符串形式保存在索引中,然后在提取出来时进行排序。
为了提高效率,我们可以在建立索引的时候,为某个不方便排序的字段建立一个辅助字段,将复杂数据类型转换成可以表示其大小的数值存储在辅助字段中,这样就容易排序了。
4. 高亮显示
关键词背景色、换颜色、加粗、斜体等,网页中的高亮是通过格式转换的方式完成的。
4.1 高亮显示的基本方式
手工实现高亮显示不大方便,有人开发了一套类库,主要类是 org.apache.lucene.search.highlight.Higelighter ,与其配合的还有 Fragmenter 、 Scorer 、 Formatter 、 Encoder 等类。
高亮原理:
l 建立索引时,在文档相关字段(需要高亮处理的字段,被搜索的字段)中记录 Term 的位置
l 搜索时,利用字段中记录的词条的位置信息,将修饰符号添加进去,从而改变了搜索关键词的显示格式,达到突出显示的目的。
(1) 建立索引时,在文档相关字段中记录词条位置
Field field = new Field(“text”,text, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.WITH_POSITIONS_OFFSETS );
这是个新的 Field 构造方法,可以将词条位置信息记录进去。
(2) 在搜索时修改词条显示格式
a. 首先要构造 Highlighter 对象
Highlighter(Scorer fragmentScore)
参数类型 Scorer , Scorer 是一个接口,常用子类是 QueryScorer ,可以由 QueryScorer(Query query) 方法来构造。
Term t = new Term(“id”, 2);
Query q = new TermQuery(t);
QueryScorer qs = new QueryScorer(q);
Highlighter hl = new Highlighter(qs);
b. 再设置文本分块
Highlighter 类通过 setTextFragmenter(Fragmenter fragmenter) 方法设定文本的分块,参数 Framenter 是一个接口,其最常用的子类是 SimpleFragmenter ,可以通过 SimpleFragmenter() 或 SimpleFragmenter(int fragmentSize) 方法构造。参数表示分块的大小,即显示给用户的含有关键词的文本块的大小。
SimpleFragmenter sf = new SimpleFragmenter(60);
highlighter.setTextFragmenter(sf);
c. 建立 TermPositionVector 对象
TermPositionVector tpv = (TermPositionVector)IndexReader 对象 .getTermFreqVector( 文档编号,被搜索字段 )
d. 设定高亮显示块的分隔符号
int maxNumFragmentsRequired = 3;
String fragmentSeparater = “…”
e. 最后,获得高亮处理后的结果
TokenStream tokenStream = TokenSouces.getTokenStream(tpv);
String result = highlighter.getBestFragments(tokenStream, text, maxNumFragmentsRequired, fragmentSeparator);
示例:
包
import org.apache.lucene.search.highlight.*;
创建索引时:
//doc 0
doc = new Document();
id = "0";
field = new Field("id",id ,Field.Store.YES, Field.Index.UN_TOKENIZED);
doc.add(field);
text = "i love you, my mother land!i love you, my mother land!i love you, my mother land! ";
field = new Field("text", text ,Field.Store.YES, Field.Index.TOKENIZED,Field.TermVector.WITH_POSITIONS_OFFSETS );
doc.add(field);
time = "2007-05-28";
field = new Field("time", time ,Field.Store.YES, Field.Index.UN_TOKENIZED);
doc.add(field);
writer.addDocument(doc);
结果输出时
//IndexSearcher
IndexSearcher searcher = new IndexSearcher(rd);
//Term & Query
String searchField = "text";
String searchPhrase = "love";
Term t = new Term(searchField, searchPhrase);
TermQuery q = new TermQuery(t);
//Hits
Hits hs = searcher.search(q,Sort.RELEVANCE);
//highlight
Highlighter highlighter =new Highlighter(new QueryScorer(q));
SimpleFragmenter sf = new SimpleFragmenter(10);
highlighter.setTextFragmenter( sf );
for (int i = 0; i < hs.length(); i++)
{
text = hs.doc(i).get("text");
int maxNumFragmentsRequired = 3;
String fragmentSeparator = "...";
TermPositionVector tpv = (TermPositionVector)searcher.getIndexReader().getTermFreqVector(hs.id(i),"text");
TokenStream tokenStream=TokenSources.getTokenStream(tpv);
String result = highlighter.getBestFragments( tokenStream, text, maxNumFragmentsRequired, fragmentSeparator);
System.out.println(result);
}
searcher.close();
}
结果:
索引前文本:
text = "i love you, my mother land!i love you, my mother land!i love you, my mother land! ";
输出结果:
I <B>love</B> … <B>love</B> you… <B>love</B> you, my
4.2 高亮显示的基本设置
(1) 如果一个文档包含多个被搜索的关键词,则可以限定显示出来的关键词的数量,用 maxNumFragmentsRequired 进行设置
(2) 高亮处理后的结果是一段文本,可以在 SimpleFragmenter 的构造方法中指定显示给用户的文本长度 SimpleFragmenter sf = new SimpleFragmenter(10);
(3) 通常,我们设置含有关键词的文本块直接使用“ … ”符号来连接,也可以通过 String fragmentSeparator = "..." 改为别的符号
(4) 默认情况下,高亮处理是使用“ <b></b> ”来修饰关键词的,可以自定义修饰格式
4.3 为高亮显示设置新的格式
默认情况下,高亮处理是使用“ <b></b> ”来修饰关键词的,可以用 SimpleHTMLFormatter 类进行修改,
Highlighter 的一种构造方法:
Hightlighter(Formatter formatter, Scorer fragmentScorer);
SimpleHTMLFormatter 也就是格式器,有两个构造方法:
( 1 ) SimpleHTMLFormatter()
默认格式“ <B></B> ”
( 2 ) SimpleHTMLFormatter(String preTag, String postTag)
自定义标记,前标记,后标记
//Hits
Hits hs = searcher.search(q,Sort.RELEVANCE);
//highlight
SimpleHTMLFormatter shf = new SimpleHTMLFormatter("< 前 >","</ 后 >");
Highlighter highlighter =new Highlighter(shf,new QueryScorer(q));
SimpleFragmenter sf = new SimpleFragmenter(60);
highlighter.setTextFragmenter( sf );