Lucene分类统计示例

需求

在检索系统中,遇到了分组统计(Grouping/GroupBy)的需求,比如将搜索结果按照栏目分类,统计每个栏目下各有多少条结果。以前的做法很愚蠢,先发起一次search统计出有多少组,然后在每个组里发起一次search;这样在有N组的情况下一共执行了N+1此搜索,效率低下。

改进

最近发现Lucene提供了分组的功能,是通过Collector实现的,最多可以在2次search的时候得出结果,如果内存够用,CachingCollector还可以节约一次查询。

两次检索

第一次

第一次的目的是收集符合条件的组,创建一个FirstPassGroupingCollector送入search接口即可。在此处使用CachingCollector对其cache的话,可以节省一次查询:


 
  1.         TermFirstPassGroupingCollector c1 = new TermFirstPassGroupingCollector("catalog", groupSort, topNGroups);
  2.         boolean cacheScores = true;
  3.         double maxCacheRAMMB = 16.0;
  4.         CachingCollector cachedCollector = CachingCollector.create(c1, cacheScores, maxCacheRAMMB);
  5.         searcher.search(query, cachedCollector);

第二次

第二次的目的是收集每个组里面符合条件的文档,此时利用第一次的分组结果创建TermSecondPassGroupingCollector,并执行/replay搜索。

