chapter03_为应用程序添加搜索功能

  1. Lucene主要的API

    目的
    IndexSearcher 搜索索引的入口,调用重载的search方法
    Query及其子类 封装某种查询类型的具体子类,作为search方法的参数
    QueryParser 将用户输入的查询表达式处理成具体的Query的对象
    TopDocs 保持由IndexSearcher.search()方法返回的具有较高评分的顶部文档
    ScoreDoc TopDocs中每条搜索结果的访问接口
  2. 对特定项的搜索 —— 使用 TermQuery

     public void testTerm() throws Exception {
         // 1. 打开索引文件所在的目录,委托给 Directory 对象
         Directory directory = FSDirectory.open(new File("./index"));
     
         // 2. 创建 IndexSearcher 对象
         IndexSearcher indexSearcher = new IndexSearcher(directory);  
    
         // 3. 创建 TermQuery
         Term term = new Term("subject", "ant");
         Query query = new TermQuery(term);
    
         // 4. 返回 TopDocs 结果
         TopDocs docs = indexSearcher.search(query, 10);
    
         ...   // 这里可以使用 docs.scoreDocs 获取细节
    
         // 5. 关闭 IndexSearcher 对象和 Directory 对象
         indexSearcher.close();
         directory.close();
     }
    

    在一个程序中,最好让 IndexSearcher 对象和 Directory 对象保持打开状态,最后结束时再关闭。因为重新打开要花费代价。

  3. 解析用户输入的查询表达式 —— QueryParser

     public void testQueryParser() throws Exception {
    
         // 1. 打开索引文件所在的目录,委托给 Directory 对象
         Directory directory = FSDirectory.open(new File("./index"));
    
         // 2. 创建 IndexSearcher 对象
         IndexSearcher indexSearcher = new IndexSearcher(directory);
    
         // 3. 创建 QueryParser 对象,
         QueryParser parser = new QueryParser(
             Version.LUCENE_30,      
             "contents",                  
             new SimpleAnalyzer());       
    
         // Example 1
         // 4. 解析 "+JUNIT +ANT -MOCK" 类型的查询
         Query query = parser.parse("+JUNIT +ANT -MOCK");                  /
         
         TopDocs docs = indexSearcher.search(query, 10);
         assertEquals(1, docs.totalHits);
    
         Document d = indexSearcher.doc(docs.scoreDocs[0].doc);
         assertEquals("Ant in Action", d.get("title"));
    
         // Example 2
         // 4. 解析 "mock OR junit" 类型的查询
         query = parser.parse("mock OR junit");                            
         docs = indexSearcher.search(query, 10);
         assertEquals("Ant in Action, " +
                     "JUnit in Action, Second Edition",
                 2, docs.totalHits);
    
         // 关闭 IndexSearcher 对象和 Directory 对象
         indexSearcher.close();
         directory.close();
     }
    

    QueryParser将用户输入转换为可以处理的Query

     QueryParser parser = new QueryParser(
         Version matchVersion, 
         String defaultField, 
         Analyzer analyzer);
    

    QueryParser处理的表达式范例 P75

    查询表达式 匹配Doc
    java 默认Field包含java的文档
    java junit
    java OR junit 默认Field包含java或junit的文档
    +java +junit
    java AND junit 默认Field同时包含java和junit的文档
    title:ant title Field包含ant的文档
    title:extreme -subject:sports title Field包含extreme且subject Field不包含sports的文档
    (agile OR extreme) AND methodology 默认Field包含methodology且包含agile和extreme其中之一的文档
    title:“junit in action” title Field恰好为junit in action的文档
    title:“junit action” ~5 title Field恰好为junit和action距离小于5的文档
    java* 包含由java*开头的文档
    java~ 包含与单词java相近的项的文档
    lastModified:[1/1/09 TO 12/31/09] lastModified项在1/1/09和12/31/09之间的文档
  4. 使用IndexSearcher类

    1. 示例

       Directory dir = FSDirectory.open(new File("/path/to/index"));
       IndexReader reader = IndexReader.open(dir);
       IndexSearcher searcher = new IndexSearcher(reader);
      
    2. 上个示例和之前的区别是多了一个 IndexReader 中间层,事实上以前直接使用

       IndexSearcher searcher = new IndexSearcher(dir);
      

      的时候,系统后台也会创建私有的IndexReader

      值得注意的是,IndexReader和IndexSearcher对象在关闭的时候关闭一个就行,因为IndexSearcher的源码是这样的

       public class IndexSearcher extends Searcher {
      
           ...
      
           public void close() throws IOException {
               if(closeReader)
                   reader.close();
           }
      
           ...
       }
      
    3. 几个类的关系

       Query ----> IndexSearcher ----> TopDocs
                         |
                         |
                    IndexReader
                         |
                         |
                     Directory
                         |
                         |
                      索引文件
      
    4. 打开IndexReader需要较大的系统开销,因此最好使用同一个IndexReader对象

    5. 当底层索引更新时,原有的IndexReader看不到更新,必须使用新的IndexReader对象

    6. 为了同时解决4和5的问题,建议使用reopen方法

       IndexReader newReader = reader.reopen();
       if (reader != newReader) {
           reader.close();
           reader = newReader;
           searcher = new IndexSearcher(reader);
       }
      

      更新IndexReader后别忘了同时更新IndexSearcher

    7. search API

       TopDocs topDocs = indexSearcher.search(Query query, int n);
      

      返回评分最高的n个Docs

    8. TopDocs

       public class TopDocs implements java.io.Serializable {
      
           /** The total number of hits for the query.*/
           public int totalHits;
      
           /** The top hits for the query. */
           public ScoreDoc[] scoreDocs;
      
           /** Stores the maximum score value encountered, needed for normalizing. */
           private float maxScore;
      
           ...
       }
      

      totalHits:匹配搜索条件的总Docs数量

      maxScore:所有匹配Doc的最大评分

      scoreDocs:一个数组,包含top n个匹配Doc的信息,这个数组按照匹配程度进行排序(第一个文档最匹配)

       public class ScoreDoc implements java.io.Serializable {
      
           /** Expert: The score of this document for the query. */
           public float score;
      
           /** Expert: A hit document's number.
           * @see Searcher#doc(int)
           */
           public int doc;
      
           ...
       }
      

      其中,score代表该文档的相关性评分,doc代表文档ID

    9. 搜索结果分页

      (1) 两种方案

      1° 首次搜索时多搜一些,然后缓存在ScoreDocs实例中

      2° 每次用户换页时重新搜索

      (2) 一般方案2°更合理,因为Lucene可以快速处理,这样不需要缓存用户状态

    10. 近实时搜索

      (1) 含义:IndexWriter未commit或close的结果,就会被IndexSearcher看到

      (2) 主要API

      IndexReader indexReader = indexWriter.getReader();
      

      这种方式可以实现近实时搜索,语义上等价于先commit再open一个IndexReader,但是大大减少commit开销

      但是要注意的是这种方法只是不用commit就能看到结果而已,还是要显式IndexReader.reopen()以及更新IndexSearcher对象

  5. Lucene的评分机制

    1. 一个没看懂的评分公式 P82

    2. 使用 explain() 理解搜索结果评分

       indexSearcher.explain(Query query, int doc)
      
    3. 一个 explain 结果示例

       Query: junit
      
       ----------
       JUnit in Action, Second Edition
       0.7629841 = (MATCH) fieldWeight(contents:junit in 8), product of:
           1.4142135 = tf(termFreq(contents:junit)=2)
           2.466337 = idf(docFreq=2, maxDocs=13)
           0.21875 = fieldNorm(field=contents, doc=8)
      
       ----------
       Ant in Action
       0.61658424 = (MATCH) fieldWeight(contents:junit in 6), product of:
           1.0 = tf(termFreq(contents:junit)=1)
           2.466337 = idf(docFreq=2, maxDocs=13)
           0.25 = fieldNorm(field=contents, doc=6)
      

      其中,

       0.7629841 = 1.4142135 * 2.466337 * 0.21875
      

       0.61658424 = 1.0 * 2.466337 * 0.25
      
  6. Lucene的多样化查询

    1. 创建Query的办法有两种

      (1) 直接实例化Query的子类

      (2) 通过 QueryParser.query(String userInput) 返回Query实例对象

    2. 通过项进行搜索——TermQuery

      (1) 示例

       Directory directory = FSDirectory.open(new File("./index"));
       IndexSearcher indexSearcher = new IndexSearcher(directory);
      
       Term t = new Term("isbn", "9781935182023");
       Query query = new TermQuery(t);
       TopDocs docs = indexSearcher.search(query, 10);
      

      (2) TermQuery在根据关键字查询文档时特别实用,如果文档是通过 Field.Index.NOT_ANALYZED进行索引的

    3. 在指定的项范围内搜索——TermRangeQuery

      (1) 示例

       TermRangeQuery query = new TermRangeQuery(
           "title2", "d", "j",
           true, true);
      

      创建时的域

       doc.add(new Field(
           "title2", 
           title.toLowerCase(), 
           Field.Store.YES, 
           Field.Index.NOT_ANALYZED_NO_NORMS, 
           Field.TermVector.WITH_POSITIONS_OFFSETS)); 
      

      (2) TermRangeQuery用于文本范围查询,上面示例的意思是,在title2这个Field里,找[d, j]范围内的Doc

      (3) Lucene通常按照字典编排顺序(String.compareTo)来存储Term,也就是说可以很快速的找到某个区间范围

    4. 在指定的数字范围内搜索——NumericRangeQuery

      (1) 如果使用 NumericField 对象来索引域,那么就可以有效使用 NumericRangeQuery 类来在某个特定范围内搜索该域

       NumericRangeQuery query = NumericRangeQuery.newIntRange(
           "pubmonth", 200605, 200609,
           true, true);
      

      创建域时使用NumericField

       doc.add(new NumericField(
           "pubmonth", Field.Store.YES, true).setIntValue(Integer.parseInt(pubmonth)));  
      
    5. 通过字符串搜索——PrefixQuery

      (1) PrefixQuery搜索包含指定字符串开头的项的文档

       Term term = new Term("category", 
       "/technology/computers/programming");    
      
       PrefixQuery query = new PrefixQuery(term);    
      
    6. 组合查询——BooleanQuery

      (1) BooleanQuery 可以作为多个查询子句的组合,依靠add API

       public void add(Query query, BooleanClause.Occur occur)
      

      其中,Occur是枚举类型,包含三种取值

       MUST      --- 代表 AND
       SHOULD    --- 代表 OR
       MUST_NOT  --- 代表 NOT
      

      (2) 示例

       TermQuery searchingBooks = new TermQuery(new Term("subject", "search"));  // 1
      
       Query books2010 = NumericRangeQuery.newIntRange("pubmonth", 201001, 201012, true, true);   // 2     
       BooleanQuery searchingBooks2010 = new BooleanQuery();  
      
       // AND two sub-queries
       searchingBooks2010.add(searchingBooks, BooleanClause.Occur.MUST);  
       searchingBooks2010.add(books2010, BooleanClause.Occur.MUST);       
      
    7. 通过短语搜索——PhraseQuery

      (1) 两个term之间的最大间隔距离slop

      term若要按顺序组成给定的短语所需要的移动位置的次数

      示例

       原Doc:the quick brown fox jumped over the lazy dog
      
       短语 "quick fox": slop = 1,让fox向右移动1次和原Doc匹配
      
       短语 "fox quick": slop = 3,让fox向右移动3次和原Doc匹配
      

      (2) 多个term同样支持

      示例

       原Doc:the quick brown fox jumped over the lazy dog
      
       短语 "lazy jumped quick": slop = 8(4+4)
      

      (3) 代码示例

       PhraseQuery query = new PhraseQuery();  
       query.setSlop(slop);  // 设置Query的slop值
      
       String[] phrase = new String[]{"lazy", "jumped", "quick"};
      
       for (String word : phrase) {           
           query.add(new Term("field", word));          
       }                                                   
      
       TopDocs matches = searcher.search(query, 10);
      
       AssertTrue(matches.totalHits > 0);
      

      注:在setSlop之前,slop默认值为0

      (4) 短语查询评分

       1/(distance + 1)
      
    8. 通配符查询——WildcardQuery

      (1) Lucene使用两个标准的通配符

      1° * 代表0个或多个字母

      2° ? 代表0个或1个字母

      (2) 示例

       Query query = new WildcardQuery(new Term("contents", "?ild*"));    
      
       indexSingleFieldDocs(
           new Field[]{
                   new Field("contents", "wild", Field.Store.YES, Field.Index.ANALYZED),
                   new Field("contents", "child", Field.Store.YES, Field.Index.ANALYZED),
                   new Field("contents", "mild", Field.Store.YES, Field.Index.ANALYZED),
                   new Field("contents", "mildew", Field.Store.YES, Field.Index.ANALYZED)});
      

      (3) 通配符匹配查询对评分没有任何影响

    9. 搜索类似项——FuzzyQuery

      (1) 用于判断一个term内部字母移动的次数,如果

       1 - distance / min(textLen, targetLen)
      

      小于阈值则hit

      (2) 示例

       Query query = new FuzzyQuery(new Term("contents", "wuzza"));
      
       indexSingleFieldDocs(new Field[]{
           new Field("contents", "fuzzy", Field.Store.YES, Field.Index.ANALYZED),
           new Field("contents", "wuzzy", Field.Store.YES, Field.Index.ANALYZED)
       });
      
    10. 匹配所有文档——MatchAllDocsQuery

      匹配索引中的所有文档

  7. 解析查询表达式——QueryParser

    1. 通过QueryParser可以将自然语言的查询表达式转换为某个Query子类实例,转换过程由后台完成

      转换后的Query对象调用toString()方法可以看到转换后的形式

    2. TermQuery

      单个词在默认情况下,如果不被识别为更长的其他查询表达式的一部分,就会被QueryParser解析为TermQuery对象

       Query query = new QueryParser(
           Version.LUCENE_30, "subject", 
           new WhitespaceAnalyzer()
           ).parse("computers);
      
    3. TermRangeQuery

      (1) 用TO连接,必须大写

      (2) 中括号[]代表inclusive,大括号{}代表exclusive,不支持半开半闭

       Query query = new QueryParser(
           Version.LUCENE_30, "subject", 
           new WhitespaceAnalyzer()
           ).parse("title2:[Q TO V]");
      
       Query query = new QueryParser(
           Version.LUCENE_30, "subject", 
           new WhitespaceAnalyzer()
           ).parse("title2:{Q TO \"Tapestry in Action\"}");
      

      (3) 3.0.2及其以前版本的Lucene不支持自动转换为 NumericRangeQuery

    4. 前缀查询和通配符查询

      (1) 如果某个项中包含了一个?或*,该项被视为通配符查询WildcardQuery;如果某个项只在末尾有一个?,该项被视为前缀查询PrefixQuery

      (2) 示例

       Query query1 = new QueryParser(
           Version.LUCENE_30, "field", 
           new WhitespaceAnalyzer()
           ).parse("PrefixQuery*");    
       assertTrue(query1 instanceof PrefixQuery); 
      
       Query query2 = new QueryParser(
           Version.LUCENE_30, "field", 
           new WhitespaceAnalyzer()
           ).parse("Prefix?Query*");    
       assertTrue(query2 instanceof WildcardQuery);   
      
    5. 布尔查询

      (1) 布尔操作符必须是大写形式

      AND OR NOT

      (2) AND 可以用 + 代替;

      OR 可以用 空格代替(默认)

      NOT 可以用 - 代替

      (3) 示例

       Query query = new QueryParser(
           Version.LUCENE_30, "subject",
           new WhitespaceAnalyzer()
           ).parse("(agile OR extreme) AND methodology");
      
       assertTrue(query instanceof BooleanQuery);
      
    6. 短语查询

      (1) 双引号""括起来的term会被创建一个PhraseQuery,如果内部是多个单词的话,但是分析之后的结果会随着Analyzer的不同而不同,一些the/a之类的可能会被去掉

       Query query1 = new QueryParser(
           Version.LUCENE_30, "field",
           new StandardAnalyzer(Version.LUCENE_30))
           .parse("\"This is Some Phrase*\"");
      
       assertTrue(query1 instanceof PhraseQuery);   // 短语查询
      
       Query query2 = new QueryParser(Version.LUCENE_30, "field",
           new StandardAnalyzer(Version.LUCENE_30))
           .parse("This is Some Phrase");
      
       assertTrue(query instanceof BooleanQuery);  // 布尔查询
      

      注意,没有"",就会被解析为布尔查询,因为空格默认代表OR

    7. MatchAllDocsQuery

      输入

       *:*
      
    8. 为子查询加权

      (1) 浮点数前面加上一个^符号代表对查询处理进行加权因子的设置,默认为1.0

       Query q = new QueryParser(Version.LUCENE_30,
           "field", analyzer).parse("term^2 junit");
      
发布了391 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/captxb/article/details/103299925