• [ lucene扩展 ] 自定义Collector实现统计功能


    对于lucene的统计,我基本放弃使用factedSearch了,效率不高,而且两套索引总觉得有点臃肿!

    这次我们通过改造Collector,实现简单的统计功能。经过测试,对几十万的统计还是比较快的。

    首先我们简单理解下Collector在search中的使用情况!

    Collector是一个接口,主要包括以下重要方法:

    public abstract class Collector {
      
    //指定打分器
      public abstract void setScorer(Scorer scorer) throws IOException;
      
    //对目标结果进行收集,很重要!
      public abstract void collect(int doc) throws IOException;
    
    //一个索引可能会有多个子索引,这里相当于是对子索引的遍历操作
      public abstract void setNextReader(IndexReader reader, int docBase) throws IOException;
    
    //
      public abstract boolean acceptsDocsOutOfOrder();
      
    }
    

    在search中我们来看看collector是怎么收集结果的!

    public void search(Weight weight, Filter filter, Collector collector)
    			throws IOException {
    
    		// TODO: should we make this
    		// threaded...? the Collector could be sync'd?
    
    		// always use single thread:
    		for (int i = 0; i < subReaders.length; i++) { // 检索每个子索引
    			collector.setNextReader(subReaders[i], docBase + docStarts[i]);
    			final Scorer scorer = (filter == null) ? weight.scorer(
    					subReaders[i], !collector.acceptsDocsOutOfOrder(), true)
    					: FilteredQuery.getFilteredScorer(subReaders[i],
    							getSimilarity(), weight, weight, filter);//构建打分器
    			if (scorer != null) {
    				scorer.score(collector);//打分
    			}
    		}
    	}
    

    scorer.score(collector)的过程如下:

    public void score(Collector collector) throws IOException {
        collector.setScorer(this);
        int doc;
        while ((doc = nextDoc()) != NO_MORE_DOCS) {
          collector.collect(doc);//搜集结果
        }
      }
    

    collector.collect(doc)的过程如下:

        @Override
        public void collect(int doc) throws IOException {
          float score = scorer.score();
    
          // This collector cannot handle these scores:
          assert score != Float.NEGATIVE_INFINITY;
          assert !Float.isNaN(score);
    
          totalHits++;
          if (score <= pqTop.score) {
            // 以下的实现使用了优先级队列,如果当前分值小于队列中pqTop.score则直接pass!
            return;
          }
          pqTop.doc = doc + docBase;
          pqTop.score = score;
          pqTop = pq.updateTop();
        }
    

    从上面这一坨坨代码我们可以大概看清collector在search中的应用情况。

    那么统计呢?

    首先我们来分析最简单的统计——“一维统计”,就只对一个字段的统计。例如统计图书每年的出版量、专利发明人发明专利数量的排行榜等。

    统计的输入:检索式、统计字段

    统计的输出:<统计项、数量>的集合

    其中关键是我们怎么拿到统计项。这个又分成以下一种情况:

    1)统计字段没有存储、不分词

    我们可以使用FieldCache.DEFAULT.getStrings(reader, f);获取统计项。

    2)统计字段没有存储、分词

    需要通过唯一标识从数据库(如果正向信息存在数据库的话)取出统计项(字段内容),然后统计分析。可想而知效率极低。

    3)统计字段存储、分词

    可以通过doc.get(fieldName)取出统计项,依然比较低效

    4)统计字段存储、不分词

    和1)类似

    因此我们如果要对某个字段进行统计,那么最好选用不分词(Index.NOT_ANALYZED),这个和排序字段的要求类似!

    拿到统计项后,我们可以通过累加然后排序。(这里可以借助map)

    下面给出主要代码:

    package com.fox.group;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.apache.lucene.index.IndexReader;
    import org.apache.lucene.search.Collector;
    import org.apache.lucene.search.FieldCache;
    import org.apache.lucene.search.Scorer;
    
    /**
     * @author huangfox
     * @data 2012-7-10
     * @email huangfox009@126.com
     * @desc 
     */
    public class GroupCollectorDemo extends Collector {
    
    	private GF gf = new GF();// 保存分组统计结果
    	private String[] fc;// fieldCache
    	private String f;// 统计字段
    	String spliter;
    	int length;
    
    	public void setFc(String[] fc) {
    		this.fc = fc;
    	}
    
    	@Override
    	public void setScorer(Scorer scorer) throws IOException {
    	}
    
    	@Override
    	public void setNextReader(IndexReader reader, int docBase)
    			throws IOException {
    		fc = FieldCache.DEFAULT.getStrings(reader, f);
    	}
    
    	@Override
    	public void collect(int doc) throws IOException {
    		// 添加的GroupField中,由GroupField负责统计每个不同值的数目
    		gf.addValue(fc[doc]);
    	}
    
    	@Override
    	public boolean acceptsDocsOutOfOrder() {
    		return true;
    	}
    
    	public GF getGroupField() {
    		return gf;
    	}
    
    	public void setSpliter(String spliter) {
    		this.spliter = spliter;
    	}
    
    	public void setLength(int length) {
    		this.length = length;
    	}
    
    	public void setF(String f) {
    		this.f = f;
    	}
    }
    
    class GF {
    	// 所有可能的分组字段值,排序按每个字段值的文档个数大小排序
    	private List<String> values = new ArrayList<String>();
    	// 保存字段值和文档个数的对应关系
    	private Map<String, Integer> countMap = new HashMap<String, Integer>();
    
    	public Map<String, Integer> getCountMap() {
    		return countMap;
    	}
    
    	public void setCountMap(Map<String, Integer> countMap) {
    		this.countMap = countMap;
    	}
    
    	public List<String> getValues() {
    		Collections.sort(values, new ValueComparator());
    		return values;
    	}
    
    	public void setValues(List<String> values) {
    		this.values = values;
    	}
    
    	public void addValue(String value) {
    		if (value == null || "".equals(value))
    			return;
    		if (countMap.get(value) == null) {
    			countMap.put(value, 1);
    			values.add(value);
    		} else {
    			countMap.put(value, countMap.get(value) + 1);
    		}
    	}
    
    	class ValueComparator implements Comparator<String> {
    		public int compare(String value0, String value1) {
    			if (countMap.get(value0) > countMap.get(value1)) {
    				return -1;
    			} else if (countMap.get(value0) < countMap.get(value1)) {
    				return 1;
    			}
    			return 0;
    		}
    	}
    }
    

    这里是对collector的collect方法的讨巧应用,search是对打分的排序,统计是构造一个结果收集器,提供排序功能。

    测试类:

    package com.fox.group;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.List;
    
    import org.apache.lucene.index.IndexReader;
    import org.apache.lucene.queryParser.ParseException;
    import org.apache.lucene.queryParser.QueryParser;
    import org.apache.lucene.search.IndexSearcher;
    import org.apache.lucene.search.Query;
    import org.apache.lucene.store.FSDirectory;
    import org.apache.lucene.store.SimpleFSDirectory;
    import org.apache.lucene.util.Version;
    import org.wltea.analyzer.lucene.IKAnalyzer;
    
    /**
     * @author huangfox
     * @data 2012-7-10
     * @email huangfox009@126.com
     * @desc 
     */
    public class GroupTest {
    	public static void main(String[] f) throws IOException, ParseException {
    		FSDirectory dir = SimpleFSDirectory.open(new File("d:/nrttest"));
    		IndexReader reader = IndexReader.open(dir);
    		IndexSearcher searcher = new IndexSearcher(reader);
    		// GroupCollector是自定义文档收集器,用于实现分组统计
    		String str = "";
    		QueryParser parser = new QueryParser(Version.LUCENE_36, "f",
    				new IKAnalyzer());
    		while (true) {
    			str = "an:cn*";
    			long bt = System.currentTimeMillis();
    			Query query = parser.parse(str);
    			System.out.println(query);
    			GroupCollectorDemo myCollector = new GroupCollectorDemo();
    			// myCollector.setFc(ad);
    			myCollector.setF("in");
    			searcher.search(query, myCollector);
    			// GroupField用来保存分组统计的结果
    			GF gf = myCollector.getGroupField();
    			List<String> values = gf.getValues();
    			long et = System.currentTimeMillis();
    			System.out.println((et - bt) + "ms");
    			for (int i = 0; i < 10; i++) {
    				String value = values.get(i);
    				System.out.println(value + "=" + gf.getCountMap().get(value));
    			}
    		}
    	}
    }
    

    以上是对200多万数据的统计,而且是全数据统计。测试结果如下:

    an:cn*
    6616ms
    毛裕民;谢毅=13728
    邱则有=10126
    杨孟君=3771
    王尔中=1712
    王信锁=1658
    张逶=1314
    朱炜=1200
    赵蕴岚;何唯平=1039
    杨贻方=872
    黄金富=871
    

    系统使用情况:

    你可能会说——这不是坑爹吗?要6s的时间消耗!!!

    解释:

    1.数据量,统计的数据量在200万;

    如果数据量在几十万,测试结果如下:

    ad:2006*
    213ms
    邱则有=1244
    张云波=628
    赵蕴岚;何唯平=398
    余内逊;余谦梁=376
    杨贻方=298
    王尔中=258
    汪铁良=224
    赵发=222
    黄振华=212
    陆舟;于华章=196
    

      

    2.运行在pc机上;

    以上解释也可以理解成借口,那么还有哪些环节可以优化呢?

    从cpu和io来看,cpu应该主要是由于hashMap的操作引起的,io主要是由FieldCache.DEFAULT.getStrings(reader, f)获取统计项引起的。

    如果高并发的情况下,io无疑是个大问题,我们可以考虑缓存。

    对于运算量大的情况,我们可以考虑分布式。

    后续我们将分析:

    1)二维统计、多维统计

    2)个性化统计

  • 相关阅读:
    pycharm调试时出现十分缓慢,变量数据没法预览的解决办法
    gitignore设置失效的问题
    存储过程计算两个时间段的请假天数
    Vue 多层级目录拖动排序
    vue 动态插入渲染html
    Vue:eliment-ui el-tree动态加载更新
    Angular2+ iframe跨域调用父页面js
    Angular2.0+动态绑定html文本
    Angular2.0+开发 -实现部门树形结构
    Angular2.0+开发(1)-WebStorm配置及第一个Hello World
  • 原文地址:https://www.cnblogs.com/huangfox/p/2584750.html
Copyright © 2020-2023  润新知