对于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)个性化统计