从Lucene到Elasticsearch:全文检索实战
上QQ阅读APP看书,第一时间看更新

2.5 Lucene查询详解

文档索引完成以后就可以对其进行搜索,当用户输入一个关键字,搜索引擎接收到后,并不是立刻就将它放入后台开始进行关键字的检索,而应当首先对这个关键字进行一定的分析和处理,使之成为一种后台可以理解的形式,只有这样,才能提高检索的效率,同时检索出更加有效的结果。

2.5.1 搜索入门

在Lucene中,处理用户输入的查询关键词其实就是构建Query对象的过程。Lucene搜索文档需要实例化一个IndexSearcher对象,IndexSearcher对象的search()方法完成搜索过程,Query对象作为search()方法的对象。搜索结果会保存在一个TopDocs类型的文档集合中,遍历TopDocs集合输出文档信息。见代码清单2-11。

代码清单2-11

            package tup.lucene.queries;
            import java.io.IOException;
            import java.nio.file.Path;
            import java.nio.file.Paths;
            import org.apache.lucene.analysis.Analyzer;
            import org.apache.lucene.document.Document;
            import org.apache.lucene.index.DirectoryReader;
            import org.apache.lucene.index.IndexReader;
            import org.apache.lucene.queryparser.classic.ParseException;
            import org.apache.lucene.queryparser.classic.QueryParser;
            import org.apache.lucene.search.IndexSearcher;
            import org.apache.lucene.search.Query;
            import org.apache.lucene.search.ScoreDoc;
            import org.apache.lucene.search.TopDocs;
            import org.apache.lucene.store.Directory;
            import org.apache.lucene.store.FSDirectory;
            import tup.lucene.ik.IKAnalyzer6x;
            public class QueryParseTest{
            public static void main(String[] args)
                throws ParseException, IOException {
                    String field = "title";
                    Path indexPath = Paths.get("indexdir");
                    Directory dir = FSDirectory.open(indexPath);
                    IndexReader reader = DirectoryReader.open(dir);
                    IndexSearcher searcher = new IndexSearcher(reader);
                    Analyzer analyzer = new IKAnalyzer6x();
                    QueryParser parser = new QueryParser(field, analyzer);
                    parser.setDefaultOperator(Operator.AND);
                    Query query = parser.parse("农村学生"); // 查询关键词
                    System.out.println("Query:"+query.toString());
                    // 返回前10条
                    TopDocs tds = searcher.search(query, 10);
                    for(ScoreDoc sd : tds.scoreDocs){
                        Document doc = searcher.doc(sd.doc);
                        System.out.println("DocID:" + sd.doc);
                        System.out.println("id:" + doc.get("id"));
                        System.out.println("title:" + doc.get("title"));
                        System.out.println("文档评分:" + sd.score);
                    }
                    dir.close();
                    reader.close();
                }
            }

注意上面代码中的这几行代码:

        QueryParser parser = new QueryParser(field, analyzer);
        Query query = parser.parse("农村学生");
        parser.setDefaultOperator(Operator.AND);
        TopDocs topDocs = searcher.search(query, 10);

QueryParser实际上就是一个解析用户输入的工具,可以通过扫描用户输入的字符串生成Query对象。当使用QueryParser构建用户Query时,要搜索的field和analyzer对象作为参数传入QueryParser类,告诉QueryParser在哪个字段内查找该关键字信息以及搜索时使用什么样的分词器。这里设置要查询的字段为title字段,查询所用的分词器为IK智能分词,查询关键词为“农村学生”,关键词经过分词器分成“农村”和“学生”两个词项,title中含有这两个词项中的任何一个的文档都是本次查询的匹配文档。如果只想返回同时包含两个词项的文档,可以通过setDefaultOperator()方法把关键词理解为AND操作。

注意,在结果中打印了DocID和id,前者是文档ID,是Lucene为索引的每个文档做的标记,后者是文档内部的id字段。

运行结果如下:

        加载扩展词典:ext.dic
        加载扩展停止词典:stopword.dic
        加载扩展停止词典:ext_stopword.dic
        Query:+title:农村 +title:学生
        DocID:1
        id:2
        title:北大迎4380名新生 农村学生700多人近年最多
        文档评分:1.6022166

改变上面搜索实例的Query对象就可以实现不同种类型的搜索需求,比如多域查询、布尔查询、模糊查询、通配符查询等。

2.5.2 多域搜索(MultiFieldQueryParser)

2.4.1小节中介绍的QueryParser可以搜索单个字段,而MultiFieldQueryParser则可以查询多个字段。通过MultiFieldQueryParser对象生成Query对象的代码如下:

        String[] fields = { "title", "content" };
        Analyzer analyzer = new IKAnalyzer6x(true);
        MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer);
        Query multiFieldQuery = parser.parse("日本");

通过IndexSearcher搜索文档和打印结果的代码和2.4.1中的一样,这里省略,给出查询结果:

        title:日本content:日本
        DocID:0
        id:1
        title:安倍晋三本周会晤特朗普 将强调日本对美国益处
        content:日本首相安倍晋三计划2月10日在华盛顿与美国总统特朗普举行会晤时提出加
                大日本在美国投资的设想
        文档评分:2.0341508

2.5.3 词项搜索(TermQuery)

TermQuery是最简单也是最常用的Query。TermQuery可以理解成为“词项搜索”,在搜索引擎中最基本的搜索就是在索引中搜索某一词条,而TermQuery就是用来完成这项工作的。在Lucene中词条是最基本的搜索单位,从本质上来讲一个词条其实就是一个key/value对。只不过这个key是字段名,而value则表示字段中所包含的某个关键字。要使用TermQuery进行搜索首先需要构造一个Term对象,示例代码如下:

        Term term = new Term("title", "美国");

