solr的facet源码解读(三)——facet.field之数字单值域类型

承接上一篇文章,在对单值域的数字类型的域做facet的时候,会使用FCS方法,里面再调用的方法是NumericFacets.getCounts(searcher, base, field, offset, limit, mincount, missing, sort);所以看看这个的代码吧:

/**
 * 处理单值域的数字类型的facet
 * @param searcher
 * @param docs 		基础范围(即有q和fq确定的所有的doc的id)
 * @param fieldName	要facet的域的名字
 * @param offset	最后返回的结果的偏移量
 * @param limit		最后返回的结果的数量,小于0表示全部返回!
 * @param mincount	能够被facet的term值得最小的doc的数量,如果一个term匹配的doc的数量小于这个值,则不计算这个term。如果这个值位0且其他的term不能够满足条件,则要收集匹配的doc数量为0的term,即没有在上面的docs中doc的term。
 * @param missing	要不要返回null的值。即上面的
 * @param sort		收集到的值得排序
 * @return  
 * @throws IOException
 */
public static NamedList<Integer> getCounts(SolrIndexSearcher searcher, DocSet docs, String fieldName, int offset, int limit, int mincount, boolean missing, String sort) throws IOException {
	final boolean zeros = mincount <= 0;//要不要收集没有doc的值(包括没有在docs中的那些doc匹配的term)
	mincount = Math.max(mincount, 1);   //这么做是有好处的,这样可以加快速度。因为可能不需要使用不命中的那些值,单单那些已经命中的doc的term就已经可以得到结果了。
	final SchemaField sf = searcher.getSchema().getField(fieldName);//
	final FieldType ft = sf.getType();
	final NumericType numericType = ft.getNumericType();
	if (numericType == null) {//只能facet数字类型的
		throw new IllegalStateException();
	}
	final List<AtomicReaderContext> leaves = searcher.getIndexReader().leaves();
	
	// 1. 先把已经搜索到的docs的对应的值都收集了,收集到hashTable中。单独创建了这么一个类,用于保存term和匹配的doc的数量。
	final HashTable hashTable = new HashTable();
	final Iterator<AtomicReaderContext> ctxIt = leaves.iterator();
	AtomicReaderContext ctx = null;
	FieldCache.Longs longs = null;
	Bits docsWithField = null;
	int missingCount = 0;//
	for (DocIterator docsIt = docs.iterator(); docsIt.hasNext();) {//
		final int doc = docsIt.nextDoc();
		if (ctx == null || doc >= ctx.docBase + ctx.reader().maxDoc()) {//找到这个doc所在的段
			do {
				ctx = ctxIt.next();
			} while (ctx == null || doc >= ctx.docBase + ctx.reader().maxDoc());
			//从fieldCache中获得这个doc的值,从之前的博客中可以知道,fieldCache也是优先获取docValue的值的,所以说这个收集方式就是优先使用docValue的值。
			switch (numericType) {
			case LONG:	longs = FieldCache.DEFAULT.getLongs(ctx.reader(), fieldName, true);	break;
			case INT:
				final FieldCache.Ints ints = FieldCache.DEFAULT.getInts(ctx.reader(), fieldName, true);
				longs = new FieldCache.Longs() {
					@Override
					public long get(int docID) {		return ints.get(docID);
					}
				};
				break;
			case FLOAT:
				final FieldCache.Floats floats = FieldCache.DEFAULT.getFloats(ctx.reader(), fieldName, true);
				longs = new FieldCache.Longs() {
					@Override
					public long get(int docID) {
						return NumericUtils.floatToSortableInt(floats.get(docID));
					}
				};
				break;
			case DOUBLE:
				final FieldCache.Doubles doubles = FieldCache.DEFAULT.getDoubles(ctx.reader(), fieldName, true);
				longs = new FieldCache.Longs() {
					@Override
					public long get(int docID) {
						return NumericUtils.doubleToSortableLong(doubles.get(docID));
					}
				};
				break;
			default:
				throw new AssertionError();
			}
			docsWithField = FieldCache.DEFAULT.getDocsWithField(ctx.reader(), fieldName);//含有这个域的doc的bit
		}
		long v = longs.get(doc - ctx.docBase);//获得这个id的值
		if (v != 0 || docsWithField.get(doc - ctx.docBase)) {//如果v != 0说明是一定有值得,但是==0的话可能也有值的,所以要判断两次。
			hashTable.add(doc, v, 1);//收集到了,加入到hash表里面。
		} else {
			++missingCount;//没有值,也就是null的数量,如果需要返回missing的话这个就有用了。
		}
	}
	// 2. 从hash表中根据规则 选择offset+limit个。
	final int pqSize = limit < 0 ? hashTable.size : Math.min(offset + limit, hashTable.size);,如果limit小于0 则全部的term都要返回,否则返回offset+ limit个。
	final PriorityQueue<Entry> pq;//根据排序创建一个优先队列
	if (FacetParams.FACET_SORT_COUNT.equals(sort) || FacetParams.FACET_SORT_COUNT_LEGACY.equals(sort)) {//如果排序是按照term匹配的doc数量排序
		pq = new PriorityQueue<Entry>(pqSize) {
			@Override
			protected boolean lessThan(Entry a, Entry b) {
				if (a.count < b.count || (a.count == b.count && a.bits > b.bits)) {//现根据count排序,如果count一样,按照数字排序
					return true;
				} else {
					return false;
				}
			}
		};
	} else {
		pq = new PriorityQueue<Entry>(pqSize) {
			@Override
			protected boolean lessThan(Entry a, Entry b) {//按照facet到的数字的大小排序
				return a.bits > b.bits;
			}
		};
	}
	Entry e = null;
	for (int i = 0; i < hashTable.bits.length; ++i) {//循环已经收集的term,这些的doc都是大于0的,因为他们的获取方式就是从已经搜索到的doc中获取的。
		if (hashTable.counts[i] >= mincount) {//如果大于指定的值,hashTable.counts[i]的这个值最小是1,所以如果这些的term已经够数量了,就不去查询词典表了,所以前面才将其置位最小是1的数字,当然如果指定了>1的数字,就使用那个数字
			if (e == null) {
				e = new Entry();
			}
			e.bits = hashTable.bits[i];
			e.count = hashTable.counts[i];
			e.docID = hashTable.docIDs[i];
			e = pq.insertWithOverflow(e);
		}
	}
	
	// 4. build the NamedList  构建最后的结果
	final ValueSource vs = ft.getValueSource(sf, null);//使用valueSource查询具体的值,因为之前查询的都是long类型的值,而我们要返回的是字符串,这次就是要查询字符串。
	final NamedList<Integer> result = new NamedList<>();
	
//	如果上面的term的数量不够,体现在两个方面,一个是排序,即收集的term的排序是按照term的字面值排序的,或者是minCount=0,表示要获得所有的term, 则要查询词典表,这就复杂了!
	// This stuff is complicated because if facet.mincount=0, the counts needs to be merged with terms from the terms dict(翻译过来是:如果mincount=0,则要读取词典表获得所有的term,因为现在仅仅是收集了一部分doc的term)
	// 或者不计算不命中的doc的term值或者是按照count排序的,就不需要查词典表了。
	if (!zeros || FacetParams.FACET_SORT_COUNT.equals(sort) || FacetParams.FACET_SORT_COUNT_LEGACY.equals(sort)) {
		final Deque<Entry> counts = new ArrayDeque<>();//保存offset后面的那些值
		while (pq.size() > offset) {//删除offset个到counts中去
			counts.addFirst(pq.pop());
		}
		// Entries from the PQ first, then using the terms dictionary
		for (Entry entry : counts) {
			final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves);
			final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx));//valueSource读取真正的值,使用FieldCache
			result.add(values.strVal(entry.docID - leaves.get(readerIdx).docBase), entry.count);//放入结果
		}

		//如果计算那些不命中的且单单使用docSet不够数量,则要查看词典表,即检查所有的term
		if (zeros && (limit < 0 || result.size() < limit)) { // need to merge with the term dict
			if (!sf.indexed()) {//此时必须要简历索引,不然没法查词典表了
				throw new IllegalStateException("Cannot use " + FacetParams.FACET_MINCOUNT + "=0 on field " + sf.getName() + " which is not indexed");
			}
			// Add zeros until there are limit results
			final Set<String> alreadySeen = new HashSet<>();
			//将使用docSet已经查找到的所有的值放入set集合里面,放置重复了
			while (pq.size() > 0) {//第一步是放入offset的那些
				Entry entry = pq.pop();
				final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves);
				final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx));
				alreadySeen.add(values.strVal(entry.docID - leaves.get(readerIdx).docBase));
			}
			//第二部是放入已经放入到result里面的那些
			for (int i = 0; i < result.size(); ++i) {
				alreadySeen.add(result.getName(i));
			}
			
			//获得这个域的所有的term
			final Terms terms = searcher.getAtomicReader().terms(fieldName);
			if (terms != null) {
				
				final String prefixStr = TrieField.getMainValuePrefix(ft);//这个域的前缀
				final BytesRef prefix;
				if (prefixStr != null) {
					prefix = new BytesRef(prefixStr);
				} else {
					prefix = new BytesRef();
				}
				
				final TermsEnum termsEnum = terms.iterator(null);
				BytesRef term;
				switch (termsEnum.seekCeil(prefix)) {
				case FOUND:
				case NOT_FOUND:
					term = termsEnum.term();
					break;
				case END:
					term = null;
					break;
				default:
					throw new AssertionError();
				}
				
				final CharsRef spare = new CharsRef();

				
				//继续跳过offset-hashtable.size,因为这一部分不要。
				for (int skipped = hashTable.size; skipped < offset && term != null	&& StringHelper.startsWith(term, prefix);) {
					ft.indexedToReadable(term, spare);
					final String termStr = spare.toString();
					if (!alreadySeen.contains(termStr)) {
						++skipped;
					}
					term = termsEnum.next();
				}
				
				
				
				//读取limit-result.size个term
				for (; term != null && StringHelper.startsWith(term, prefix) && (limit < 0 || result.size() < limit); term = termsEnum.next()) {
					ft.indexedToReadable(term, spare);
					final String termStr = spare.toString();
					if (!alreadySeen.contains(termStr)) {//如果从来没有出现过!
						result.add(termStr, 0);//添加到结果中
					}
				}	
			}
		}
	} else {//收集docset中没有的且按照字面值排序,读取词典表
		// sort=index, mincount=0 and we have less than limit items => Merge the PQ and the terms dictionary on the fly
		if (!sf.indexed()) {
			throw new IllegalStateException("Cannot use " + FacetParams.FACET_SORT + "=" + FacetParams.FACET_SORT_INDEX + " on a field which is not indexed");
		}
		//key是facet的数字的字面值,value是次数
		final Map<String, Integer> counts = new HashMap<>();
		while (pq.size() > 0) {//从优先队列里面取出来,再放入到counts里面,放入的key是字面值,value是在docSet中facet到的次数
			final Entry entry = pq.pop();
			final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves);
			final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx));
			counts.put(values.strVal(entry.docID - leaves.get(readerIdx).docBase), entry.count);
		}
		final Terms terms = searcher.getAtomicReader().terms(fieldName);
		if (terms != null) {
			final String prefixStr = TrieField.getMainValuePrefix(ft);
			final BytesRef prefix;
			if (prefixStr != null) {
				prefix = new BytesRef(prefixStr);
			} else {
				prefix = new BytesRef();
			}
			final TermsEnum termsEnum = terms.iterator(null);
			BytesRef term;
			switch (termsEnum.seekCeil(prefix)) {
			case FOUND:
			case NOT_FOUND:
				term = termsEnum.term();
				break;
			case END:
				term = null;
				break;
			default:
				throw new AssertionError();
			}
			final CharsRef spare = new CharsRef();
			for (int i = 0; i < offset && term != null && StringHelper.startsWith(term, prefix); ++i) {//
				term = termsEnum.next();
			}
			for (; term != null && StringHelper.startsWith(term, prefix)
					&& (limit < 0 || result.size() < limit); term = termsEnum.next()) {
				ft.indexedToReadable(term, spare);
				final String termStr = spare.toString();
				Integer count = counts.get(termStr);
				if (count == null) {
					count = 0;
				}
				result.add(termStr, count);
			}
		}
	}

	if (missing) {//添加null的值得数量
		result.add(null, missingCount);
	}
	return result;
}

 从上面可以总结出经验来,在对单值域的数字类型的域做facet的时候,最好是设置上mincount>0,且按照doc的数量排序,在这个时候仅仅是使用命中的所有的doc的term做聚合,数量较少,不会有其他的操作; 否则会读取词典表,导致效率低下。还需要注意的是,上面的所有的操作都是在一个线程中完成的,之前说的多线程是在多个facet.field的情况下才会使用的。

猜你喜欢

转载自suichangkele.iteye.com/blog/2411063