完整实例


 
  1. package com.hankcs;
  2.  
  3. import org.apache.lucene.analysis.Analyzer;
  4. import org.apache.lucene.analysis.standard.StandardAnalyzer;
  5. import org.apache.lucene.document.Document;
  6. import org.apache.lucene.document.Field;
  7. import org.apache.lucene.document.TextField;
  8. import org.apache.lucene.index.DirectoryReader;
  9. import org.apache.lucene.index.IndexReader;
  10. import org.apache.lucene.index.IndexWriter;
  11. import org.apache.lucene.index.IndexWriterConfig;
  12. import org.apache.lucene.queryparser.classic.QueryParser;
  13. import org.apache.lucene.search.*;
  14. import org.apache.lucene.search.grouping.GroupDocs;
  15. import org.apache.lucene.search.grouping.SearchGroup;
  16. import org.apache.lucene.search.grouping.TopGroups;
  17. import org.apache.lucene.search.grouping.term.TermAllGroupsCollector;
  18. import org.apache.lucene.search.grouping.term.TermFirstPassGroupingCollector;
  19. import org.apache.lucene.search.grouping.term.TermSecondPassGroupingCollector;
  20. import org.apache.lucene.store.Directory;
  21. import org.apache.lucene.store.RAMDirectory;
  22. import org.apache.lucene.util.BytesRef;
  23. import org.apache.lucene.util.Version;
  24.  
  25. import java.util.Collection;
  26.  
  27.  
  28. /**
  29.  * 演示faceting
  30.  *
  31.  * @author hankcs
  32.  */
  33. public class FacetingDemo
  34. {
  35.     public static void main(String[] args) throws Exception
  36.     {
  37.         // Lucene Document的主要域名
  38.         String mainFieldName = "text";
  39.         // Lucene版本
  40.         Version ver = Version.LUCENE_48;
  41.  
  42.         // 实例化Analyzer分词器
  43.         Analyzer analyzer = new StandardAnalyzer(ver);
  44.  
  45.         Directory directory;
  46.         IndexWriter writer;
  47.         IndexReader reader;
  48.         IndexSearcher searcher;
  49.         //索引过程**********************************
  50.         //建立内存索引对象
  51.         directory = new RAMDirectory();
  52.  
  53.         //配置IndexWriterConfig
  54.         IndexWriterConfig iwConfig = new IndexWriterConfig(ver, analyzer);
  55.         iwConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
  56.         writer = new IndexWriter(directory, iwConfig);
  57.         for (int i = 0; i < 100; ++i)
  58.         {
  59.             Document doc = new Document();
  60.             doc.add(new TextField(mainFieldName, "Banana is sweet " + i, Field.Store.YES));
  61.             doc.add(new TextField("catalog", "fruit", Field.Store.YES));
  62.             writer.addDocument(doc);
  63.         }
  64.         for (int i = 0; i < 50; ++i)
  65.         {
  66.             Document doc = new Document();
  67.             doc.add(new TextField(mainFieldName, "Juice is sweet " + i, Field.Store.YES));
  68.             doc.add(new TextField("catalog", "drink", Field.Store.YES));
  69.             writer.addDocument(doc);
  70.         }
  71.         for (int i = 0; i < 25; ++i)
  72.         {
  73.             Document doc = new Document();
  74.             doc.add(new TextField(mainFieldName, "Hankcs is here " + i, Field.Store.YES));
  75.             doc.add(new TextField("catalog", "person", Field.Store.YES));
  76.             writer.addDocument(doc);
  77.         }
  78.         writer.close();
  79.  
  80.         //搜索过程**********************************
  81.         //实例化搜索器
  82.         reader = DirectoryReader.open(directory);
  83.         searcher = new IndexSearcher(reader);
  84.  
  85.         String keyword = "sweet";
  86.         //使用QueryParser查询分析器构造Query对象
  87.         QueryParser qp = new QueryParser(ver, mainFieldName, analyzer);
  88.         Query query = qp.parse(keyword);
  89.         System.out.println("Query = " + query);
  90.  
  91.         //搜索相似度最高的5条记录并且分组
  92.         int topNGroups = 10; // 每页需要多少个组
  93.         int groupOffset = 0; // 起始的组
  94.         boolean fillFields = true;
  95.         Sort docSort = Sort.RELEVANCE; // groupSort用于对组进行排序,docSort用于对组内记录进行排序,多数情况下两者是相同的,但也可不同
  96.         Sort groupSort = docSort;
  97.         int docOffset = 0;   // 用于组内分页,起始的记录
  98.         int docsPerGroup = 2;// 每组返回多少条结果
  99.         boolean requiredTotalGroupCount = true; // 是否需要计算总的组的数量
  100.  
  101.         // 如果需要对Lucene的score进行修正,则需要重载TermFirstPassGroupingCollector
  102.         TermFirstPassGroupingCollector c1 = new TermFirstPassGroupingCollector("catalog", groupSort, topNGroups);
  103.         boolean cacheScores = true;
  104.         double maxCacheRAMMB = 16.0;
  105.         CachingCollector cachedCollector = CachingCollector.create(c1, cacheScores, maxCacheRAMMB);
  106.         searcher.search(query, cachedCollector);
  107.  
  108.         Collection<SearchGroup<BytesRef>> topGroups = c1.getTopGroups(groupOffset, fillFields);
  109.  
  110.         if (topGroups == null)
  111.         {
  112.             // No groups matched
  113.             return;
  114.         }
  115.  
  116.         Collector secondPassCollector = null;
  117.  
  118.         boolean getScores = true;
  119.         boolean getMaxScores = true;
  120.         // 如果需要对Lucene的score进行修正,则需要重载TermSecondPassGroupingCollector
  121.         TermSecondPassGroupingCollector c2 = new TermSecondPassGroupingCollector("catalog", topGroups, groupSort, docSort, docsPerGroup, getScores, getMaxScores, fillFields);
  122.  
  123.         // 是否需要计算一共有多少个分类,这一步是可选的
  124.         TermAllGroupsCollector allGroupsCollector = null;
  125.         if (requiredTotalGroupCount)
  126.         {
  127.             allGroupsCollector = new TermAllGroupsCollector("catalog");
  128.             secondPassCollector = MultiCollector.wrap(c2, allGroupsCollector);
  129.         }
  130.         else
  131.         {
  132.             secondPassCollector = c2;
  133.         }
  134.  
  135.         if (cachedCollector.isCached())
  136.         {
  137.             // 被缓存的话,就用缓存
  138.             cachedCollector.replay(secondPassCollector);
  139.         }
  140.         else
  141.         {
  142.             // 超出缓存大小,重新执行一次查询
  143.             searcher.search(query, secondPassCollector);
  144.         }
  145.  
  146.         int totalGroupCount = -1; // 所有组的数量
  147.         int totalHitCount = -1; // 所有满足条件的记录数
  148.         int totalGroupedHitCount = -1; // 所有组内的满足条件的记录数(通常该值与totalHitCount是一致的)
  149.         if (requiredTotalGroupCount)
  150.         {
  151.             totalGroupCount = allGroupsCollector.getGroupCount();
  152.         }
  153.         System.out.println("一共匹配到多少个分类: " + totalGroupCount);
  154.  
  155.         TopGroups<BytesRef> groupsResult = c2.getTopGroups(docOffset);
  156.         totalHitCount = groupsResult.totalHitCount;
  157.         totalGroupedHitCount = groupsResult.totalGroupedHitCount;
  158.         System.out.println("groupsResult.totalHitCount:" + totalHitCount);
  159.         System.out.println("groupsResult.totalGroupedHitCount:" + totalGroupedHitCount);
  160.  
  161.         int groupIdx = 0;
  162.         // 迭代组
  163.         for (GroupDocs<BytesRef> groupDocs : groupsResult.groups)
  164.         {
  165.             groupIdx++;
  166.             System.out.println("group[" + groupIdx + "]:" + groupDocs.groupValue); // 组的标识
  167.             System.out.println("group[" + groupIdx + "]:" + groupDocs.totalHits);  // 组内的记录数
  168.             int docIdx = 0;
  169.             // 迭代组内的记录
  170.             for (ScoreDoc scoreDoc : groupDocs.scoreDocs)
  171.             {
  172.                 docIdx++;
  173.                 System.out.println("group[" + groupIdx + "][" + docIdx + "]:" + scoreDoc.doc + "/" + scoreDoc.score);
  174.                 Document doc = searcher.doc(scoreDoc.doc);
  175.                 System.out.println("group[" + groupIdx + "][" + docIdx + "]:" + doc);
  176.             }
  177.         }
  178.     }
  179. }

输出


 
  1. Query = text:sweet
  2. 一共匹配到多少个分类: 2
  3. groupsResult.totalHitCount:150
  4. groupsResult.totalGroupedHitCount:150
  5. group[1]:[66 72 75 69 74]
  6. group[1]:100
  7. group[1][1]:0/0.573753
  8. group[1][1]:Document<stored,indexed,tokenized<text:Banana is sweet 0> stored,indexed,tokenized<catalog:fruit>>
  9. group[1][2]:1/0.573753
  10. group[1][2]:Document<stored,indexed,tokenized<text:Banana is sweet 1> stored,indexed,tokenized<catalog:fruit>>
  11. group[2]:[64 72 69 6e 6b]
  12. group[2]:50
  13. group[2][1]:100/0.573753
  14. group[2][1]:Document<stored,indexed,tokenized<text:Juice is sweet 0> stored,indexed,tokenized<catalog:drink>>
  15. group[2][2]:101/0.573753
  16. group[2][2]:Document<stored,indexed,tokenized<text:Juice is sweet 1> stored,indexed,tokenized<catalog:drink>>

 

你可能感兴趣的