SOLR-14590 : Add support for Lucene's FeatureField in Solr (#1620)

Add a new RankField type that internally creates a FeatureField
Add a new RankQParser that can create queries on the FeatureField
This commit is contained in:
Tomas Fernandez Lobbe 2020-06-30 11:15:36 -07:00 committed by GitHub
parent d1c29ae8a9
commit 6eb7bc3b7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 895 additions and 0 deletions

View File

@ -119,6 +119,10 @@ New Features
* SOLR-14599: Package manager support for cluster level plugins (see SOLR-14404) (Ishan Chattopadhyaya)
* SOLR-14590: Add support for RankFields. RankFields allow the use of per-document scoring factors in a
way that lets Solr skip over non-competitive documents when ranking. See SOLR-13289.
(Tomás Fernández Löbbe, Varun Thacker)
Improvements
---------------------
* SOLR-14316: Remove unchecked type conversion warning in JavaBinCodec's readMapEntry's equals() method

View File

@ -0,0 +1,140 @@
/*
* 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.schema;
import java.io.IOException;
import java.util.Map;
import org.apache.lucene.document.FeatureField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.IndexableFieldType;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.RankQParserPlugin;
import org.apache.solr.uninverting.UninvertingReader.Type;
/**
* <p>
* {@code RankField}s can be used to store scoring factors to improve document ranking. They should be used
* in combination with {@link RankQParserPlugin}. To use:
* </p>
* <p>
* Define the {@code RankField} {@code fieldType} in your schema:
* </p>
* <pre class="prettyprint">
* &lt;fieldType name="rank" class="solr.RankField" /&gt;
* </pre>
* <p>
* Add fields to the schema, i.e.:
* </p>
* <pre class="prettyprint">
* &lt;field name="pagerank" type="rank" /&gt;
* </pre>
*
* Query using the {@link RankQParserPlugin}, for example
* <pre class="prettyprint">
* http://localhost:8983/solr/techproducts?q=memory _query_:{!rank f='pagerank', function='log' scalingFactor='1.2'}
* </pre>
*
* @see RankQParserPlugin
* @lucene.experimental
* @since 8.6
*/
public class RankField extends FieldType {
/*
* While the user can create multiple RankFields, internally we use a single Lucene field,
* and we map the Solr field name to the "feature" in Lucene's FeatureField. This is mainly
* to simplify the user experience.
*/
public static final String INTERNAL_RANK_FIELD_NAME = "_rank_";
@Override
public Type getUninversionType(SchemaField sf) {
throw null;
}
@Override
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
}
@Override
protected void init(IndexSchema schema, Map<String,String> args) {
super.init(schema, args);
if (schema.getFieldOrNull(INTERNAL_RANK_FIELD_NAME) != null) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "A field named \"" + INTERNAL_RANK_FIELD_NAME + "\" can't be defined in the schema");
}
for (int prop:new int[] {STORED, DOC_VALUES, OMIT_TF_POSITIONS, SORT_MISSING_FIRST, SORT_MISSING_LAST}) {
if ((trueProperties & prop) != 0) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Property \"" + getPropertyName(prop) + "\" can't be set to true in RankFields");
}
}
for (int prop:new int[] {UNINVERTIBLE, INDEXED, MULTIVALUED}) {
if ((falseProperties & prop) != 0) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Property \"" + getPropertyName(prop) + "\" can't be set to false in RankFields");
}
}
properties &= ~(UNINVERTIBLE | STORED | DOC_VALUES);
}
@Override
protected IndexableField createField(String name, String val, IndexableFieldType type) {
if (val == null || val.isEmpty()) {
return null;
}
float featureValue;
try {
featureValue = Float.parseFloat(val);
} catch (NumberFormatException nfe) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error while creating field '" + name + "' from value '" + val + "'. Expecting float.", nfe);
}
// Internally, we always use the same field
return new FeatureField(INTERNAL_RANK_FIELD_NAME, name, featureValue);
}
@Override
public Query getExistenceQuery(QParser parser, SchemaField field) {
return new TermQuery(new Term(INTERNAL_RANK_FIELD_NAME, field.getName()));
}
@Override
public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Only a \"*\" term query can be done on RankFields");
}
@Override
protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2,
boolean minInclusive, boolean maxInclusive) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"Range queries not supported on RankFields");
}
@Override
public SortField getSortField(SchemaField field, boolean top) {
// We could use FeatureField.newFeatureSort()
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
"can not sort on a rank field: " + field.getName());
}
}

