diff --git a/solr/core/src/java/org/apache/solr/search/FunctionQParser.java b/solr/core/src/java/org/apache/solr/search/FunctionQParser.java index d3a311d936ab..023890899328 100644 --- a/solr/core/src/java/org/apache/solr/search/FunctionQParser.java +++ b/solr/core/src/java/org/apache/solr/search/FunctionQParser.java @@ -438,6 +438,14 @@ public AggValueSource parseAgg(int flags) throws SyntaxError { return vs; } + public List parseAggValueSourceList() throws SyntaxError { + List sources = new ArrayList<>(3); + while (hasMoreArguments()) { + sources.add(parseAgg(FLAG_DEFAULT | FLAG_CONSUME_DELIMITER)); + } + return sources; + } + /** * Consume an argument delimiter (a comma) from the token stream. diff --git a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java index fda8a7e1fa48..9bc3c8940862 100644 --- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java +++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java @@ -57,8 +57,10 @@ import org.apache.solr.search.facet.AggValueSource; import org.apache.solr.search.facet.AvgAgg; import org.apache.solr.search.facet.CountAgg; +import org.apache.solr.search.facet.DivAgg; import org.apache.solr.search.facet.HLLAgg; import org.apache.solr.search.facet.MinMaxAgg; +import org.apache.solr.search.facet.MulAgg; import org.apache.solr.search.facet.PercentileAgg; import org.apache.solr.search.facet.StddevAgg; import org.apache.solr.search.facet.SumAgg; @@ -1054,6 +1056,20 @@ public ValueSource parse(FunctionQParser fp) throws SyntaxError { addParser("agg_topdocs", new TopDocsAgg.Parser()); + addParser("agg_mul", new ValueSourceParser() { + @Override + public ValueSource parse(FunctionQParser fp) throws SyntaxError { + return new MulAgg(fp.parseAggValueSourceList()); + } + }); + + addParser("agg_div", new ValueSourceParser() { + @Override + public ValueSource parse(FunctionQParser fp) throws SyntaxError { + return new DivAgg(fp.parseAggValueSourceList()); + } + }); + addParser("childfield", new ChildFieldValueSourceParser()); } diff --git a/solr/core/src/java/org/apache/solr/search/facet/CombiningAggValueSource.java b/solr/core/src/java/org/apache/solr/search/facet/CombiningAggValueSource.java new file mode 100644 index 000000000000..bdbd478c4e94 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/search/facet/CombiningAggValueSource.java @@ -0,0 +1,189 @@ +/* + * 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.search.facet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import org.apache.lucene.index.LeafReaderContext; +import org.apache.solr.search.DocSet; + +public abstract class CombiningAggValueSource extends AggValueSource { + + private final List subAggs; + + public CombiningAggValueSource(String name, List subAggs) { + super(name); + this.subAggs = subAggs; + } + + @Override + public FacetMerger createFacetMerger(Object prototype) { + List prototypeResult = (List)prototype; + if (prototypeResult.size() != subAggs.size()) { + throw new IllegalStateException("Aggregate merge prototype has unexpected size : " + prototypeResult.size()); + } + List subMergers = new ArrayList<>(subAggs.size()); + for (int pos = 0; pos < subAggs.size(); pos++) { + subMergers.add(subAggs.get(pos).createFacetMerger(prototypeResult.get(pos))); + } + + return new FacetDoubleMerger() { + @Override + public void merge(Object facetResult, Context mcontext) { + List shardResult = (List)facetResult; + if (shardResult.size() != subMergers.size()) { + throw new IllegalStateException("Aggregate merge response has unexpected size : " + shardResult.size()); + } + for (int pos = 0; pos < shardResult.size(); pos++) { + subMergers.get(pos).merge(shardResult.get(pos), mcontext); + } + } + + @Override + protected double getDouble() { + List subValues = new ArrayList<>(); + for (FacetMerger subMerger : subMergers) { + subValues.add((Number)subMerger.getMergedResult()); + } + return combine(subValues); + } + }; + } + + protected abstract Double combine(List values); + + @Override + public SlotAcc createSlotAcc(FacetContext fcontext, int numDocs, int numSlots) throws IOException { + List subAcc = new ArrayList<>(subAggs.size()); + for (AggValueSource sub : subAggs) { + subAcc.add(sub.createSlotAcc(fcontext, numDocs, numSlots)); + } + + return new SlotAcc(fcontext) { + + // cache of combined values, since compare operations may ask for these repeatedly + Double[] combinedValues = new Double[numSlots]; + + @Override + public int compare(int slotA, int slotB) { + try { + Double valueA = getCombinedValue(slotA); + Double valueB = getCombinedValue(slotB); + return Double.compare(valueA, valueB); + } catch (IOException ioe) { + throw new RuntimeException("Exception in aggregate comparison", ioe); + } + } + + @Override + public Object getValue(int slotNum) throws IOException { + if (fcontext.isShard()) { + List lst = new ArrayList<>(subAcc.size()); + for (SlotAcc acc : subAcc) { + Object accValue = acc.getValue(slotNum); + lst.add(accValue); + } + return lst; + } else { + return getCombinedValue(slotNum); + } + } + + @Override + public Object getSortableValue(int slotNum) throws IOException { + return getCombinedValue(slotNum); + } + + private Double getCombinedValue(int slot) throws IOException { + if (combinedValues[slot] == null) { + List subValues = new ArrayList<>(); + for (SlotAcc acc : subAcc) { + subValues.add((Number)acc.getSortableValue(slot)); + } + combinedValues[slot] = combine(subValues); + } + return combinedValues[slot]; + } + + @Override + public void setNextReader(LeafReaderContext readerContext) throws IOException { + super.setNextReader(readerContext); + for (SlotAcc acc : subAcc) { + acc.setNextReader(readerContext); + } + } + + @Override + public int collect(DocSet docs, int slot, IntFunction slotContext) throws IOException { + for (SlotAcc acc : subAcc) { + acc.collect(docs, slot, slotContext); + } + return docs.size(); + } + + @Override + protected void resetIterators() throws IOException { + for (SlotAcc acc : subAcc) { + acc.resetIterators(); + } + } + + @Override + public void collect(int doc, int slot, IntFunction slotContext) throws IOException { + for (SlotAcc acc : subAcc) { + acc.collect(doc, slot, slotContext); + } + } + + @Override + public void reset() throws IOException { + Arrays.fill(combinedValues, null); + for (SlotAcc acc : subAcc) { + acc.reset(); + } + } + + @Override + public void resize(Resizer resizer) { + combinedValues = resizer.resize(combinedValues, null); + for (SlotAcc acc : subAcc) { + acc.resize(resizer); + } + } + }; + } + + @Override + public int hashCode() { + return Objects.hash(subAggs.toArray()); + } + + @Override + public String description() { + String subAggsDescription = subAggs.stream() + .map(AggValueSource::description) + .collect(Collectors.joining(",")); + return name() + "(" + subAggsDescription + ")"; + } +} diff --git a/solr/core/src/java/org/apache/solr/search/facet/DivAgg.java b/solr/core/src/java/org/apache/solr/search/facet/DivAgg.java new file mode 100644 index 000000000000..d00952ad0c45 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/search/facet/DivAgg.java @@ -0,0 +1,40 @@ +/* + * 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.search.facet; + +import java.util.List; + +import org.apache.solr.common.SolrException; + +public class DivAgg extends CombiningAggValueSource { + + public DivAgg(List subAggs) { + super("div", subAggs); + if (subAggs.size() != 2) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "div aggregate requires exactly 2 arguments"); + } + } + + @Override + protected Double combine(List values) { + if (values.size() != 2) { + throw new IllegalStateException("div aggregate expects 2 values but has " + values.size()); + } + return values.get(0).doubleValue() / values.get(1).doubleValue(); + } +} diff --git a/solr/core/src/java/org/apache/solr/search/facet/HLLAgg.java b/solr/core/src/java/org/apache/solr/search/facet/HLLAgg.java index 4634bc241af8..e37f308a7be6 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/HLLAgg.java +++ b/solr/core/src/java/org/apache/solr/search/facet/HLLAgg.java @@ -170,6 +170,11 @@ public Object getValue(int slot) throws IOException { return getCardinality(slot); } + @Override + public Object getSortableValue(int slot) { + return getCardinality(slot); + } + private int getCardinality(int slot) { HLL set = sets[slot]; return set==null ? 0 : (int)set.cardinality(); diff --git a/solr/core/src/java/org/apache/solr/search/facet/MulAgg.java b/solr/core/src/java/org/apache/solr/search/facet/MulAgg.java new file mode 100644 index 000000000000..d9e8247d6d28 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/search/facet/MulAgg.java @@ -0,0 +1,39 @@ +/* + * 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.search.facet; + +import java.util.List; + +import org.apache.solr.common.SolrException; + +public class MulAgg extends CombiningAggValueSource { + + public MulAgg(List subAggs) { + super("mul", subAggs); + if (subAggs.size() < 2) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "mul aggregate requires at least 2 arguments"); + } + } + + @Override + protected Double combine(List values) { + return values.stream() + .mapToDouble(Number::doubleValue) + .reduce(1.0D, (x,y) -> x * y); + } +} diff --git a/solr/core/src/java/org/apache/solr/search/facet/PercentileAgg.java b/solr/core/src/java/org/apache/solr/search/facet/PercentileAgg.java index efdef553a806..11f325706936 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/PercentileAgg.java +++ b/solr/core/src/java/org/apache/solr/search/facet/PercentileAgg.java @@ -147,7 +147,13 @@ private void fillSortVals() { public Object getValue(int slotNum) throws IOException { if (fcontext.isShard()) { return getShardValue(slotNum); + } else { + return getSortableValue(slotNum); } + } + + @Override + public Object getSortableValue(int slotNum) { if (sortvals != null && percentiles.size()==1) { // we've already calculated everything we need return digests[slotNum] != null ? sortvals[slotNum] : null; @@ -155,7 +161,6 @@ public Object getValue(int slotNum) throws IOException { return getValueFromDigest( digests[slotNum] ); } - public Object getShardValue(int slot) throws IOException { AVLTreeDigest digest = digests[slot]; if (digest == null) return null; // no values for this slot diff --git a/solr/core/src/java/org/apache/solr/search/facet/SlotAcc.java b/solr/core/src/java/org/apache/solr/search/facet/SlotAcc.java index 9c12aff99ea4..4bdf74be953b 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/SlotAcc.java +++ b/solr/core/src/java/org/apache/solr/search/facet/SlotAcc.java @@ -141,6 +141,13 @@ public int collect(DocSet docs, int slot, IntFunction slotContext) public abstract Object getValue(int slotNum) throws IOException; + /** Returns a value that can be used for sorting, because getValue + * may return an intermediate structure when called in an isShard context + */ + public Object getSortableValue(int slotNum) throws IOException { + return getValue(slotNum); + } + public void setValues(SimpleOrderedMap bucket, int slotNum) throws IOException { if (key == null) return; Object val = getValue(slotNum); @@ -471,6 +478,11 @@ public Object getValue(int slot) { } } + @Override + public Object getSortableValue(int slot) { + return avg(slot); + } + @Override public void resize(Resizer resizer) { super.resize(resizer); @@ -529,6 +541,11 @@ public Object getValue(int slot) { } } + @Override + public Object getSortableValue(int slot) { + return this.variance(slot); + } + @Override public void collect(int doc, int slot, IntFunction slotContext) throws IOException { double val = values.doubleVal(doc); @@ -591,6 +608,11 @@ public Object getValue(int slot) { } } + @Override + public Object getSortableValue(int slot) { + return this.stdDev(slot); + } + @Override public void collect(int doc, int slot, IntFunction slotContext) throws IOException { double val = values.doubleVal(doc); diff --git a/solr/core/src/java/org/apache/solr/search/facet/UniqueAgg.java b/solr/core/src/java/org/apache/solr/search/facet/UniqueAgg.java index 5d9cf90b10e8..e368e422db7c 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/UniqueAgg.java +++ b/solr/core/src/java/org/apache/solr/search/facet/UniqueAgg.java @@ -175,6 +175,11 @@ public Object getValue(int slot) throws IOException { return getCardinality(slot); } + @Override + public Object getSortableValue(int slot) { + return getCardinality(slot); + } + private int getCardinality(int slot) { LongSet set = sets[slot]; return set==null ? 0 : set.cardinality(); diff --git a/solr/core/src/java/org/apache/solr/search/facet/UniqueSlotAcc.java b/solr/core/src/java/org/apache/solr/search/facet/UniqueSlotAcc.java index 17575fb2a6cf..e6fe70e00470 100644 --- a/solr/core/src/java/org/apache/solr/search/facet/UniqueSlotAcc.java +++ b/solr/core/src/java/org/apache/solr/search/facet/UniqueSlotAcc.java @@ -55,7 +55,13 @@ public void reset() throws IOException { public Object getValue(int slot) throws IOException { if (fcontext.isShard()) { return getShardValue(slot); + } else { + return getSortableValue(slot); } + } + + @Override + public Object getSortableValue(int slot) { if (counts != null) { // will only be pre-populated if this was used for sorting. return counts[slot]; } @@ -149,4 +155,4 @@ public int compare(int slotA, int slotB) { public void resize(Resizer resizer) { arr = resizer.resize(arr, null); } -} \ No newline at end of file +}