solr的facet源码解读(三)——facet.field之数字单值域类型 博客分类: solrlucene lucenesolrfacet数字类型
程序员文章站
2024-03-18 19:42:10
...
(这个使用的solr的版本是4.10)
承接上一篇文章,在对单值域的数字类型的域做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的情况下才会使用的。