View File

@ -87,6 +87,7 @@ public abstract class QParserPlugin implements NamedListInitializedPlugin, SolrI
map.put(BoolQParserPlugin.NAME, new BoolQParserPlugin());
map.put(MinHashQParserPlugin.NAME, new MinHashQParserPlugin());
map.put(HashRangeQParserPlugin.NAME, new HashRangeQParserPlugin());
map.put(RankQParserPlugin.NAME, new RankQParserPlugin());
standardPlugins = Collections.unmodifiableMap(map);
}

View File

@ -0,0 +1,158 @@
/*
* 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;
import java.util.Locale;
import java.util.Objects;
import org.apache.lucene.document.FeatureField;
import org.apache.lucene.search.Query;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.RankField;
import org.apache.solr.schema.SchemaField;
/**
* {@code RankQParserPlugin} can be used to introduce document-depending scoring factors to ranking.
* While this {@code QParser} delivers a (subset of) functionality already available via {@link FunctionQParser},
* the benefit is that {@code RankQParserPlugin} can be used in combination with the {@code minExactCount} to
* use BlockMax-WAND algorithm (skip non-competitive documents) to provide faster responses.
*
* @see RankField
*
* @lucene.experimental
* @since 8.6
*/
public class RankQParserPlugin extends QParserPlugin {
public static final String NAME = "rank";
public static final String FIELD = "f";
public static final String FUNCTION = "function";
public static final String WEIGHT = "weight";
public static final String PIVOT = "pivot";
public static final String SCALING_FACTOR = "scalingFactor";
public static final String EXPONENT = "exponent";
private final static FeatureFieldFunction DEFAULT_FUNCTION = FeatureFieldFunction.SATU;
private enum FeatureFieldFunction {
SATU {
@Override
public Query createQuery(String fieldName, SolrParams params) throws SyntaxError {
Float weight = params.getFloat(WEIGHT);
Float pivot = params.getFloat(PIVOT);
if (pivot == null && (weight == null || Float.compare(weight.floatValue(), 1f) == 0)) {
// No IAE expected in this case
return FeatureField.newSaturationQuery(RankField.INTERNAL_RANK_FIELD_NAME, fieldName);
}
if (pivot == null) {
throw new SyntaxError("A pivot value needs to be provided if the weight is not 1 on \"satu\" function");
}
if (weight == null) {
weight = Float.valueOf(1);
}
try {
return FeatureField.newSaturationQuery(RankField.INTERNAL_RANK_FIELD_NAME, fieldName, weight, pivot);
} catch (IllegalArgumentException iae) {
throw new SyntaxError(iae.getMessage());
}
}
},
LOG {
@Override
public Query createQuery(String fieldName, SolrParams params) throws SyntaxError {
float weight = params.getFloat(WEIGHT, 1f);
float scalingFactor = params.getFloat(SCALING_FACTOR, 1f);
try {
return FeatureField.newLogQuery(RankField.INTERNAL_RANK_FIELD_NAME, fieldName, weight, scalingFactor);
} catch (IllegalArgumentException iae) {
throw new SyntaxError(iae.getMessage());
}
}
},
SIGM {
@Override
public Query createQuery(String fieldName, SolrParams params) throws SyntaxError {
float weight = params.getFloat(WEIGHT, 1f);
Float pivot = params.getFloat(PIVOT);
if (pivot == null) {
throw new SyntaxError("A pivot value needs to be provided when using \"sigm\" function");
}
Float exponent = params.getFloat(EXPONENT);
if (exponent == null) {
throw new SyntaxError("An exponent value needs to be provided when using \"sigm\" function");
}
try {
return FeatureField.newSigmoidQuery(RankField.INTERNAL_RANK_FIELD_NAME, fieldName, weight, pivot, exponent);
} catch (IllegalArgumentException iae) {
throw new SyntaxError(iae.getMessage());
}
}
};
public abstract Query createQuery(String fieldName, SolrParams params) throws SyntaxError;
}
@Override
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
Objects.requireNonNull(localParams, "LocalParams String can't be null");
Objects.requireNonNull(req, "SolrQueryRequest can't be null");
return new RankQParser(qstr, localParams, params, req);
}
public static class RankQParser extends QParser {
private final String field;
public RankQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
super(qstr, localParams, params, req);
this.field = localParams.get(FIELD);
}
@Override
public Query parse() throws SyntaxError {
if (this.field == null || this.field.isEmpty()) {
throw new SyntaxError("Field can't be empty in rank queries");
}
SchemaField schemaField = req.getSchema().getFieldOrNull(field);
if (schemaField == null) {
throw new SyntaxError("Field \"" + this.field + "\" not found");
}
if (!(schemaField.getType() instanceof RankField)) {
throw new SyntaxError("Field \"" + this.field + "\" is not a RankField");
}
return getFeatureFieldFunction(localParams.get(FUNCTION))
.createQuery(field, localParams);
}
private FeatureFieldFunction getFeatureFieldFunction(String function) throws SyntaxError {
FeatureFieldFunction f = null;
if (function == null || function.isEmpty()) {
f = DEFAULT_FUNCTION;
} else {
try {
f = FeatureFieldFunction.valueOf(function.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException iae) {
throw new SyntaxError("Unknown function in rank query: \"" + function + "\"");
}
}
return f;
}
}
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
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.
-->
<schema name="rank_fields" version="1.6">
<fieldType name="string" class="solr.StrField" />
<fieldType name="rank" class="solr.RankField" />
<fields>
<field name="id" type="string" />
<field name="str_field" type="string" />
<field name="rank_1" type="rank" />
<field name="rank_2" type="rank" />
</fields>
</schema>

