SOLR-9279: new function queries: gt, gte, lt, lte, eq

Lucene Queries module: new ComparisonBoolFunction base class
This commit is contained in:
David Smiley 2016-07-28 22:45:43 -04:00
parent cead204fb6
commit d12b93e272
7 changed files with 311 additions and 6 deletions

View File

@ -65,6 +65,9 @@ New Features
Polygon instances from a standard GeoJSON string (Robert Muir, Mike Polygon instances from a standard GeoJSON string (Robert Muir, Mike
McCandless) McCandless)
* SOLR-9279: Queries module: new ComparisonBoolFunction base class
(Doug Turnbull via David Smiley)
Bug Fixes Bug Fixes
* LUCENE-6662: Fixed potential resource leaks. (Rishabh Patel via Adrien Grand) * LUCENE-6662: Fixed potential resource leaks. (Rishabh Patel via Adrien Grand)

View File

@ -0,0 +1,105 @@
/*
* 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.lucene.queries.function.valuesource;
import java.io.IOException;
import java.util.Map;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.BoolDocValues;
import org.apache.lucene.search.IndexSearcher;
/**
* Base class for comparison operators useful within an "if"/conditional.
*/
public abstract class ComparisonBoolFunction extends BoolFunction {
private final ValueSource lhs;
private final ValueSource rhs;
private final String name;
public ComparisonBoolFunction(ValueSource lhs, ValueSource rhs, String name) {
this.lhs = lhs;
this.rhs = rhs;
this.name = name;
}
/** Perform the comparison, returning true or false */
public abstract boolean compare(int doc, FunctionValues lhs, FunctionValues rhs);
/** Uniquely identify the operation (ie "gt", "lt" "gte", etc) */
public String name() {
return this.name;
}
@Override
public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
final FunctionValues lhsVal = this.lhs.getValues(context, readerContext);
final FunctionValues rhsVal = this.rhs.getValues(context, readerContext);
final String compLabel = this.name();
return new BoolDocValues(this) {
@Override
public boolean boolVal(int doc) {
return compare(doc, lhsVal, rhsVal);
}
@Override
public String toString(int doc) {
return compLabel + "(" + lhsVal.toString(doc) + "," + rhsVal.toString(doc) + ")";
}
@Override
public boolean exists(int doc) {
return lhsVal.exists(doc) && rhsVal.exists(doc);
}
};
}
@Override
public boolean equals(Object o) {
if (this.getClass() != o.getClass()) return false;
ComparisonBoolFunction other = (ComparisonBoolFunction)o;
return name().equals(other.name())
&& lhs.equals(other.lhs)
&& rhs.equals(other.rhs); }
@Override
public int hashCode() {
int h = this.getClass().hashCode();
h = h * 31 + this.name().hashCode();
h = h * 31 + lhs.hashCode();
h = h * 31 + rhs.hashCode();
return h;
}
@Override
public String description() {
return name() + "(" + lhs.description() + "," + rhs.description() + ")";
}
@Override
public void createWeight(Map context, IndexSearcher searcher) throws IOException {
lhs.createWeight(context, searcher);
rhs.createWeight(context, searcher);
}
}

View File

@ -108,6 +108,9 @@ New Features
doing a core backup, and in replication. Snapshot metadata is stored in a new snapshot_metadata/ dir. doing a core backup, and in replication. Snapshot metadata is stored in a new snapshot_metadata/ dir.
(Hrishikesh Gadre via David Smiley) (Hrishikesh Gadre via David Smiley)
* SOLR-9279: New boolean comparison function queries comparing numeric arguments: gt, gte, lt, lte, eq
(Doug Turnbull, David Smiley)
Bug Fixes Bug Fixes
---------------------- ----------------------

View File

