3.8 高级检索方式(二)
上节已经介绍了五种高级检索方式,现在我们来学习另外五种。事实上,今天要介绍的五种高级检索方式有共通之处:都可以解决上节提到的最后一个用户需求:帮助小明快速定位游戏分类,过滤掉其他分类内容。
六、BooleanQuery
BooleanQuery是布尔查询,通过对其他查询(如上节讲到的TermQuery,PhraseQuery或其他BooleanQuery)的组合来实现逻辑运算。
BooleanQuery的逻辑运算符是通过BooleanQuery.Occur(文档)来确定的。
- BooleanClause.Occur.MUST:与运算
- BooleanClause.Occur.SHOULD:或运算
- BooleanClause.Occur.MUST_NOT:非运算
- BooleanClause.Occur.FILTER:相当于与运算,但是不参与评分。
Lucene6.2.1版本废弃了BooleanFilter,合并到了BooleanClause.OCCUR.FILTER中,那么Filter和Query有什么区别呢?
其实各种Query和各种Filter之间非常相似,可以互相转换,最大的区别是:Query有评分操作,返回的结果集有相关性评分;Filter的结果集无相关性评分,返回的结果是无排序的。
这四者组合,妙用无穷:
- MUST和MUST:取得多个查询子句的交集。
- MUST和MUST_NOT:表示查询结果中不能包含MUST_NOT所对应得查询子句的检索结果。
- SHOULD与MUST_NOT:连用时,功能同MUST和MUST_NOT。
- SHOULD与MUST连用时,结果为MUST子句的检索结果,但是SHOULD可影响排序,是在MUST搜出来的doc里面,根据SHOULD的query进行打分。
- SHOULD与SHOULD:表示“或”关系,最终检索结果为所有检索子句的并集。
- MUST_NOT和MUST_NOT:无意义,检索无结果。
- 上述的MUST换成FILTER,就变成了不带评分的过滤功能,
1 package testAdvancedQuery; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.io.PrintWriter; 7 import java.net.ServerSocket; 8 import java.net.Socket; 9 import java.nio.file.Paths; 10 11 import org.apache.lucene.document.Document; 12 import org.apache.lucene.index.DirectoryReader; 13 import org.apache.lucene.index.Term; 14 import org.apache.lucene.queryparser.classic.QueryParser; 15 import org.apache.lucene.search.BooleanClause; 16 import org.apache.lucene.search.BooleanQuery; 17 import org.apache.lucene.search.IndexSearcher; 18 import org.apache.lucene.search.Query; 19 import org.apache.lucene.search.ScoreDoc; 20 import org.apache.lucene.search.TermQuery; 21 import org.apache.lucene.search.TopDocs; 22 import org.apache.lucene.search.highlight.Fragmenter; 23 import org.apache.lucene.search.highlight.Highlighter; 24 import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; 25 import org.apache.lucene.search.highlight.QueryScorer; 26 import org.apache.lucene.search.highlight.SimpleHTMLFormatter; 27 import org.apache.lucene.search.highlight.SimpleSpanFragmenter; 28 import org.apache.lucene.store.Directory; 29 import org.apache.lucene.store.FSDirectory; 30 import org.apache.lucene.util.Version; 31 32 import IkAnalyzer.MyIkAnalyzer; 33 34 public class testBooleanQuery { 35 public static Version luceneVersion = Version.LATEST; 36 public static void indexSearch(String keywords){ 37 DirectoryReader reader = null; 38 try{ 39 Directory directory = FSDirectory.open(Paths.get("index3")); 40 reader = DirectoryReader.open(directory); 41 IndexSearcher searcher = new IndexSearcher(reader); 42 QueryParser parserkey1 = new QueryParser("key1",new MyIkAnalyzer());//content表示搜索的域或者说字段 43 Query querykey1 = parserkey1.parse(keywords); 44 String ss=querykey1.toString(); 45 System.out.println(ss); 46 QueryParser parserkey2 = new QueryParser("key2",new MyIkAnalyzer());//content表示搜索的域或者说字段 47 Query querykey2 = parserkey2.parse(keywords);//被搜索的内容 48 System.out.println(keywords); 49 BooleanQuery query=null; 50 String cate1="知识"; 51 String cate2="百科"; 52 Query querycate1=new TermQuery(new Term("category1",cate1)); 53 Query querycate2=new TermQuery(new Term("category2",cate2)); 54 query=new BooleanQuery.Builder().add(querycate1,BooleanClause.Occur.FILTER).add(querycate2,BooleanClause.Occur.FILTER).add(querykey1,BooleanClause.Occur.SHOULD).add(querykey2,BooleanClause.Occur.SHOULD).build(); 55 TopDocs tds = searcher.search(query, 20); 56 ScoreDoc[] sds = tds.scoreDocs; 57 int cou=0; 58 for(ScoreDoc sd:sds){ 59 cou++; 60 Document d = searcher.doc(sd.doc); 61 String output=cou+". "+d.get("skey1")+"\n"+d.get("skey2"); 62 System.out.println(output); 63 } 64 }catch(Exception e){ 65 e.printStackTrace(); 66 }finally{ 67 //9、关闭reader 68 try { 69 reader.close(); 70 } catch (IOException e) { 71 e.printStackTrace(); 72 } 73 } 74 } 75 public static void main(String[] args) throws IOException 76 { 77 String keywords="为什么眼睛会流泪?"; 78 indexSearch(keywords); 79 } 80 }
对于小明的需求,我们就可以利用BooleanQuery来设计:查询一是域为“分类”,搜索词为“游戏”的TermQuery;查询二为检索搜索关键词的某一Query;用BooleanQuery组合两个查询,查询一的运算符为过滤,查询二为MUST,这样,就能查找所有分类为“游戏”的内容了。
七、MultiFieldQuery
MultiFieldQuery是多域查询。比如用户有这样的需求:一个文档中含有“标题”,“正文”等字段,搜索一个关键词,不管它在标题中出现还是在正文中出现都算符合条件。这时,我们就用到了多域查询。
1 package testAdvancedQuery; 2 import java.nio.file.Paths; 3 import java.io.*; 4 5 import org.apache.lucene.analysis.Analyzer; 6 import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; 7 import org.apache.lucene.analysis.standard.StandardAnalyzer; 8 import org.apache.lucene.document.Document; 9 import org.apache.lucene.index.DirectoryReader; 10 import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; 11 import org.apache.lucene.search.BooleanClause; 12 import org.apache.lucene.search.IndexSearcher; 13 import org.apache.lucene.search.Query; 14 import org.apache.lucene.search.ScoreDoc; 15 import org.apache.lucene.search.TopDocs; 16 import org.apache.lucene.store.Directory; 17 import org.apache.lucene.store.FSDirectory; 18 import org.apache.lucene.util.Version; 19 import IkAnalyzer.MyIkAnalyzer; 20 public class testMultiFieldQuery { 21 public static Version luceneVersion = Version.LATEST; 22 public static void indexSearch(){ 23 DirectoryReader reader = null; 24 try{ 25 Directory directory = FSDirectory.open(Paths.get("index3"));//在硬盘上生成Directory 26 reader = DirectoryReader.open(directory); 27 IndexSearcher searcher = new IndexSearcher(reader); 28 Analyzer analyzer1=new StandardAnalyzer(); 29 Analyzer analyzer2=new SmartChineseAnalyzer(); 30 Analyzer analyzer3=new MyIkAnalyzer(); 31 // //方法一:利用BooleanQuery,在两个TermQuery之间做逻辑运算 32 // Term t1=new Term("key1","张飞"); 33 // Term t2=new Term("key2","刘备"); 34 // TermQuery q1=new TermQuery(t1); 35 // TermQuery q2=new TermQuery(t2); 36 // BooleanQuery query=new BooleanQuery.Builder().add(q1,BooleanClause.Occur.FILTER).add(q2,BooleanClause.Occur.MUST).build(); 37 // 38 //方法二:MultiFieldQueryParser类,实现多字段搜索,实际上只是一个封装,用起来简单,内部还是用BooleanQuery实现 39 String fields[]={"key1","key2"}; 40 String kws[]={"张飞","刘备"}; 41 //MUST:and;SHOULD:OR;MUST_NOT:NOT;FILTER:相当于MUST,但是不参与打分。 42 BooleanClause.Occur[] flags=new BooleanClause.Occur[]{BooleanClause.Occur.FILTER,BooleanClause.Occur.MUST}; 43 Query query=MultiFieldQueryParser.parse(kws,fields,flags,analyzer3); 44 String ss=query.toString(); 45 System.out.println(ss); 46 TopDocs tds = searcher.search(query, 20); 47 ScoreDoc[] sds = tds.scoreDocs; 48 int cou=0; 49 for(ScoreDoc sd:sds){ 50 cou++; 51 Document d = searcher.doc(sd.doc); 52 String output=cou+". "+d.get("skey1")+"\n"+d.get("skey2"); 53 System.out.println(output); 54 } 55 }catch(Exception e){ 56 e.printStackTrace(); 57 }finally{ 58 try { 59 reader.close(); 60 } catch (IOException e) { 61 e.printStackTrace(); 62 } 63 } 64 } 65 public static void main(String[] args) throws IOException 66 { 67 indexSearch(); //搜索的内容可以修改 68 } 69 }
MultiFieldQuery有两种实现方法:
方法一是利用BooleanQuery在多个TermQuery之间做逻辑运算
1 //方法一:利用BooleanQuery,在两个TermQuery之间做逻辑运算 2 Term t1=new Term("key1","张飞"); 3 Term t2=new Term("key2","刘备"); 4 TermQuery q1=new TermQuery(t1); 5 TermQuery q2=new TermQuery(t2); 6 BooleanQuery query=new BooleanQuery.Builder().add(q1,BooleanClause.Occur.FILTER).add(q2,BooleanClause.Occur.MUST).build();
方法二是MultiFieldQueryParser类,实现多域搜索,实际上只是一个封装,用起来简单,内部还是用BooleanQuery实现。
1 //方法二:MultiFieldQueryParser类,实现多字段搜索,实际上只是一个封装,用起来简单,内部还是用BooleanQuery实现 2 String fields[]={"key1","key2"}; 3 String kws[]={"张飞","刘备"}; 4 //MUST:and;SHOULD:OR;MUST_NOT:NOT;FILTER:相当于MUST,但是不参与打分。 5 BooleanClause.Occur[] flags=new BooleanClause.Occur[]{BooleanClause.Occur.FILTER,BooleanClause.Occur.MUST}; 6 Query query=MultiFieldQueryParser.parse(kws,fields,flags,analyzer3);
1 String ss=query.toString(); 2 System.out.println(ss);
可以利用上述代码把查询对象打印出来,便于直观感受lucene对查询的解析。
可以看出,MultiFieldQuery本质上也是BooleanQuery的应用,具体内容可以参考官方文档。
八、FieldQuery
FieldQuery是域搜索,用户可以通过输入符合语法规则的查询语句指定一次查询是在哪些域上进行。例如,如果索引的文档包含两个域,Title 和Content,用户可以使用查询 “Title: Lucene AND Content: Java” 来返回所有在 Title域上包含 Lucene 并且在 Content 域上包含 Java 的文档。
1 package testAdvancedQuery; 2 import java.io.IOException; 3 import java.nio.file.Paths; 4 5 import org.apache.lucene.document.Document; 6 import org.apache.lucene.index.DirectoryReader; 7 import org.apache.lucene.queryparser.classic.QueryParser; 8 import org.apache.lucene.search.IndexSearcher; 9 import org.apache.lucene.search.ScoreDoc; 10 import org.apache.lucene.search.TopDocs; 11 import org.apache.lucene.store.Directory; 12 import org.apache.lucene.store.FSDirectory; 13 import org.apache.lucene.util.Version; 14 15 import IkAnalyzer.MyIkAnalyzer; 16 public class testFieldQuery { 17 public static Version luceneVersion = Version.LATEST; 18 public static void indexSearch(String keywords){ 19 DirectoryReader reader = null; 20 try{ 21 Directory directory = FSDirectory.open(Paths.get("index3")); 22 reader= DirectoryReader.open(directory); 23 IndexSearcher searcher = new IndexSearcher(reader); 24 QueryParser parser = new QueryParser("key1",new MyIkAnalyzer());//content表示搜索的域或者说字段 25 org.apache.lucene.search.Query query = parser.parse(keywords);//被搜索的内容 26 String ss=query.toString(); 27 System.out.println(ss); 28 TopDocs tds = searcher.search(query, 20); 29 ScoreDoc[] sds = tds.scoreDocs; 30 int cou=0; 31 for(ScoreDoc sd:sds){ 32 cou++; 33 Document d = searcher.doc(sd.doc); 34 String output=cou+". "+d.get("category2")+"\n"+d.get("skey1")+"\n"+d.get("skey2"); 35 System.out.println(output); 36 } 37 }catch(Exception e){ 38 e.printStackTrace(); 39 }finally{ 40 try { 41 reader.close(); 42 } catch (IOException e) { 43 e.printStackTrace(); 44 } 45 } 46 } 47 public static void main(String[] args) throws IOException 48 { 49 String keywords="key1:猪八戒 AND key2:孙悟空 "; 50 indexSearch(keywords); 51 52 } 53 }
1 String ss=query.toString(); 2 System.out.println(ss);
打印查询对象后发现,lucene对用户查询语句的处理与对BooleanQuery的处理结果是一样的。
九、MultiSearcher
MultiSearcher是多索引搜索。可以这样理解:
为了减少单个索引目录的大小,时常将索引放在许多目录中,这些索引的结构都是一致的。比如有一个城市的网站搜索引擎,随着时间的增长,我们可能会将索引的目录按照年份分成2003、2004、2005等。旧的索引目录被搜索的几率小,所以将其单独分出去,这样,可以减小新的索引目录,加快搜索速度。但是有些时候,必须实现多个索引的同时搜索,因为我们需要存放在这些索引中的信息。要实现多索引搜索,只需要对每个索引目录都用IndexSearcher搜索一遍,最后将搜索结果合并起来。
实际上,lucene6.2.1已经废弃了MultiSearcher这个类,改用MultiReader来实现。
1 package testAdvancedQuery; 2 import java.nio.file.Paths; 3 import java.io.*; 4 5 import org.apache.lucene.analysis.Analyzer; 6 import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; 7 import org.apache.lucene.analysis.standard.StandardAnalyzer; 8 import org.apache.lucene.document.Document; 9 import org.apache.lucene.index.DirectoryReader; 10 import org.apache.lucene.index.MultiReader; 11 import org.apache.lucene.index.Term; 12 import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; 13 import org.apache.lucene.queryparser.classic.QueryParser; 14 import org.apache.lucene.search.BooleanClause; 15 import org.apache.lucene.search.BooleanClause.Occur; 16 import org.apache.lucene.search.BooleanQuery; 17 import org.apache.lucene.search.Explanation; 18 import org.apache.lucene.search.IndexSearcher; 19 import org.apache.lucene.search.Query; 20 import org.apache.lucene.search.ScoreDoc; 21 import org.apache.lucene.search.TermQuery; 22 import org.apache.lucene.search.TopDocs; 23 import org.apache.lucene.store.Directory; 24 import org.apache.lucene.store.FSDirectory; 25 import org.apache.lucene.util.Version; 26 import IkAnalyzer.MyIkAnalyzer; 27 28 public class testMultiSearcher { 29 public static Version luceneVersion = Version.LATEST; 30 public static void indexSearch(String keywords){ 31 String res = ""; 32 DirectoryReader reader1 = null; 33 DirectoryReader reader2 = null; 34 MultiReader mr=null; 35 try{ 36 Directory directory1 = FSDirectory.open(Paths.get("index1")); 37 Directory directory2 = FSDirectory.open(Paths.get("index2")); 38 reader1 = DirectoryReader.open(directory1); 39 reader2 = DirectoryReader.open(directory2); 40 mr=new MultiReader(reader1,reader2); 41 IndexSearcher searcher1 = new IndexSearcher(mr); 42 IndexSearcher searcher2=new IndexSearcher(reader1); 43 IndexSearcher searcher3=new IndexSearcher(reader2); 44 QueryParser parser = new QueryParser("key2",new MyIkAnalyzer());//content表示搜索的域或者说字段 45 Query query = parser.parse(keywords); 46 String ss=query.toString(); 47 System.out.println(ss); 48 TopDocs tds = searcher1.search(query, 20); 49 ScoreDoc[] sds = tds.scoreDocs; 50 int cou=0; 51 System.out.println("MultiSearcher的结果:"); 52 for(ScoreDoc sd:sds){ 53 cou++; 54 Document d = searcher1.doc(sd.doc); 55 String output=cou+". "+d.get("category2")+"\n"+d.get("skey1"); 56 System.out.println(output); 57 } 58 //************************************************* 59 System.out.println("只搜索百科的结果:"); 60 tds = searcher2.search(query, 10); 61 sds = tds.scoreDocs; 62 cou=0; 63 for(ScoreDoc sd:sds){ 64 cou++; 65 Document d = searcher2.doc(sd.doc); 66 String output=cou+". "+d.get("category2")+"\n"+d.get("skey1"); 67 System.out.println(output); 68 } 69 //******************************************** 70 System.out.println("只搜索课本的结果:"); 71 tds = searcher3.search(query, 10); 72 sds = tds.scoreDocs; 73 cou=0; 74 for(ScoreDoc sd:sds){ 75 cou++; 76 Document d = searcher3.doc(sd.doc); 77 String output=cou+". "+d.get("category2")+"\n"+d.get("skey1"); 78 System.out.println(output); 79 } 80 }catch(Exception e){ 81 e.printStackTrace(); 82 }finally{ 83 try { 84 mr.close(); 85 } catch (IOException e) { 86 e.printStackTrace(); 87 } 88 } 89 } 90 public static void main(String[] args) throws IOException 91 { 92 String keyword="眼睛"; 93 indexSearch(keyword); 94 } 95 }
这样,我们可以为“游戏”,“课本”等独立地建立索引,用户想搜索游戏时,我们可以指定索引目录为游戏,用户想综合所有类别搜索时,我们可以用MultiSearcher来搜索所有类目~
我们可能担心,在索引的过程中,分散地存储到多个索引目录中,是否在搜索时能够得到全局的相关度计算得分?其实Lucene的这个方法支持全局得分的计算,也就是说,虽然索引分布在多个索引目录中,在搜索的时候还会将全部的索引数据聚合在一起进行查询匹配和得分计算。
利用多域搜索还可以实现多线程搜索,这个有待研究~
十、QueryParser
QueryParse类不是一种Query,但可以通过设置QueryParser的参数,实现多字段搜索,也能实现BooleanQuery的一部分效果。
使用QueryParser解析多个关键词,比如用户搜索“love China”,打印查询对象后发现,两个关键词是或的关系。
使用下面的方法后,QueryParser就可以对两个关键词取交了:
1 //方式一: 2 parser.setDefaultOperator(QueryParser.Operator.AND); 3 //方式二: 4 parser.createBooleanQuery("key1", keywords, Occur.MUST);
方法一:
1 parser.setDefaultOperator(QueryParser.Operator.AND);
参数QueryParser.Operator.AND表示与,QueryParser.Operator.OR表示或。
方法二:
1 parser.createBooleanQuery("key1", keywords, Occur.MUST);
参数一表示域,参数二表示搜索关键词,参数三只有两种,Occur.MUST和Occur.SHOULD,而且都表示逻辑与。
以上就是这节的五种高级检索,我们发现,这五种方法都可以解决分类搜索或过滤问题,其中BooleanQuery是最基本的用法。
综合一二两节,我们已经学习了十种高级检索方式,其实lucene内部还有很多方法,以后有机会我们再来一起探索。
接下来,我们会一起揭开“lucene近实时搜索”的神秘面纱~~