然后使用Term对象为参数来构造一个TermQuery对象,代码设置如下:

        Query termQuery = new TermQuery(term);

这样所有在“title”字段中包含有“美国”的文档都会在使用TermQuery进行查询时作为符合查询条件的结果返回。同样省略IndexSearcher搜索文档和打印结果的代码,运行结果如下:

        Query:title:美国
        DocID:2
        id:3
        title:特朗普宣誓就任美国第45任总统
        文档评分:0.53872687
        DocID:0
        id:1
        title:安倍晋三本周会晤特朗普 将强调日本对美国益处
        文档评分:0.38388318

2.5.4 布尔搜索(BooleanQuery)

BooleanQuery也是实际开发过程中经常使用的一种Query查询,它其实是一个组合的Query,在使用时可以把各种Query对象添加进去并标明它们之间的逻辑关系。BooleanQuery本身来讲是一个布尔子句的容器,它提供了专门的API方法往其中添加子句,并标明它们之间的关系。下面的代码中,创建了两个TermQuery, BooleanClause对象可以指定查询的包含关系,并作为参数通过BooleanQuery.Builder()构造布尔查询。查询title字段中包含关键词“美国”并且“content”字段中不包含日本的文档,代码如下:

        Query query1 = new TermQuery(new Term("title","美国"));
        Query query2 = new TermQuery(new Term("content","日本"));
        BooleanClause bc1=new BooleanClause(query1,Occur.MUST);
        BooleanClause bc2=new BooleanClause(query2,Occur.MUST_NOT);
        BooleanQuery boolQuery=new BooleanQuery.Builder()
            .add(bc1).add(bc2).build();

查询结果如下:

        Query:+title:美国 -content:日本
        DocID:2
        id:1
        title:特朗普宣誓就任美国第45任总统
        content:当地时间1月20日,唐纳德·特朗普在美国国会宣誓就职,正式成为美国第45任总统。
        文档评分:0.53872687

2.5.5 范围搜索(RangeQuery)

有时用户需要查找满足一定范围的文档,比如查找某一时间段内的所有文档,Lucene提供了RangeQuery查询来满足这种需求。

RangeQuery表示在某范围内的搜索条件,实现从一个开始词条到一个结束词条的搜索功能,在查询时“开始词条”和“结束词条”可以包含在内也可以不被包含在内。查询新闻回复条数在500条到1000条之间的有哪些,构成Query对象的代码如下:

        Query rangeQuery=IntPoint.newRangeQuery("reply",500,1000);

同样省略IndexSearcher搜索文档和打印结果的代码,查询结果如下:

        Query:reply:[500 TO 1000]
        DocID:0
        id:1
        title:安倍晋三本周会晤特朗普 将强调日本对美国益处
        Reply:672
        文档评分:1.0
        DocID:1
        id:2
        title:北大迎4380名新生 农村学生700多人近年最多
        Reply:995
        文档评分:1.0

2.5.6 前缀搜索(PrefixQuery)

PrefixQuery就是使用前缀来进行查找的。通常情况下,首先定义一个词条Term。该词条包含要查找的字段名以及关键字的前缀,然后通过该词条构造一个PrefixQuery对象,就可以进行前缀查找了。查询包含以“学”开头的词项的文档,构造Query对象的代码如下:

        Term term = new Term("title", "学");
        Query prefixQuery = new PrefixQuery(term);

同样省略IndexSearcher搜索文档和打印结果的代码,查询结果如下:

        Query:title:学*
        DocID:1
        id:2
        title:北大迎4380名新生 农村学生700多人近年最多
        文档评分:1.0

2.5.7 多关键字搜索(PhraseQuery)

除了普通的TermQuery外,Lucene还提供了一种Phrase查询功能。用户在搜索引擎中进行搜索时,常常查找的并非是一个简单的单词,很有可能是几个不同的关键字。这些关键字之间要么是紧密相连的,成为一个精确的短语,要么在这几个关键字之间还插有其他无关的内容。

PhraseQuery正是Lucene所提供的满足上述需求的一种Query对象。它的add方法可以让用户向其内部添加关键字,在添加完毕后,用户还可以通过setSlop()方法来设定一个称之为“坡度”的变量来确定关键字之间是否允许或允许多少个无关词汇的存在。

        PhraseQuery.Builder builder = new PhraseQuery.Builder();
        builder.add(new Term("title", "日本"), 2);
        builder.add(new Term("title", "美国"), 3);
        PhraseQuery phraseQuery = builder.build();
        打印phraseQuery对象:
        Query:title:"? ?日本 美国"

2.5.8 模糊搜索(FuzzyQuery)

FuzzyQuery是一种模糊查询,它可以简单地识别两个相近的词语。例如,由于拼写错误把“Trump”拼成“Trmp”或者“Tramp”,使用FuzzyQuery仍可搜索到正确的结果,代码如下:

        Term term = new Term("title", "Tramp");
        FuzzyQuery fuzzyQuery = new FuzzyQuery(term);
        Query:title:Tramp~2
        DocID:2
        id:3
        title:特朗普宣誓(Donald Trump)就任美国第45任总统
        文档评分:0.6056149

2.5.9 通配符搜索(WildcardQuery)

Lucene也提供了通配符的查询,就是使用WildcardQuery。示例如下:

        WildcardQuery wildcardQuery=new WildcardQuery(new Term(field, "学?"));
        Query:title:学*
        DocID:1
        id:2
        title:北大迎4380名新生 农村学生700多人近年最多
        文档评分:1.0