View File

@ -41,6 +41,7 @@
<fieldType name="tlong" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="8" positionIncrementGap="0"/>
<fieldType name="tdouble" class="${solr.tests.DoubleFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="8" positionIncrementGap="0"/>
<fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml" multiValued="false"/>
<fieldType name="rank" class="solr.RankField"/>
<!-- Field type demonstrating an Analyzer failure -->
<fieldType name="failtype1" class="solr.TextField">
@ -614,6 +615,8 @@
<dynamicField name="ignored_*" type="ignored" multiValued="true"/>
<dynamicField name="attr_*" type="text" indexed="true" stored="true" multiValued="true"/>
<dynamicField name="rank_*" type="rank"/>
<dynamicField name="random_*" type="random"/>
<dynamicField name="*_dpf" type="delimited_payloads_float" indexed="true" stored="true"/>

View File

@ -0,0 +1,285 @@
/*
* 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.schema;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.xml.xpath.XPathConstants;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.util.TestHarness;
import org.junit.BeforeClass;
import org.junit.Ignore;
public class RankFieldTest extends SolrTestCaseJ4 {
private static final String RANK_1 = "rank_1";
private static final String RANK_2 = "rank_2";
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-minimal.xml","schema-rank-fields.xml");
}
@Override
public void setUp() throws Exception {
clearIndex();
assertU(commit());
super.setUp();
}
public void testInternalFieldName() {
assertEquals("RankField.INTERNAL_RANK_FIELD_NAME changed in an incompatible way",
"_rank_", RankField.INTERNAL_RANK_FIELD_NAME);
}
public void testBasic() {
assertNotNull(h.getCore().getLatestSchema().getFieldOrNull(RANK_1));
assertEquals(RankField.class, h.getCore().getLatestSchema().getField(RANK_1).getType().getClass());
}
public void testBadFormat() {
ignoreException("Expecting float");
assertFailedU(adoc(
"id", "1",
RANK_1, "foo"
));
assertFailedU(adoc(
"id", "1",
RANK_1, "1.2.3"
));
unIgnoreException("Expecting float");
ignoreException("must be finite");
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(Float.POSITIVE_INFINITY)
));
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(Float.NEGATIVE_INFINITY)
));
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(Float.NaN)
));
unIgnoreException("must be finite");
ignoreException("must be a positive");
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(-0.0f)
));
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(-1f)
));
assertFailedU(adoc(
"id", "1",
RANK_1, Float.toString(0.0f)
));
unIgnoreException("must be a positive");
}
public void testAddRandom() {
for (int i = 0 ; i < random().nextInt(TEST_NIGHTLY ? 10000 : 100); i++) {
assertU(adoc(
"id", String.valueOf(i),
RANK_1, Float.toString(random().nextFloat())
));
}
assertU(commit());
}
public void testSkipEmpty() {
assertU(adoc(
"id", "1",
RANK_1, ""
));
}
public void testBasicAdd() throws IOException {
assertU(adoc(
"id", "testBasicAdd",
RANK_1, "1"
));
assertU(commit());
//assert that the document made it in
assertQ(req("q", "id:testBasicAdd"), "//*[@numFound='1']");
h.getCore().withSearcher((searcher) -> {
LeafReader reader = searcher.getIndexReader().getContext().leaves().get(0).reader();
// assert that the field made it in
assertNotNull(reader.getFieldInfos().fieldInfo(RankField.INTERNAL_RANK_FIELD_NAME));
// assert that the feature made it in
assertTrue(reader.terms(RankField.INTERNAL_RANK_FIELD_NAME).iterator().seekExact(new BytesRef(RANK_1.getBytes(StandardCharsets.UTF_8))));
return null;
});
}
public void testMultipleRankFields() throws IOException {
assertU(adoc(
"id", "testMultiValueAdd",
RANK_1, "1",
RANK_2, "2"
));
assertU(commit());
//assert that the document made it in
assertQ(req("q", "id:testMultiValueAdd"), "//*[@numFound='1']");
h.getCore().withSearcher((searcher) -> {
LeafReader reader = searcher.getIndexReader().getContext().leaves().get(0).reader();
// assert that the field made it in
assertNotNull(reader.getFieldInfos().fieldInfo(RankField.INTERNAL_RANK_FIELD_NAME));
// assert that the features made it in
assertTrue(reader.terms(RankField.INTERNAL_RANK_FIELD_NAME).iterator().seekExact(new BytesRef(RANK_2.getBytes(StandardCharsets.UTF_8))));
assertTrue(reader.terms(RankField.INTERNAL_RANK_FIELD_NAME).iterator().seekExact(new BytesRef(RANK_1.getBytes(StandardCharsets.UTF_8))));
return null;
});
}
public void testSortFails() throws IOException {
assertU(adoc(
"id", "testSortFails",
RANK_1, "1"
));
assertU(commit());
assertQEx("Can't sort on rank field", req(
"q", "id:testSortFails",
"sort", RANK_1 + " desc"), 400);
}
@Ignore("We currently don't fail these kinds of requests with other field types")
public void testFacetFails() throws IOException {
assertU(adoc(
"id", "testFacetFails",
RANK_1, "1"
));
assertU(commit());
assertQEx("Can't facet on rank field", req(
"q", "id:testFacetFails",
"facet", "true",
"facet.field", RANK_1), 400);
}
public void testTermQuery() throws IOException {
assertU(adoc(
"id", "testTermQuery",
RANK_1, "1",
RANK_2, "1"
));
assertU(adoc(
"id", "testTermQuery2",
RANK_1, "1"
));
assertU(commit());
assertQ(req("q", RANK_1 + ":*"), "//*[@numFound='2']");
assertQ(req("q", RANK_1 + ":[* TO *]"), "//*[@numFound='2']");
assertQ(req("q", RANK_2 + ":*"), "//*[@numFound='1']");
assertQ(req("q", RANK_2 + ":[* TO *]"), "//*[@numFound='1']");
assertQEx("Term queries not supported", req("q", RANK_1 + ":1"), 400);
assertQEx("Range queries not supported", req("q", RANK_1 + ":[1 TO 10]"), 400);
}
public void testResponseQuery() throws IOException {
assertU(adoc(
"id", "testResponseQuery",
RANK_1, "1"
));
assertU(commit());
// Ignore requests to retrieve rank
assertQ(req("q", RANK_1 + ":*",
"fl", "id," + RANK_1),
"//*[@numFound='1']",
"count(//result/doc[1]/str)=1");
}
public void testRankQParserQuery() throws IOException {
assertU(adoc(
"id", "1",
"str_field", "foo",
RANK_1, "1",
RANK_2, "2"
));
assertU(adoc(
"id", "2",
"str_field", "foo",
RANK_1, "2",
RANK_2, "1"
));
assertU(commit());
assertQ(req("q", "str_field:foo _query_:{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}"),
"//*[@numFound='2']",
"//result/doc[1]/str[@name='id'][.='2']",
"//result/doc[2]/str[@name='id'][.='1']");
assertQ(req("q", "str_field:foo _query_:{!rank f='" + RANK_2 + "' function='log' scalingFactor='1'}"),
"//*[@numFound='2']",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='2']");
assertQ(req("q", "foo",
"defType", "dismax",
"qf", "str_field^10",
"bq", "{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}"
),
"//*[@numFound='2']",
"//result/doc[1]/str[@name='id'][.='2']",
"//result/doc[2]/str[@name='id'][.='1']");
assertQ(req("q", "foo",
"defType", "dismax",
"qf", "str_field^10",
"bq", "{!rank f='" + RANK_2 + "' function='log' scalingFactor='1'}"
),
"//*[@numFound='2']",
"//result/doc[1]/str[@name='id'][.='1']",
"//result/doc[2]/str[@name='id'][.='2']");
}
public void testScoreChanges() throws Exception {
assertU(adoc(
"id", "1",
"str_field", "foo",
RANK_1, "1"
));
assertU(commit());
ModifiableSolrParams params = params("q", "foo",
"defType", "dismax",
"qf", "str_field^10",
"fl", "id,score",
"wt", "xml");
double scoreBefore = (Double) TestHarness.evaluateXPath(h.query(req(params)), "//result/doc[1]/float[@name='score']", XPathConstants.NUMBER);
params.add("bq", "{!rank f='" + RANK_1 + "' function='log' scalingFactor='1'}");
double scoreAfter = (Double) TestHarness.evaluateXPath(h.query(req(params)), "//result/doc[1]/float[@name='score']", XPathConstants.NUMBER);
assertNotEquals("Expecting score to change", scoreBefore, scoreAfter, 0f);
}
}

View File

@ -357,6 +357,18 @@ public class QueryEqualityTest extends SolrTestCaseJ4 {
}
}
public void testRankQuery() throws Exception {
SolrQueryRequest req = req("df", "foo_s");
try {
assertQueryEquals("rank", req,
"{!rank f='rank_1'}",
"{!rank f='rank_1' function='satu'}",
"{!rank f='rank_1' function='satu' weight=1}");
} finally {
req.close();
}
}
public void testQueryNested() throws Exception {
SolrQueryRequest req = req("df", "foo_s");
try {

View File

@ -0,0 +1,258 @@
/*
* 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;
import static org.apache.solr.search.RankQParserPlugin.EXPONENT;
import static org.apache.solr.search.RankQParserPlugin.FIELD;
import static org.apache.solr.search.RankQParserPlugin.FUNCTION;
import static org.apache.solr.search.RankQParserPlugin.NAME;
import static org.apache.solr.search.RankQParserPlugin.PIVOT;
import static org.apache.solr.search.RankQParserPlugin.SCALING_FACTOR;
import static org.apache.solr.search.RankQParserPlugin.WEIGHT;
import java.io.IOException;
import org.apache.lucene.search.Query;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.RankField;
import org.apache.solr.search.RankQParserPlugin.RankQParser;
import org.hamcrest.CoreMatchers;
import org.junit.BeforeClass;
public class RankQParserPluginTest extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-minimal.xml", "schema-rank-fields.xml");
}
public void testParamCompatibility() {
assertEquals("RankQParserPlugin.NAME changed in an incompatible way", "rank", NAME);
assertEquals("RankQParserPlugin.FIELD changed in an incompatible way", "f", FIELD);
assertEquals("RankQParserPlugin.FUNCTION changed in an incompatible way", "function", FUNCTION);
assertEquals("RankQParserPlugin.PIVOT changed in an incompatible way", "pivot", PIVOT);
assertEquals("RankQParserPlugin.SCALING_FACTOR changed in an incompatible way", "scalingFactor", SCALING_FACTOR);
assertEquals("RankQParserPlugin.WEIGHT changed in an incompatible way", "weight", WEIGHT);
assertEquals("RankQParserPlugin.EXPONENT changed in an incompatible way", "exponent", EXPONENT);
}
public void testCreateParser() throws IOException {
try (RankQParserPlugin rankQPPlugin = new RankQParserPlugin()) {
QParser parser = rankQPPlugin.createParser("", new ModifiableSolrParams(), null, req());
assertNotNull(parser);
assertTrue(parser instanceof RankQParser);
}
}
public void testSyntaxErrors() throws IOException, SyntaxError {
assertSyntaxError("No Field", "Field can't be empty", () ->
getRankQParser(new ModifiableSolrParams(), null, req()).parse());
assertSyntaxError("Field empty", "Field can't be empty", () ->
getRankQParser(
params(FIELD, ""), null, req()).parse());
assertSyntaxError("Field doesn't exist", "Field \"foo\" not found", () ->
getRankQParser(
params(FIELD, "foo"), null, req()).parse());
assertSyntaxError("ID is not a feature field", "Field \"id\" is not a RankField", () ->
getRankQParser(
params(FIELD, "id"), null, req()).parse());
}
public void testBadLogParameters() throws IOException, SyntaxError {
assertSyntaxError("Expecting bad weight", "weight must be in", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "log",
WEIGHT, "0"), null, req()).parse());
assertSyntaxError("Expecting bad scaling factor", "scalingFactor must be", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "log",
SCALING_FACTOR, "0"), null, req()).parse());
}
public void testBadSaturationParameters() throws IOException, SyntaxError {
assertSyntaxError("Expecting a pivot value", "A pivot value", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "satu",
WEIGHT, "2"), null, req()).parse());
assertSyntaxError("Expecting bad weight", "weight must be in", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "satu",
PIVOT, "1",
WEIGHT, "-1"), null, req()).parse());
}
public void testBadSigmoidParameters() throws IOException, SyntaxError {
assertSyntaxError("Expecting missing pivot", "A pivot value", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "sigm",
EXPONENT, "1"), null, req()).parse());
assertSyntaxError("Expecting missing exponent", "An exponent value", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "1"), null, req()).parse());
assertSyntaxError("Expecting bad weight", "weight must be in", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "1",
EXPONENT, "1",
WEIGHT, "-1"), null, req()).parse());
assertSyntaxError("Expecting bad pivot", "pivot must be", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "0",
EXPONENT, "1"), null, req()).parse());
assertSyntaxError("Expecting bad exponent", "exp must be", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "1",
EXPONENT, "0"), null, req()).parse());
}
public void testUnknownFunction() throws IOException, SyntaxError {
assertSyntaxError("Expecting bad function", "Unknown function in rank query: \"foo\"", () ->
getRankQParser(
params(FIELD, "rank_1",
FUNCTION, "foo"), null, req()).parse());
}
public void testParseLog() throws IOException, SyntaxError {
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedLogToString(1), 1),
params(FIELD, "rank_1",
FUNCTION, "log",
SCALING_FACTOR, "1",
WEIGHT, "1"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedLogToString(2.5f), 1),
params(FIELD, "rank_1",
FUNCTION, "log",
SCALING_FACTOR, "2.5",
WEIGHT, "1"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedLogToString(1), 2.5f),
params(FIELD, "rank_1",
FUNCTION, "log",
SCALING_FACTOR, "1",
WEIGHT, "2.5"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedLogToString(1), 2.5f),
params(FIELD, "rank_1",
FUNCTION, "Log", //use different case
SCALING_FACTOR, "1",
WEIGHT, "2.5"));
}
public void testParseSigm() throws IOException, SyntaxError {
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSigmoidToString(1.5f, 2f), 1),
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "1.5",
EXPONENT, "2",
WEIGHT, "1"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSigmoidToString(1.5f, 2f), 2),
params(FIELD, "rank_1",
FUNCTION, "sigm",
PIVOT, "1.5",
EXPONENT, "2",
WEIGHT, "2"));
}
public void testParseSatu() throws IOException, SyntaxError {
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 1),
params(FIELD, "rank_1",
FUNCTION, "satu",
PIVOT, "1.5",
WEIGHT, "1"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 2),
params(FIELD, "rank_1",
FUNCTION, "satu",
PIVOT, "1.5",
WEIGHT, "2"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
params(FIELD, "rank_1",
FUNCTION, "satu"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
params(FIELD, "rank_1",
FUNCTION, "satu",
WEIGHT, "1"));
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(1.5f), 1),
params(FIELD, "rank_1",
FUNCTION, "satu",
PIVOT, "1.5"));
}
public void testParseDefault() throws IOException, SyntaxError {
assertValidRankQuery(expectedFeatureQueryToString("rank_1", expectedSaturationToString(null), 1),
params(FIELD, "rank_1"));
}
private void assertValidRankQuery(String expctedToString, SolrParams localParams) throws IOException, SyntaxError {
QParser parser = getRankQParser(localParams, null, req());
Query q = parser.parse();
assertNotNull(q);
assertThat(q.toString(), CoreMatchers.equalTo(expctedToString));
}
private String expectedFeatureQueryToString(String fieldName, String function, float boost) {
String featureQueryStr = "FeatureQuery(field=" + RankField.INTERNAL_RANK_FIELD_NAME + ", feature=" + fieldName + ", function=" + function + ")";
if (boost == 1f) {
return featureQueryStr;
}
return "(" + featureQueryStr + ")^" + boost;
}
private String expectedLogToString(float scalingFactor) {
return "LogFunction(scalingFactor=" + scalingFactor + ")";
}
private String expectedSigmoidToString(float pivot, float exp) {
return "SigmoidFunction(pivot=" + pivot + ", a=" + exp + ")";
}
private String expectedSaturationToString(Float pivot) {
return "SaturationFunction(pivot=" + pivot + ")";
}
private void assertSyntaxError(String assertionMsg, String expectedExceptionMsg, ThrowingRunnable runnable) {
SyntaxError se = expectThrows(SyntaxError.class, assertionMsg, runnable);
assertThat(se.getMessage(), CoreMatchers.containsString(expectedExceptionMsg));
}
private RankQParser getRankQParser(SolrParams localParams, SolrParams params, SolrQueryRequest req) throws IOException {
try (RankQParserPlugin rankQPPlugin = new RankQParserPlugin()) {
return (RankQParser) rankQPPlugin.createParser("", localParams, params, req);
}
}
}

View File

@ -251,6 +251,13 @@
<!--Binary data type. The data should be sent/retrieved in as Base64 encoded Strings -->
<fieldType name="binary" class="solr.BinaryField"/>
<!--
RankFields can be used to store scoring factors to improve document ranking. They should be used
in combination with RankQParserPlugin.
(experimental)
-->
<fieldType name="rank" class="solr.RankField"/>
<!-- solr.TextField allows the specification of custom text analyzers
specified as a tokenizer and a list of token filters. Different
analyzers may be specified for indexing and querying.