@ -64,6 +64,7 @@ import org.apache.solr.search.facet.UniqueAgg;
import org.apache.solr.search.function.CollapseScoreFunction; import org.apache.solr.search.function.CollapseScoreFunction;
import org.apache.solr.search.function.OrdFieldSource; import org.apache.solr.search.function.OrdFieldSource;
import org.apache.solr.search.function.ReverseOrdFieldSource; import org.apache.solr.search.function.ReverseOrdFieldSource;
import org.apache.solr.search.function.SolrComparisonBoolFunction;
import org.apache.solr.search.function.distance.GeoDistValueSourceParser; import org.apache.solr.search.function.distance.GeoDistValueSourceParser;
import org.apache.solr.search.function.distance.GeohashFunction; import org.apache.solr.search.function.distance.GeohashFunction;
import org.apache.solr.search.function.distance.GeohashHaversineFunction; import org.apache.solr.search.function.distance.GeohashHaversineFunction;
@ -822,6 +823,57 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin {
} }
}); });
addParser("gt", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
ValueSource lhsValSource = fp.parseValueSource();
ValueSource rhsValSource = fp.parseValueSource();
return new SolrComparisonBoolFunction(lhsValSource, rhsValSource, "gt", (cmp) -> cmp > 0);
}
});
addParser("lt", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
ValueSource lhsValSource = fp.parseValueSource();
ValueSource rhsValSource = fp.parseValueSource();
return new SolrComparisonBoolFunction(lhsValSource, rhsValSource, "lt", (cmp) -> cmp < 0);
}
});
addParser("gte", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
ValueSource lhsValSource = fp.parseValueSource();
ValueSource rhsValSource = fp.parseValueSource();
return new SolrComparisonBoolFunction(lhsValSource, rhsValSource, "gte", (cmp) -> cmp >= 0);
}
});
addParser("lte", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
ValueSource lhsValSource = fp.parseValueSource();
ValueSource rhsValSource = fp.parseValueSource();
return new SolrComparisonBoolFunction(lhsValSource, rhsValSource, "lte", (cmp) -> cmp <= 0);
}
});
addParser("eq", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError {
ValueSource lhsValSource = fp.parseValueSource();
ValueSource rhsValSource = fp.parseValueSource();
return new SolrComparisonBoolFunction(lhsValSource, rhsValSource, "eq", (cmp) -> cmp == 0);
}
});
addParser("def", new ValueSourceParser() { addParser("def", new ValueSourceParser() {
@Override @Override
public ValueSource parse(FunctionQParser fp) throws SyntaxError { public ValueSource parse(FunctionQParser fp) throws SyntaxError {

View File

@ -0,0 +1,58 @@
/*
* 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.function;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.docvalues.IntDocValues;
import org.apache.lucene.queries.function.docvalues.LongDocValues;
import org.apache.lucene.queries.function.valuesource.ComparisonBoolFunction;
/**
* Refines {@link ComparisonBoolFunction} to compare based on a 'long' or 'double' depending on if the
* any of the FunctionValues are {@link LongDocValues}.
*/
public class SolrComparisonBoolFunction extends ComparisonBoolFunction {
private final Compare cmp;
public interface Compare {
boolean compare(int integer);
}
public SolrComparisonBoolFunction(ValueSource lhs, ValueSource rhs, String name, Compare cmp) {
super(lhs, rhs, name);
this.cmp = cmp;
}
@Override
public boolean compare(int doc, FunctionValues lhs, FunctionValues rhs) {
// TODO consider a separate FunctionValues impl, one for Long, one for Double
// performs the safest possible numeric comparison, if both lhs and rhs are Longs, then
// we perform a Long comparison to avoid the issues with precision when casting to doubles
boolean lhsAnInt = (lhs instanceof LongDocValues || lhs instanceof IntDocValues);
boolean rhsAnInt = (rhs instanceof LongDocValues || rhs instanceof IntDocValues);
if (lhsAnInt && rhsAnInt) {
return cmp.compare(Long.compare(lhs.longVal(doc), rhs.longVal(doc)));
} else {
return cmp.compare(Double.compare(lhs.doubleVal(doc), rhs.doubleVal(doc)));
}
}
// note: don't override equals; the "name" will be unique and is already compared
}

View File

@ -15,6 +15,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.apache.solr.search; package org.apache.solr.search;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryUtils; import org.apache.lucene.search.QueryUtils;
import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.SolrTestCaseJ4;
@ -24,10 +29,6 @@ import org.apache.solr.response.SolrQueryResponse;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** /**
@ -1075,4 +1076,22 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
// assertFuncEquals("agg_multistat(foo_i)", "agg_multistat(foo_i)"); // assertFuncEquals("agg_multistat(foo_i)", "agg_multistat(foo_i)");
} }
public void testCompares() throws Exception {
assertFuncEquals("gt(foo_i,2)", "gt(foo_i, 2)");
assertFuncEquals("gt(foo_i,2)", "gt(foo_i,2)");
assertFuncEquals("lt(foo_i,2)", "lt(foo_i,2)");
assertFuncEquals("lte(foo_i,2)", "lte(foo_i,2)");
assertFuncEquals("gte(foo_i,2)", "gte(foo_i,2)");
assertFuncEquals("eq(foo_i,2)", "eq(foo_i,2)");
boolean equals = false;
try {
assertFuncEquals("eq(foo_i,2)", "lt(foo_i,2)");
equals = true;
} catch (AssertionError e) {
//expected
}
assertFalse(equals);
}
} }

View File

@ -98,7 +98,7 @@ public class TestFunctionQuery extends SolrTestCaseJ4 {
return sb.toString(); return sb.toString();
} }
void singleTest(String field, String funcTemplate, List<String> args, float... results) { protected void singleTest(String field, String funcTemplate, List<String> args, float... results) {
String parseableQuery = func(field, funcTemplate); String parseableQuery = func(field, funcTemplate);
List<String> nargs = new ArrayList<>(Arrays.asList("q", parseableQuery List<String> nargs = new ArrayList<>(Arrays.asList("q", parseableQuery
@ -793,4 +793,69 @@ public class TestFunctionQuery extends SolrTestCaseJ4 {
} }
} }
} @Test
public void testNumericComparisons() throws Exception {
assertU(adoc("id", "1", "age_i", "35"));
assertU(adoc("id", "2", "age_i", "25"));
assertU(commit());
// test weighting of functions
assertJQ(req("q", "id:1", "fl", "a:gt(age_i,30),b:lt(age_i,30)")
, "/response/docs/[0]=={'a':true,'b':false}");
assertJQ(req("q", "id:1", "fl", "a:exists(gt(foo_i,30))")
, "/response/docs/[0]=={'a':false}");
singleTest("age_i", "if(gt(age_i,30),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/2);
singleTest("age_i", "if(lt(age_i,30),5,2)",
/*id*/1, /*score*/2,
/*id*/2, /*score*/5);
singleTest("age_i", "if(lt(age_i,34.5),5,2)",
/*id*/1, /*score*/2,
/*id*/2, /*score*/5);
singleTest("age_i", "if(lte(age_i,35),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/5);
singleTest("age_i", "if(gte(age_i,25),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/5);
singleTest("age_i", "if(lte(age_i,25),5,2)",
/*id*/1, /*score*/2,
/*id*/2, /*score*/5);
singleTest("age_i", "if(gte(age_i,35),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/2);
singleTest("age_i", "if(eq(age_i,30),5,2)",
/*id*/1, /*score*/2,
/*id*/2, /*score*/2);
singleTest("age_i", "if(eq(age_i,35),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/2);
}
public void testLongComparisons() {
assertU(adoc("id", "1", "number_of_atoms_in_universe_l", Long.toString(Long.MAX_VALUE)));
assertU(adoc("id", "2", "number_of_atoms_in_universe_l", Long.toString(Long.MAX_VALUE - 1)));
assertU(commit());
singleTest("number_of_atoms_in_universe_l", "if(gt(number_of_atoms_in_universe_l," + Long.toString(Long.MAX_VALUE - 1) + "),5,2)",
/*id*/1, /*score*/5,
/*id*/2, /*score*/2);
singleTest("number_of_atoms_in_universe_l", "if(lt(number_of_atoms_in_universe_l," + Long.toString(Long.MAX_VALUE) + "),5,2)",
/*id*/2, /*score*/5,
/*id*/1, /*score*/2);
}
}