lucene实现分组统计的方法

转: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是lucene用来排序的缓存。对要用来排序的字段,lucene会从索引中将每篇文档该字段的值都读出来,放到一个大小为maxDoc的数组中。maxDoc是lucene内部文档编号的最大值。有两点需要注意一下:
  • fieldCache中的字段值是从倒排表中读出来的,而不是从索引文件中存储的字段值,所以排序的字段必须是为设为索引字段
  • 用来排序的字段在索引的时候不能拆分(tokenized),因为fieldCache数组中,每个文档只对应一个字段值,拆分的话,cache中只会保存在词典中靠后的值。
fieldcache是lucene最占用的内存的部分,大部分内存溢出的错误都是由它而起,需要特别注意。

分组统计可以借用fieldCache来高效率的实现。调用lucene进行查询,通过读取倒排表并进行boolean运算,得到一个满足条件的文档的集合。通过每个结果文档号读取fieldCache数组中的值,并分不同的值累加数目,即可实现分组统计的功能。其中,如果某个字段对应多值,则在索引的时候不拆分,从filedCache数组读出后,再进行拆分统计。

对于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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值