转:http://www.cnblogs.com/huangfox/archive/2012/07/10/2584750.html
http://blog.163.com/liugangc@126/blog/static/20374821201011313238137/
1、工程目录
所谓分组统计,就是类似sql里group by的功能。在solr里,这个功能称为faceting。lucene本身不支持分组统计,不过可以使用fieldCache来实现分组统计功能,而且也有很好的性能。solr根据不同的情况,还提供了其他方法(filterCache和UnInvertedField)来实现,这个以后再说。
- fieldCache中的字段值是从倒排表中读出来的,而不是从索引文件中存储的字段值,所以排序的字段必须是为设为索引字段
- 用来排序的字段在索引的时候不能拆分(tokenized),因为fieldCache数组中,每个文档只对应一个字段值,拆分的话,cache中只会保存在词典中靠后的值。
对于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 org.itat.collector;
import java.io.IOException;
import java.util.Arrays;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.Collector;
import org.apache.lucene.search.FieldCache;
import org.apache.lucene.search.Scorer;
public class GroupCollectorDemo extends Collector {
private GF gf = new GF();// 保存分组统计结果
private String[] fc;// fieldCache
private String f;// 统计字段
String spliter;
int length;
@Override
public void setScorer(Scorer scorer) throws IOException {
}
@Override
public void setNextReader(IndexReader reader, int docBase)
throws IOException {
//读取f的字段值,放入FieldCache中
//在这里把所有文档的docid和它的f属性的值放入缓存中,以便获取
fc = FieldCache.DEFAULT.getStrings(reader, f);
System.out.println("fc:"+Arrays.toString(fc));
/**
* 先执行setNextReader方法再执行collect方法,
*
* 打印结果:
* fc:[5611, 5611, 5611, 5611, 5611, 5611, 5611, 5611, 5611, 5611]
*/
}
@Override
public void collect(int doc) throws IOException {
//因为doc是每个segment的文档编号,需要加上docBase才是总的文档编号
// 添加到GroupField中,由GroupField负责统计每个不同值的数目
System.out.println(doc+"##"+doc+"##");
gf.addValue(fc[doc]);
/**
* 打印结果:
* 0##5611
1##5611
2##5611
3##5611
5##5611
6##5611
9##5611
*/
}
@Override
public boolean acceptsDocsOutOfOrder() {
return true;
}
public void setFc(String[] fc) {
this.fc = fc;
}
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;
}
}
package org.itat.collector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GF {
// 所有可能的分组字段值,排序按每个字段值的文档个数大小排序
private List<String> values = new ArrayList<String>();
// 保存字段值和文档个数的对应关系
private Map<String, Integer> countMap = new HashMap<String, Integer>();
public List<String> getValues() {
Collections.sort(values, new ValueComparator());
return 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;
}
}
public void setValues(List<String> values) {
this.values = values;
}
public Map<String, Integer> getCountMap() {
return countMap;
}
public void setCountMap(Map<String, Integer> countMap) {
this.countMap = countMap;
}
}
package org.itat.collector;
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;
public class GroupTest {
public static void main(String[] f) throws IOException, ParseException {
FSDirectory dir = SimpleFSDirectory.open(new File("F:\\Workspaces\\collectortest\\index"));
IndexReader reader = IndexReader.open(dir);
IndexSearcher searcher = new IndexSearcher(reader);
// GroupCollector是自定义文档收集器,用于实现分组统计
String field = "content";//查询的字段
String queryStr = "程序";//查询的内容
QueryParser parser = new QueryParser(Version.LUCENE_35, field,new IKAnalyzer());
long bt = System.currentTimeMillis();
Query query = parser.parse(queryStr);
System.out.println(query);
GroupCollectorDemo myCollector = new GroupCollectorDemo();
//classid是用来分组的字段,在查询后的结果中得到该字段的值然后进行分组统计
//用来排序的字段在索引的时候不能拆分(tokenized),因为fieldCache数组中,
//每个文档只对应一个字段值,拆分的话,cache中只会保存在词典中靠后的值。
myCollector.setF("classid");
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 < values.size(); 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)个性化统计
工程地址: http://download.csdn.net/detail/wxwzy738/5310947