| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.handler.component; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import com.carrotsearch.hppc.IntHashSet; |
| import com.carrotsearch.hppc.IntObjectHashMap; |
| import com.carrotsearch.hppc.LongHashSet; |
| import com.carrotsearch.hppc.LongObjectHashMap; |
| import com.carrotsearch.hppc.LongObjectMap; |
| import com.carrotsearch.hppc.cursors.IntObjectCursor; |
| import com.carrotsearch.hppc.cursors.LongCursor; |
| import com.carrotsearch.hppc.cursors.LongObjectCursor; |
| import com.carrotsearch.hppc.cursors.ObjectCursor; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.lucene.index.DocValues; |
| import org.apache.lucene.index.LeafReader; |
| import org.apache.lucene.index.LeafReaderContext; |
| import org.apache.lucene.index.MultiDocValues; |
| import org.apache.lucene.index.NumericDocValues; |
| import org.apache.lucene.index.OrdinalMap; |
| import org.apache.lucene.index.SortedDocValues; |
| import org.apache.lucene.search.Collector; |
| import org.apache.lucene.search.DocIdSetIterator; |
| import org.apache.lucene.search.LeafCollector; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.Scorable; |
| import org.apache.lucene.search.ScoreDoc; |
| import org.apache.lucene.search.ScoreMode; |
| import org.apache.lucene.search.Sort; |
| import org.apache.lucene.search.TermInSetQuery; |
| import org.apache.lucene.search.TopDocs; |
| import org.apache.lucene.search.TopDocsCollector; |
| import org.apache.lucene.search.TopFieldCollector; |
| import org.apache.lucene.search.TopScoreDocCollector; |
| import org.apache.lucene.search.TotalHitCountCollector; |
| import org.apache.lucene.search.TotalHits; |
| import org.apache.lucene.util.BitSetIterator; |
| import org.apache.lucene.util.BytesRef; |
| import org.apache.lucene.util.BytesRefBuilder; |
| import org.apache.lucene.util.CharsRefBuilder; |
| import org.apache.lucene.util.FixedBitSet; |
| import org.apache.lucene.util.LongValues; |
| import org.apache.solr.common.SolrDocumentList; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.ExpandParams; |
| import org.apache.solr.common.params.GroupParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.SimpleOrderedMap; |
| import org.apache.solr.core.PluginInfo; |
| import org.apache.solr.core.SolrCore; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.schema.FieldType; |
| import org.apache.solr.schema.NumberType; |
| import org.apache.solr.schema.SchemaField; |
| import org.apache.solr.schema.StrField; |
| import org.apache.solr.search.CollapsingQParserPlugin; |
| import org.apache.solr.search.DocIterator; |
| import org.apache.solr.search.DocList; |
| import org.apache.solr.search.DocSlice; |
| import org.apache.solr.search.QParser; |
| import org.apache.solr.search.QueryUtils; |
| import org.apache.solr.search.ReturnFields; |
| import org.apache.solr.search.SolrIndexSearcher; |
| import org.apache.solr.search.SortSpecParsing; |
| import org.apache.solr.search.SyntaxError; |
| import org.apache.solr.util.plugin.PluginInfoInitialized; |
| import org.apache.solr.util.plugin.SolrCoreAware; |
| |
| /** |
| * The ExpandComponent is designed to work with the CollapsingPostFilter. |
| * The CollapsingPostFilter collapses a result set on a field. |
| * <p> |
| * The ExpandComponent expands the collapsed groups for a single page. |
| * When multiple collapse groups are specified then, the field is chosen from collapse group with min cost. |
| * If the cost are equal then, the field is chosen from first collapse group. |
| * <p> |
| * http parameters: |
| * <p> |
| * expand=true <br> |
| * expand.rows=5 <br> |
| * expand.sort=field asc|desc<br> |
| * expand.q=*:* (optional, overrides the main query)<br> |
| * expand.fq=type:child (optional, overrides the main filter queries)<br> |
| * expand.field=field (mandatory, if the not used with the CollapsingQParserPlugin. This is given higher priority when both are present)<br> |
| */ |
| public class ExpandComponent extends SearchComponent implements PluginInfoInitialized, SolrCoreAware { |
| public static final String COMPONENT_NAME = "expand"; |
| private static final int finishingStage = ResponseBuilder.STAGE_GET_FIELDS; |
| private PluginInfo info = PluginInfo.EMPTY_INFO; |
| |
| @Override |
| public void init(PluginInfo info) { |
| this.info = info; |
| } |
| |
| @Override |
| public void prepare(ResponseBuilder rb) throws IOException { |
| if (rb.req.getParams().getBool(ExpandParams.EXPAND, false)) { |
| if (rb.req.getParams().getBool(GroupParams.GROUP, false)) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can not use expand with Grouping enabled"); |
| } |
| rb.doExpand = true; |
| } |
| } |
| |
| @Override |
| public void inform(SolrCore core) { |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public void process(ResponseBuilder rb) throws IOException { |
| |
| if (!rb.doExpand) { |
| return; |
| } |
| |
| SolrQueryRequest req = rb.req; |
| SolrParams params = req.getParams(); |
| |
| String field = params.get(ExpandParams.EXPAND_FIELD); |
| String hint = null; |
| if (field == null) { |
| List<Query> filters = rb.getFilters(); |
| if (filters != null) { |
| int cost = Integer.MAX_VALUE; |
| for (Query q : filters) { |
| if (q instanceof CollapsingQParserPlugin.CollapsingPostFilter) { |
| CollapsingQParserPlugin.CollapsingPostFilter cp = (CollapsingQParserPlugin.CollapsingPostFilter) q; |
| // if there are multiple collapse pick the low cost one |
| // if cost are equal then first one is picked |
| if (cp.getCost() < cost) { |
| cost = cp.getCost(); |
| field = cp.getField(); |
| hint = cp.hint; |
| } |
| } |
| } |
| } |
| } |
| |
| if (field == null) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "missing expand field"); |
| } |
| |
| String sortParam = params.get(ExpandParams.EXPAND_SORT); |
| String[] fqs = params.getParams(ExpandParams.EXPAND_FQ); |
| String qs = params.get(ExpandParams.EXPAND_Q); |
| int limit = params.getInt(ExpandParams.EXPAND_ROWS, 5); |
| |
| Sort sort = null; |
| |
| if (sortParam != null) { |
| sort = SortSpecParsing.parseSortSpec(sortParam, rb.req).getSort(); |
| } |
| |
| final Query query; |
| List<Query> newFilters = new ArrayList<>(); |
| try { |
| if (qs == null) { |
| query = rb.getQuery(); |
| } else { |
| QParser parser = QParser.getParser(qs, req); |
| query = parser.getQuery(); |
| } |
| |
| if (fqs == null) { |
| List<Query> filters = rb.getFilters(); |
| if (filters != null) { |
| for (Query q : filters) { |
| if (!(q instanceof CollapsingQParserPlugin.CollapsingPostFilter)) { |
| newFilters.add(q); |
| } |
| } |
| } |
| } else { |
| for (String fq : fqs) { |
| if (StringUtils.isNotBlank(fq) && !fq.equals("*:*")) { |
| QParser fqp = QParser.getParser(fq, req); |
| newFilters.add(fqp.getQuery()); |
| } |
| } |
| } |
| } catch (SyntaxError e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); |
| } |
| |
| SolrIndexSearcher searcher = req.getSearcher(); |
| LeafReader reader = searcher.getSlowAtomicReader(); |
| |
| SchemaField schemaField = searcher.getSchema().getField(field); |
| FieldType fieldType = schemaField.getType(); |
| |
| SortedDocValues values = null; |
| long nullValue = 0L; |
| |
| if(fieldType instanceof StrField) { |
| //Get The Top Level SortedDocValues |
| if(CollapsingQParserPlugin.HINT_TOP_FC.equals(hint)) { |
| @SuppressWarnings("resource") |
| LeafReader uninvertingReader = CollapsingQParserPlugin.getTopFieldCacheReader(searcher, field); |
| values = uninvertingReader.getSortedDocValues(field); |
| } else { |
| values = DocValues.getSorted(reader, field); |
| } |
| } else if (fieldType.getNumberType() != null) { |
| //Get the nullValue for the numeric collapse field |
| String defaultValue = searcher.getSchema().getField(field).getDefaultValue(); |
| |
| final NumberType numType = fieldType.getNumberType(); |
| |
| // Since the expand component depends on the operation of the collapse component, |
| // which validates that numeric field types are 32-bit, |
| // we don't need to handle invalid 64-bit field types here. |
| // FIXME: what happens when expand.field specified? |
| // how would this work for date field? |
| // SOLR-10400: before this, long and double were explicitly handled |
| if (defaultValue != null) { |
| if (numType == NumberType.INTEGER) { |
| nullValue = Long.parseLong(defaultValue); |
| } else if (numType == NumberType.FLOAT) { |
| nullValue = Float.floatToIntBits(Float.parseFloat(defaultValue)); |
| } |
| } else if (NumberType.FLOAT.equals(numType)) { // Integer case already handled by nullValue defaulting to 0 |
| nullValue = Float.floatToIntBits(0.0f); |
| } |
| } else { |
| // possible if directly expand.field is specified |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, |
| "Expand not supported for fieldType:'" + fieldType.getTypeName() +"'"); |
| } |
| |
| FixedBitSet groupBits = null; |
| LongHashSet groupSet = null; |
| DocList docList = rb.getResults().docList; |
| IntHashSet collapsedSet = new IntHashSet(docList.size() * 2); |
| |
| //Gather the groups for the current page of documents |
| DocIterator idit = docList.iterator(); |
| int[] globalDocs = new int[docList.size()]; |
| int docsIndex = -1; |
| while (idit.hasNext()) { |
| globalDocs[++docsIndex] = idit.nextDoc(); |
| } |
| |
| Arrays.sort(globalDocs); |
| Query groupQuery = null; |
| |
| /* |
| * This code gathers the group information for the current page. |
| */ |
| List<LeafReaderContext> contexts = searcher.getTopReaderContext().leaves(); |
| |
| if(contexts.size() == 0) { |
| //When no context is available we can skip the expanding |
| return; |
| } |
| |
| int currentContext = 0; |
| int currentDocBase = contexts.get(currentContext).docBase; |
| int nextDocBase = (currentContext+1)<contexts.size() ? contexts.get(currentContext+1).docBase : Integer.MAX_VALUE; |
| IntObjectHashMap<BytesRef> ordBytes = null; |
| if(values != null) { |
| groupBits = new FixedBitSet(values.getValueCount()); |
| OrdinalMap ordinalMap = null; |
| SortedDocValues[] sortedDocValues = null; |
| LongValues segmentOrdinalMap = null; |
| SortedDocValues currentValues = null; |
| if(values instanceof MultiDocValues.MultiSortedDocValues) { |
| ordinalMap = ((MultiDocValues.MultiSortedDocValues)values).mapping; |
| sortedDocValues = ((MultiDocValues.MultiSortedDocValues)values).values; |
| currentValues = sortedDocValues[currentContext]; |
| segmentOrdinalMap = ordinalMap.getGlobalOrds(currentContext); |
| } |
| |
| ordBytes = new IntObjectHashMap<>(); |
| |
| for(int i=0; i<globalDocs.length; i++) { |
| int globalDoc = globalDocs[i]; |
| while(globalDoc >= nextDocBase) { |
| currentContext++; |
| currentDocBase = contexts.get(currentContext).docBase; |
| nextDocBase = (currentContext+1) < contexts.size() ? contexts.get(currentContext+1).docBase : Integer.MAX_VALUE; |
| if(ordinalMap != null) { |
| currentValues = sortedDocValues[currentContext]; |
| segmentOrdinalMap = ordinalMap.getGlobalOrds(currentContext); |
| } |
| } |
| |
| int contextDoc = globalDoc - currentDocBase; |
| if(ordinalMap != null) { |
| if (contextDoc > currentValues.docID()) { |
| currentValues.advance(contextDoc); |
| } |
| if (contextDoc == currentValues.docID()) { |
| int contextOrd = currentValues.ordValue(); |
| int ord = (int)segmentOrdinalMap.get(contextOrd); |
| if (!groupBits.getAndSet(ord)) { |
| BytesRef ref = currentValues.lookupOrd(contextOrd); |
| ordBytes.put(ord, BytesRef.deepCopyOf(ref)); |
| } |
| collapsedSet.add(globalDoc); |
| } |
| } else { |
| if (globalDoc > values.docID()) { |
| values.advance(globalDoc); |
| } |
| if (globalDoc == values.docID()) { |
| int ord = values.ordValue(); |
| if (!groupBits.getAndSet(ord)) { |
| BytesRef ref = values.lookupOrd(ord); |
| ordBytes.put(ord, BytesRef.deepCopyOf(ref)); |
| } |
| collapsedSet.add(globalDoc); |
| } |
| } |
| } |
| |
| int count = ordBytes.size(); |
| if(count > 0 && count < 200) { |
| groupQuery = getGroupQuery(field, count, ordBytes); |
| } |
| } else { |
| groupSet = new LongHashSet(docList.size()); |
| NumericDocValues collapseValues = contexts.get(currentContext).reader().getNumericDocValues(field); |
| for(int i=0; i<globalDocs.length; i++) { |
| int globalDoc = globalDocs[i]; |
| while(globalDoc >= nextDocBase) { |
| currentContext++; |
| currentDocBase = contexts.get(currentContext).docBase; |
| nextDocBase = currentContext+1 < contexts.size() ? contexts.get(currentContext+1).docBase : Integer.MAX_VALUE; |
| collapseValues = contexts.get(currentContext).reader().getNumericDocValues(field); |
| } |
| int contextDoc = globalDoc - currentDocBase; |
| int valueDocID = collapseValues.docID(); |
| if (valueDocID < contextDoc) { |
| valueDocID = collapseValues.advance(contextDoc); |
| } |
| long value; |
| if (valueDocID == contextDoc) { |
| value = collapseValues.longValue(); |
| } else { |
| value = 0; |
| } |
| if(value != nullValue) { |
| groupSet.add(value); |
| collapsedSet.add(globalDoc); |
| } |
| } |
| |
| int count = groupSet.size(); |
| if(count > 0 && count < 200) { |
| if (fieldType.isPointField()) { |
| groupQuery = getPointGroupQuery(schemaField, count, groupSet); |
| } else { |
| groupQuery = getGroupQuery(field, fieldType, count, groupSet); |
| } |
| } |
| } |
| |
| Collector collector; |
| if (sort != null) |
| sort = sort.rewrite(searcher); |
| |
| |
| Collector groupExpandCollector = null; |
| |
| if(values != null) { |
| //Get The Top Level SortedDocValues again so we can re-iterate: |
| if(CollapsingQParserPlugin.HINT_TOP_FC.equals(hint)) { |
| @SuppressWarnings("resource") |
| LeafReader uninvertingReader = CollapsingQParserPlugin.getTopFieldCacheReader(searcher, field); |
| values = uninvertingReader.getSortedDocValues(field); |
| } else { |
| values = DocValues.getSorted(reader, field); |
| } |
| |
| groupExpandCollector = new GroupExpandCollector(values, groupBits, collapsedSet, limit, sort); |
| } else { |
| groupExpandCollector = new NumericGroupExpandCollector(field, nullValue, groupSet, collapsedSet, limit, sort); |
| } |
| |
| if(groupQuery != null) { |
| //Limits the results to documents that are in the same group as the documents in the page. |
| newFilters.add(groupQuery); |
| } |
| |
| SolrIndexSearcher.ProcessedFilter pfilter = searcher.getProcessedFilter(null, newFilters); |
| if (pfilter.postFilter != null) { |
| pfilter.postFilter.setLastDelegate(groupExpandCollector); |
| collector = pfilter.postFilter; |
| } else { |
| collector = groupExpandCollector; |
| } |
| |
| searcher.search(QueryUtils.combineQueryAndFilter(query, pfilter.filter), collector); |
| |
| ReturnFields returnFields = rb.rsp.getReturnFields(); |
| LongObjectMap<Collector> groups = ((GroupCollector) groupExpandCollector).getGroups(); |
| |
| @SuppressWarnings({"rawtypes"}) |
| NamedList outMap = new SimpleOrderedMap(); |
| CharsRefBuilder charsRef = new CharsRefBuilder(); |
| for (LongObjectCursor<Collector> cursor : groups) { |
| long groupValue = cursor.key; |
| if (cursor.value instanceof TopDocsCollector) { |
| TopDocsCollector<?> topDocsCollector = TopDocsCollector.class.cast(cursor.value); |
| TopDocs topDocs = topDocsCollector.topDocs(); |
| ScoreDoc[] scoreDocs = topDocs.scoreDocs; |
| if (scoreDocs.length > 0) { |
| if (returnFields.wantsScore() && sort != null) { |
| TopFieldCollector.populateScores(scoreDocs, searcher, query); |
| } |
| int[] docs = new int[scoreDocs.length]; |
| float[] scores = new float[scoreDocs.length]; |
| for (int i = 0; i < docs.length; i++) { |
| ScoreDoc scoreDoc = scoreDocs[i]; |
| docs[i] = scoreDoc.doc; |
| scores[i] = scoreDoc.score; |
| } |
| assert topDocs.totalHits.relation == TotalHits.Relation.EQUAL_TO; |
| DocSlice slice = new DocSlice(0, docs.length, docs, scores, topDocs.totalHits.value, Float.NaN, TotalHits.Relation.EQUAL_TO); |
| addGroupSliceToOutputMap(fieldType, ordBytes, outMap, charsRef, groupValue, slice); |
| } |
| } else { |
| int totalHits = ((TotalHitCountCollector) cursor.value).getTotalHits(); |
| if (totalHits > 0) { |
| DocSlice slice = new DocSlice(0, 0, null, null, totalHits, 0, TotalHits.Relation.EQUAL_TO); |
| addGroupSliceToOutputMap(fieldType, ordBytes, outMap, charsRef, groupValue, slice); |
| } |
| } |
| } |
| |
| rb.rsp.add("expanded", outMap); |
| } |
| |
| |
| @SuppressWarnings({"unchecked"}) |
| private void addGroupSliceToOutputMap(FieldType fieldType, IntObjectHashMap<BytesRef> ordBytes, |
| @SuppressWarnings({"rawtypes"})NamedList outMap, CharsRefBuilder charsRef, long groupValue, DocSlice slice) { |
| if(fieldType instanceof StrField) { |
| final BytesRef bytesRef = ordBytes.get((int)groupValue); |
| fieldType.indexedToReadable(bytesRef, charsRef); |
| String group = charsRef.toString(); |
| outMap.add(group, slice); |
| } else { |
| outMap.add(numericToString(fieldType, groupValue), slice); |
| } |
| } |
| |
| @Override |
| public int distributedProcess(ResponseBuilder rb) throws IOException { |
| if (rb.doExpand && rb.stage < finishingStage) { |
| return finishingStage; |
| } |
| return ResponseBuilder.STAGE_DONE; |
| } |
| |
| @Override |
| public void modifyRequest(ResponseBuilder rb, SearchComponent who, ShardRequest sreq) { |
| SolrParams params = rb.req.getParams(); |
| if (!params.getBool(COMPONENT_NAME, false)) return; |
| if (!rb.onePassDistributedQuery && (sreq.purpose & ShardRequest.PURPOSE_GET_FIELDS) == 0) { |
| sreq.params.set(COMPONENT_NAME, "false"); |
| } else { |
| sreq.params.set(COMPONENT_NAME, "true"); |
| } |
| } |
| |
| |
| @Override |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| public void handleResponses(ResponseBuilder rb, ShardRequest sreq) { |
| |
| if (!rb.doExpand) { |
| return; |
| } |
| if ((sreq.purpose & ShardRequest.PURPOSE_GET_FIELDS) != 0) { |
| SolrQueryRequest req = rb.req; |
| NamedList expanded = (NamedList) req.getContext().get("expanded"); |
| if (expanded == null) { |
| expanded = new SimpleOrderedMap(); |
| req.getContext().put("expanded", expanded); |
| } |
| |
| for (ShardResponse srsp : sreq.responses) { |
| NamedList response = srsp.getSolrResponse().getResponse(); |
| NamedList ex = (NamedList) response.get("expanded"); |
| for (int i=0; i<ex.size(); i++) { |
| String name = ex.getName(i); |
| SolrDocumentList val = (SolrDocumentList) ex.getVal(i); |
| expanded.add(name, val); |
| } |
| } |
| } |
| } |
| |
| @SuppressWarnings("rawtypes") |
| @Override |
| public void finishStage(ResponseBuilder rb) { |
| |
| if (!rb.doExpand) { |
| return; |
| } |
| |
| if (rb.stage != finishingStage) { |
| return; |
| } |
| |
| NamedList expanded = (NamedList) rb.req.getContext().get("expanded"); |
| if (expanded == null) { |
| expanded = new SimpleOrderedMap(); |
| } |
| |
| rb.rsp.add("expanded", expanded); |
| } |
| |
| private static class GroupExpandCollector implements Collector, GroupCollector { |
| private SortedDocValues docValues; |
| private OrdinalMap ordinalMap; |
| private SortedDocValues segmentValues; |
| private LongValues segmentOrdinalMap; |
| private MultiDocValues.MultiSortedDocValues multiSortedDocValues; |
| |
| private LongObjectMap<Collector> groups; |
| private FixedBitSet groupBits; |
| private IntHashSet collapsedSet; |
| |
| public GroupExpandCollector(SortedDocValues docValues, FixedBitSet groupBits, IntHashSet collapsedSet, int limit, Sort sort) throws IOException { |
| int numGroups = collapsedSet.size(); |
| groups = new LongObjectHashMap<>(numGroups); |
| DocIdSetIterator iterator = new BitSetIterator(groupBits, 0); // cost is not useful here |
| int group; |
| while ((group = iterator.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) { |
| groups.put(group, getCollector(limit, sort)); |
| } |
| |
| this.collapsedSet = collapsedSet; |
| this.groupBits = groupBits; |
| this.docValues = docValues; |
| if(docValues instanceof MultiDocValues.MultiSortedDocValues) { |
| this.multiSortedDocValues = (MultiDocValues.MultiSortedDocValues)docValues; |
| this.ordinalMap = multiSortedDocValues.mapping; |
| } |
| } |
| |
| public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { |
| final int docBase = context.docBase; |
| |
| if(ordinalMap != null) { |
| this.segmentValues = this.multiSortedDocValues.values[context.ord]; |
| this.segmentOrdinalMap = ordinalMap.getGlobalOrds(context.ord); |
| } |
| |
| final LongObjectMap<LeafCollector> leafCollectors = new LongObjectHashMap<>(); |
| for (LongObjectCursor<Collector> entry : groups) { |
| leafCollectors.put(entry.key, entry.value.getLeafCollector(context)); |
| } |
| return new LeafCollector() { |
| |
| @Override |
| public void setScorer(Scorable scorer) throws IOException { |
| for (ObjectCursor<LeafCollector> c : leafCollectors.values()) { |
| c.value.setScorer(scorer); |
| } |
| } |
| |
| @Override |
| public void collect(int docId) throws IOException { |
| int globalDoc = docId + docBase; |
| int ord = -1; |
| if(ordinalMap != null) { |
| if (docId > segmentValues.docID()) { |
| segmentValues.advance(docId); |
| } |
| if (docId == segmentValues.docID()) { |
| ord = (int)segmentOrdinalMap.get(segmentValues.ordValue()); |
| } else { |
| ord = -1; |
| } |
| } else { |
| if (docValues.advanceExact(globalDoc)) { |
| ord = docValues.ordValue(); |
| } else { |
| ord = -1; |
| } |
| } |
| |
| if (ord > -1 && groupBits.get(ord) && !collapsedSet.contains(globalDoc)) { |
| LeafCollector c = leafCollectors.get(ord); |
| c.collect(docId); |
| } |
| } |
| }; |
| } |
| |
| public LongObjectMap<Collector> getGroups() { |
| return groups; |
| } |
| } |
| |
| private static class NumericGroupExpandCollector implements Collector, GroupCollector { |
| private NumericDocValues docValues; |
| |
| private String field; |
| private LongObjectHashMap<Collector> groups; |
| |
| private IntHashSet collapsedSet; |
| private long nullValue; |
| |
| public NumericGroupExpandCollector(String field, long nullValue, LongHashSet groupSet, IntHashSet collapsedSet, int limit, Sort sort) throws IOException { |
| int numGroups = collapsedSet.size(); |
| this.nullValue = nullValue; |
| groups = new LongObjectHashMap<>(numGroups); |
| for (LongCursor cursor : groupSet) { |
| groups.put(cursor.value, getCollector(limit, sort)); |
| } |
| |
| this.field = field; |
| this.collapsedSet = collapsedSet; |
| } |
| |
| public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { |
| final int docBase = context.docBase; |
| this.docValues = context.reader().getNumericDocValues(this.field); |
| |
| final LongObjectHashMap<LeafCollector> leafCollectors = new LongObjectHashMap<>(); |
| |
| for (LongObjectCursor<Collector> entry : groups) { |
| leafCollectors.put(entry.key, entry.value.getLeafCollector(context)); |
| } |
| |
| return new LeafCollector() { |
| |
| @Override |
| public void setScorer(Scorable scorer) throws IOException { |
| for (ObjectCursor<LeafCollector> c : leafCollectors.values()) { |
| c.value.setScorer(scorer); |
| } |
| } |
| |
| @Override |
| public void collect(int docId) throws IOException { |
| long value; |
| if (docValues.advanceExact(docId)) { |
| value = docValues.longValue(); |
| } else { |
| value = 0; |
| } |
| final int index; |
| if (value != nullValue && |
| (index = leafCollectors.indexOf(value)) >= 0 && |
| !collapsedSet.contains(docId + docBase)) { |
| leafCollectors.indexGet(index).collect(docId); |
| } |
| } |
| }; |
| } |
| |
| public LongObjectHashMap<Collector> getGroups() { |
| return groups; |
| } |
| |
| } |
| |
| //TODO lets just do simple abstract base class -- a fine use of inheritance |
| private interface GroupCollector extends Collector { |
| public LongObjectMap<Collector> getGroups(); |
| |
| @Override |
| default ScoreMode scoreMode() { |
| final LongObjectMap<Collector> groups = getGroups(); |
| if (groups.isEmpty()) { |
| return ScoreMode.COMPLETE; // doesn't matter? |
| } else { |
| return groups.iterator().next().value.scoreMode(); // we assume all the collectors should have the same nature |
| } |
| } |
| |
| default Collector getCollector(int limit, Sort sort) throws IOException { |
| Collector collector; |
| if (limit == 0) { |
| collector = new TotalHitCountCollector(); |
| } else if (sort == null) { |
| collector = TopScoreDocCollector.create(limit, Integer.MAX_VALUE); |
| } else { |
| collector = TopFieldCollector.create(sort, limit, Integer.MAX_VALUE); |
| } |
| return collector; |
| } |
| } |
| |
| private Query getGroupQuery(String fname, |
| FieldType ft, |
| int size, |
| LongHashSet groupSet) { |
| |
| BytesRef[] bytesRefs = new BytesRef[size]; |
| int index = -1; |
| BytesRefBuilder term = new BytesRefBuilder(); |
| Iterator<LongCursor> it = groupSet.iterator(); |
| |
| while (it.hasNext()) { |
| LongCursor cursor = it.next(); |
| String stringVal = numericToString(ft, cursor.value); |
| ft.readableToIndexed(stringVal, term); |
| bytesRefs[++index] = term.toBytesRef(); |
| } |
| |
| return new TermInSetQuery(fname, bytesRefs); |
| } |
| |
| private Query getPointGroupQuery(SchemaField sf, |
| int size, |
| LongHashSet groupSet) { |
| |
| Iterator<LongCursor> it = groupSet.iterator(); |
| List<String> values = new ArrayList<>(size); |
| FieldType ft = sf.getType(); |
| while (it.hasNext()) { |
| LongCursor cursor = it.next(); |
| values.add(numericToString(ft, cursor.value)); |
| } |
| |
| return sf.getType().getSetQuery(null, sf, values); |
| } |
| |
| private String numericToString(FieldType fieldType, long val) { |
| if (fieldType.getNumberType() != null) { |
| switch (fieldType.getNumberType()) { |
| case INTEGER: |
| case LONG: |
| return Long.toString(val); |
| case FLOAT: |
| return Float.toString(Float.intBitsToFloat((int)val)); |
| case DOUBLE: |
| return Double.toString(Double.longBitsToDouble(val)); |
| case DATE: |
| break; |
| } |
| } |
| throw new IllegalArgumentException("FieldType must be INT,LONG,FLOAT,DOUBLE found " + fieldType); |
| } |
| |
| private Query getGroupQuery(String fname, |
| int size, |
| IntObjectHashMap<BytesRef> ordBytes) { |
| BytesRef[] bytesRefs = new BytesRef[size]; |
| int index = -1; |
| Iterator<IntObjectCursor<BytesRef>>it = ordBytes.iterator(); |
| while (it.hasNext()) { |
| IntObjectCursor<BytesRef> cursor = it.next(); |
| bytesRefs[++index] = cursor.value; |
| } |
| return new TermInSetQuery(fname, bytesRefs); |
| } |
| |
| |
| //////////////////////////////////////////// |
| /// SolrInfoBean |
| //////////////////////////////////////////// |
| |
| @Override |
| public String getDescription() { |
| return "Expand Component"; |
| } |
| |
| @Override |
| public Category getCategory() { |
| return Category.QUERY; |
| } |
| |
| } |