blob: 9dad2f611fab00d370618ac22ac9370050f703c2 [file] [log] [blame]
/*
* 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.Collections;
import java.util.List;
import java.util.Set;
import org.apache.lucene.search.Query;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams.FacetRangeMethod;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
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.request.IntervalFacets;
import org.apache.solr.request.SimpleFacets;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.TrieField;
import org.apache.solr.search.DocSet;
import org.apache.solr.search.SyntaxError;
/**
* Processor for Range Facets
*/
public class RangeFacetProcessor extends SimpleFacets {
public RangeFacetProcessor(SolrQueryRequest req, DocSet docs, SolrParams params, ResponseBuilder rb) {
super(req, docs, params, rb);
}
/**
* Returns a list of value constraints and the associated facet
* counts for each facet numerical field, range, and interval
* specified in the SolrParams
*
* @see org.apache.solr.common.params.FacetParams#FACET_RANGE
*/
public NamedList<Object> getFacetRangeCounts() throws IOException, SyntaxError {
final NamedList<Object> resOuter = new SimpleOrderedMap<>();
List<RangeFacetRequest> rangeFacetRequests = Collections.emptyList();
try {
FacetComponent.FacetContext facetContext = FacetComponent.FacetContext.getFacetContext(req);
rangeFacetRequests = facetContext.getAllRangeFacetRequests();
} catch (IllegalStateException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unable to compute facet ranges, facet context is not set");
}
if (rangeFacetRequests.isEmpty()) return resOuter;
for (RangeFacetRequest rangeFacetRequest : rangeFacetRequests) {
getFacetRangeCounts(rangeFacetRequest, resOuter);
}
return resOuter;
}
/**
* Returns a list of value constraints and the associated facet counts
* for each facet range specified by the given {@link RangeFacetRequest}
*/
public void getFacetRangeCounts(RangeFacetRequest rangeFacetRequest, NamedList<Object> resOuter)
throws IOException, SyntaxError {
final IndexSchema schema = searcher.getSchema();
final String key = rangeFacetRequest.getKey();
final String f = rangeFacetRequest.facetOn;
FacetRangeMethod method = rangeFacetRequest.getMethod();
final SchemaField sf = schema.getField(f);
final FieldType ft = sf.getType();
if (method.equals(FacetRangeMethod.DV)) {
assert ft instanceof TrieField || ft.isPointField();
resOuter.add(key, getFacetRangeCountsDocValues(rangeFacetRequest));
} else {
resOuter.add(key, getFacetRangeCounts(rangeFacetRequest));
}
}
@SuppressWarnings({"rawtypes"})
private <T extends Comparable<T>> NamedList getFacetRangeCounts(final RangeFacetRequest rfr)
throws IOException, SyntaxError {
final NamedList<Object> res = new SimpleOrderedMap<>();
final NamedList<Integer> counts = new NamedList<>();
res.add("counts", counts);
// explicitly return the gap.
res.add("gap", rfr.getGapObj());
DocSet docSet = computeDocSet(docsOrig, rfr.getExcludeTags());
for (RangeFacetRequest.FacetRange range : rfr.getFacetRanges()) {
if (range.other != null) {
// these are added to top-level NamedList
// and we always include them regardless of mincount
res.add(range.other.toString(), rangeCount(docSet, rfr, range));
} else {
final int count = rangeCount(docSet, rfr, range);
if (count >= rfr.getMinCount()) {
counts.add(range.lower, count);
}
}
}
// explicitly return the start and end so all the counts
// (including before/after/between) are meaningful - even if mincount
// has removed the neighboring ranges
res.add("start", rfr.getStartObj());
res.add("end", rfr.getEndObj());
return res;
}
private <T extends Comparable<T>> NamedList<Object> getFacetRangeCountsDocValues(RangeFacetRequest rfr)
throws IOException, SyntaxError {
SchemaField sf = rfr.getSchemaField();
final NamedList<Object> res = new SimpleOrderedMap<>();
final NamedList<Integer> counts = new NamedList<>();
res.add("counts", counts);
ArrayList<IntervalFacets.FacetInterval> intervals = new ArrayList<>();
// explicitly return the gap. compute this early so we are more
// likely to catch parse errors before attempting math
res.add("gap", rfr.getGapObj());
final int minCount = rfr.getMinCount();
boolean includeBefore = false;
boolean includeBetween = false;
boolean includeAfter = false;
Set<FacetRangeOther> others = rfr.getOthers();
// Intervals must be in order (see IntervalFacets.getSortedIntervals), if "BEFORE" or
// "BETWEEN" are set, they must be added first
// no matter what other values are listed, we don't do
// anything if "none" is specified.
if (!others.contains(FacetRangeOther.NONE)) {
if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.BEFORE)) {
// We'll add an interval later in this position
intervals.add(null);
includeBefore = true;
}
if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.BETWEEN)) {
// We'll add an interval later in this position
intervals.add(null);
includeBetween = true;
}
if (others.contains(FacetRangeOther.ALL) || others.contains(FacetRangeOther.AFTER)) {
includeAfter = true;
}
}
IntervalFacets.FacetInterval after = null;
for (RangeFacetRequest.FacetRange range : rfr.getFacetRanges()) {
if (range.other != null) {
switch (range.other) {
case BEFORE:
assert range.lower == null;
intervals.set(0, new IntervalFacets.FacetInterval(sf, "*", range.upper, range.includeLower,
range.includeUpper, FacetRangeOther.BEFORE.toString()));
break;
case AFTER:
assert range.upper == null;
after = new IntervalFacets.FacetInterval(sf, range.lower, "*",
range.includeLower, range.includeUpper, FacetRangeOther.AFTER.toString());
break;
case BETWEEN:
intervals.set(includeBefore ? 1 : 0, new IntervalFacets.FacetInterval(sf, range.lower, range.upper,
range.includeLower, range.includeUpper, FacetRangeOther.BETWEEN.toString()));
break;
case ALL:
case NONE:
break;
}
} else {
intervals.add(new IntervalFacets.FacetInterval(sf, range.lower, range.upper, range.includeLower, range.includeUpper, range.lower));
}
}
if (includeAfter) {
assert after != null;
intervals.add(after);
}
IntervalFacets.FacetInterval[] intervalsArray = intervals.toArray(new IntervalFacets.FacetInterval[intervals.size()]);
// don't use the ArrayList anymore
intervals = null;
new IntervalFacets(sf, searcher, computeDocSet(docsOrig, rfr.getExcludeTags()), intervalsArray);
int intervalIndex = 0;
int lastIntervalIndex = intervalsArray.length - 1;
// if the user requested "BEFORE", it will be the first of the intervals. Needs to be added to the
// response named list instead of with the counts
if (includeBefore) {
res.add(intervalsArray[intervalIndex].getKey(), intervalsArray[intervalIndex].getCount());
intervalIndex++;
}
// if the user requested "BETWEEN", it will be the first or second of the intervals (depending on if
// "BEFORE" was also requested). Needs to be added to the response named list instead of with the counts
if (includeBetween) {
res.add(intervalsArray[intervalIndex].getKey(), intervalsArray[intervalIndex].getCount());
intervalIndex++;
}
// if the user requested "AFTER", it will be the last of the intervals.
// Needs to be added to the response named list instead of with the counts
if (includeAfter) {
res.add(intervalsArray[lastIntervalIndex].getKey(), intervalsArray[lastIntervalIndex].getCount());
lastIntervalIndex--;
}
// now add all other intervals to the counts NL
while (intervalIndex <= lastIntervalIndex) {
IntervalFacets.FacetInterval interval = intervalsArray[intervalIndex];
if (interval.getCount() >= minCount) {
counts.add(interval.getKey(), interval.getCount());
}
intervalIndex++;
}
res.add("start", rfr.getStartObj());
res.add("end", rfr.getEndObj());
return res;
}
/**
* Macro for getting the numDocs of range over docs
*
* @see org.apache.solr.search.SolrIndexSearcher#numDocs
* @see org.apache.lucene.search.TermRangeQuery
*/
protected int rangeCount(DocSet subset, RangeFacetRequest rfr, RangeFacetRequest.FacetRange fr) throws IOException, SyntaxError {
SchemaField schemaField = rfr.getSchemaField();
Query rangeQ = schemaField.getType().getRangeQuery(null, schemaField, fr.lower, fr.upper, fr.includeLower, fr.includeUpper);
if (rfr.isGroupFacet()) {
return getGroupedFacetQueryCount(rangeQ, subset);
} else {
return searcher.numDocs(rangeQ, subset);
}
}
}