blob: 15a007bb584632286098f557cfa1642213a074a0 [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.ltr.feature;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.ltr.FeatureLoggerTestUtils;
import org.apache.solr.ltr.TestRerankBase;
import org.apache.solr.ltr.feature.FieldValueFeature.FieldValueFeatureWeight.DefaultValueFieldValueFeatureScorer;
import org.apache.solr.ltr.feature.FieldValueFeature.FieldValueFeatureWeight.FieldValueFeatureScorer;
import org.apache.solr.ltr.feature.FieldValueFeature.FieldValueFeatureWeight.NumericDocValuesFieldValueFeatureScorer;
import org.apache.solr.ltr.feature.FieldValueFeature.FieldValueFeatureWeight.SortedDocValuesFieldValueFeatureScorer;
import org.apache.solr.ltr.model.LinearModel;
import org.apache.solr.request.SolrQueryRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TestFieldValueFeature extends TestRerankBase {
private static final float FIELD_VALUE_FEATURE_DEFAULT_VAL = 0.0f;
private static final String[] FIELDS = {
"popularity",
"dvIntPopularity", "dvLongPopularity",
"dvFloatPopularity", "dvDoublePopularity",
"dvStringPopularity",
"isTrendy",
"dvIsTrendy"
};
@Before
public void before() throws Exception {
setuptest(false);
assertU(adoc("id", "1", "popularity", "1", "title", "w1",
"dvStringPopularity", "1",
"description", "w1", "isTrendy", "true"));
assertU(adoc("id", "2", "popularity", "2", "title", "w2 2asd asdd didid",
"dvStringPopularity", "2",
"description", "w2 2asd asdd didid"));
assertU(adoc("id", "3", "popularity", "3", "title", "w3",
"dvStringPopularity", "3",
"description", "w3", "isTrendy", "true"));
assertU(adoc("id", "4", "popularity", "4", "title", "w4",
"dvStringPopularity", "4",
"description", "w4", "isTrendy", "false"));
assertU(adoc("id", "5", "popularity", "5", "title", "w5",
"dvStringPopularity", "5",
"description", "w5", "isTrendy", "true"));
assertU(adoc("id", "6", "popularity", "6", "title", "w1 w2",
"dvStringPopularity", "6",
"description", "w1 w2", "isTrendy", "false"));
assertU(adoc("id", "7", "popularity", "7", "title", "w1 w2 w3 w4 w5",
"dvStringPopularity", "7",
"description", "w1 w2 w3 w4 w5 w8", "isTrendy", "true"));
assertU(adoc("id", "8", "popularity", "8", "title", "w1 w1 w1 w2 w2 w8",
"dvStringPopularity", "8",
"description", "w1 w1 w1 w2 w2", "isTrendy", "false"));
// a document without the popularity and the dv fields
assertU(adoc("id", "42", "title", "NO popularity or isTrendy", "description", "NO popularity or isTrendy"));
assertU(commit());
for (String field : FIELDS) {
loadFeature(field, FieldValueFeature.class.getName(),
"{\"field\":\"" + field + "\"}");
}
loadModel("model", LinearModel.class.getName(), FIELDS,
"{\"weights\":{\"popularity\":1.0,\"dvIntPopularity\":1.0,\"dvLongPopularity\":1.0," +
"\"dvFloatPopularity\":1.0,\"dvDoublePopularity\":1.0," +
"\"dvStringPopularity\":1.0,\"isTrendy\":1.0,\"dvIsTrendy\":1.0}}");
}
@After
public void after() throws Exception {
aftertest();
}
@Test
public void testRanking() throws Exception {
final SolrQuery query = new SolrQuery();
query.setQuery("title:w1");
query.add("fl", "*, score");
query.add("rows", "4");
// Normal term match
assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
query.add("rq", "{!ltr model=model reRankDocs=4}");
assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='1'");
query.setQuery("*:*");
query.remove("rows");
query.add("rows", "8");
query.remove("rq");
query.add("rq", "{!ltr model=model reRankDocs=8}");
assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='5'");
}
@Test
public void testIfADocumentDoesntHaveAFieldDefaultValueIsReturned() throws Exception {
SolrQuery query = new SolrQuery();
query.setQuery("id:42");
query.add("fl", "*, score");
query.add("rows", "4");
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='42'");
query = new SolrQuery();
query.setQuery("id:42");
query.add("rq", "{!ltr model=model reRankDocs=4}");
query.add("fl", "[fv]");
// "0.0" in the assertJQ below is more readable than
// Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL) but first make sure it's equivalent
assertEquals("0.0", Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL));
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'popularity=0.0,dvIntPopularity=0.0,dvLongPopularity=0.0," +
"dvFloatPopularity=0.0,dvDoublePopularity=0.0," +
"dvStringPopularity=0.0,isTrendy=0.0,dvIsTrendy=0.0'}");
}
@Test
public void testIfADocumentDoesntHaveAFieldASetDefaultValueIsReturned() throws Exception {
for (String field : FIELDS) {
final String fstore = "testIfADocumentDoesntHaveAFieldASetDefaultValueIsReturned"+field;
loadFeature(field+"42", FieldValueFeature.class.getName(), fstore,
"{\"field\":\""+field+"\",\"defaultValue\":\"42.0\"}");
SolrQuery query = new SolrQuery();
query.setQuery("id:42");
query.add("fl", "*, score");
query.add("rows", "4");
loadModel(field+"-model42", LinearModel.class.getName(),
new String[] {field+"42"}, fstore, "{\"weights\":{\""+field+"42\":1.0}}");
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='42'");
query = new SolrQuery();
query.setQuery("id:42");
query.add("rq", "{!ltr model="+field+"-model42 reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector(field+"42","42.0")+"'}");
}
}
@Test
public void testIfADocumentDoesntHaveAFieldTheDefaultValueFromSchemaIsReturned() throws Exception {
final String[] fieldsWithDefaultValues = {"dvIntField", "dvLongField", "dvFloatField"};
final String[] defaultValues = {"-1.0", "-2.0", "-3.0"};
for (int idx = 0; idx < fieldsWithDefaultValues.length; ++idx) {
final String field = fieldsWithDefaultValues[idx];
final String defaultValue = defaultValues[idx];
final String fstore = "testIfADocumentDoesntHaveAFieldTheDefaultValueFromSchemaIsReturned"+field;
assertU(adoc("id", "21"));
assertU(commit());
loadFeature(field, FieldValueFeature.class.getName(), fstore,
"{\"field\":\""+field+"\"}");
loadModel(field+"-model", LinearModel.class.getName(),
new String[] {field}, fstore, "{\"weights\":{\"" + field + "\":1.0}}");
final SolrQuery query = new SolrQuery("id:21");
query.add("rq", "{!ltr model="+field+"-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector(field, defaultValue)+"'}");
}
}
@Test
public void testThatFieldValueFeatureScorerIsUsedAndDefaultIsReturned() throws Exception {
// this tests the case that we create a feature for a non-existent field
// using a different fstore to avoid a clash with the other tests
final String fstore = "testThatFieldValueFeatureScorerIsUsedAndDefaultIsReturned";
loadFeature("not-existing-field", ObservingFieldValueFeature.class.getName(), fstore,
"{\"field\":\"cowabunga\"}");
loadModel("not-existing-field-model", LinearModel.class.getName(),
new String[] {"not-existing-field"}, fstore, "{\"weights\":{\"not-existing-field\":1.0}}");
final SolrQuery query = new SolrQuery();
query.setQuery("id:42");
query.add("rq", "{!ltr model=not-existing-field-model reRankDocs=4}");
query.add("fl", "[fv]");
ObservingFieldValueFeature.usedScorerClass = null; // to clear away any previous test's use
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector("not-existing-field",Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}");
assertEquals(FieldValueFeatureScorer.class.getName(), ObservingFieldValueFeature.usedScorerClass);
}
@Test
public void testThatDefaultFieldValueScorerIsUsedAndDefaultIsReturned() throws Exception {
final String[] fieldsWithoutDefaultValues = {"dvDoubleField", "dvStrBoolField"};
// this tests the case that no document contains docValues for the provided existing field
for (String field : fieldsWithoutDefaultValues) {
final String fstore = "testThatDefaultFieldValueScorerIsUsedAndDefaultIsReturned"+field;
loadFeature(field, ObservingFieldValueFeature.class.getName(), fstore,
"{\"field\":\""+field+"\"}");
loadModel(field+"-model", LinearModel.class.getName(),
new String[] {field}, fstore, "{\"weights\":{\""+field+"\":1.0}}");
final SolrQuery query = new SolrQuery("id:42");
query.add("rq", "{!ltr model="+field+"-model reRankDocs=4}");
query.add("fl", "[fv]");
ObservingFieldValueFeature.usedScorerClass = null; // to clear away any previous test's use
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils
.toFeatureVector(field,Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}");
assertEquals(DefaultValueFieldValueFeatureScorer.class.getName(), ObservingFieldValueFeature.usedScorerClass);
}
}
@Test
public void testBooleanValue() throws Exception {
final String fstore = "test_boolean_store";
loadFeature("trendy", FieldValueFeature.class.getName(), fstore,
"{\"field\":\"isTrendy\"}");
loadModel("trendy-model", LinearModel.class.getName(),
new String[] {"trendy"}, fstore, "{\"weights\":{\"trendy\":1.0}}");
SolrQuery query = new SolrQuery();
query.setQuery("id:4");
query.add("rq", "{!ltr model=trendy-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector("trendy","0.0")+"'}");
query = new SolrQuery();
query.setQuery("id:5");
query.add("rq", "{!ltr model=trendy-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector("trendy","1.0")+"'}");
// check default value is false
query = new SolrQuery();
query.setQuery("id:2");
query.add("rq", "{!ltr model=trendy-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector("trendy","0.0")+"'}");
}
@Test
public void testThatExceptionIsThrownForUnsupportedType() throws Exception {
final String fstore = "test_store";
assertU(adoc("id", "21", "title", "multivalued not supported", "dvStringPopularities", "wow value"));
assertU(commit());
loadFeature("dvStringPopularities", FieldValueFeature.class.getName(), fstore,
"{\"field\":\"dvStringPopularities\"}");
loadModel("dvStringPopularities-model", LinearModel.class.getName(),
new String[] {"dvStringPopularities"}, fstore, "{\"weights\":{\"dvStringPopularities\":1.0}}");
final SolrQuery query = new SolrQuery("id:21");
query.add("rq", "{!ltr model=dvStringPopularities-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(),
"/error/msg/=='java.lang.IllegalArgumentException: Doc values type SORTED_SET of field dvStringPopularities is not supported'");
}
@Test
public void testThatCorrectFieldValueFeatureIsUsedForDocValueTypes() throws Exception {
final String[][] fieldsWithDifferentTypes = {
new String[]{"dvIntPopularity", "1", NumericDocValuesFieldValueFeatureScorer.class.getName()},
new String[]{"dvStringPopularity", "T", SortedDocValuesFieldValueFeatureScorer.class.getName()},
new String[]{"noDvFloatField", "1", FieldValueFeatureScorer.class.getName()},
new String[]{"noDvStrNumField", "T", FieldValueFeatureScorer.class.getName()}
};
for (String[] fieldAndScorerClass : fieldsWithDifferentTypes) {
final String field = fieldAndScorerClass[0];
final String fieldValue = fieldAndScorerClass[1];
final String fstore = "testThatCorrectFieldValueFeatureIsUsedForDocValueTypes"+field;
assertU(adoc("id", "21", field, fieldValue));
assertU(commit());
loadFeature(field, ObservingFieldValueFeature.class.getName(), fstore,
"{\"field\":\""+field+"\"}");
loadModel(field+"-model", LinearModel.class.getName(),
new String[] {field}, fstore, "{\"weights\":{\"" + field + "\":1.0}}");
final SolrQuery query = new SolrQuery("id:21");
query.add("rq", "{!ltr model="+field+"-model reRankDocs=4}");
query.add("fl", "[fv]");
ObservingFieldValueFeature.usedScorerClass = null; // to clear away any previous test's use
assertJQ("/query" + query.toQueryString(), "/response/numFound/==1");
assertJQ("/query" + query.toQueryString(),
"/response/docs/[0]/=={'[fv]':'"+FeatureLoggerTestUtils.toFeatureVector(field, "1.0")+"'}");
assertEquals(fieldAndScorerClass[2], ObservingFieldValueFeature.usedScorerClass);
}
}
@Test
public void testParamsToMap() throws Exception {
final LinkedHashMap<String,Object> params = new LinkedHashMap<String,Object>();
params.put("field", "field"+random().nextInt(10));
doTestParamsToMap(FieldValueFeature.class.getName(), params);
}
@Test
public void testThatStringValuesAreCorrectlyParsed() throws Exception {
for (String field : new String[] {"dvStrNumField" , "noDvStrNumField"}) {
final String[][] inputsAndTests = {
new String[]{"T", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, "1.0")+"'}"},
new String[]{"F", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, "0.0")+"'}"},
new String[]{"-7324.427", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}"},
new String[]{"532", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}"},
new String[]{Float.toString(Float.NaN), "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}"},
new String[]{"notanumber", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, Float.toString(FIELD_VALUE_FEATURE_DEFAULT_VAL))+"'}"}
};
final String fstore = "testThatStringValuesAreCorrectlyParsed"+field;
loadFeature(field, FieldValueFeature.class.getName(), fstore,
"{\"field\":\"" + field + "\"}");
loadModel(field+"-model", LinearModel.class.getName(),
new String[]{field}, fstore,
"{\"weights\":{\""+field+"\":1.0}}");
for (String[] inputAndTest : inputsAndTests) {
assertU(adoc("id", "21", field, inputAndTest[0]));
assertU(commit());
final SolrQuery query = new SolrQuery("id:21");
query.add("rq", "{!ltr model=" + field + "-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(), inputAndTest[1]);
}
}
}
@Test
public void testThatDateValuesAreCorrectlyParsed() throws Exception {
for (String field : new String[] {"dvDateField", "noDvDateField"}) {
final String[][] inputsAndTests = {
new String[]{"1970-01-01T00:00:00.000Z", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, "0.0")+"'}"},
new String[]{"1970-01-01T00:00:00.001Z", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, "1.0")+"'}"},
new String[]{"1970-01-01T00:00:01.234Z", "/response/docs/[0]/=={'[fv]':'" +
FeatureLoggerTestUtils.toFeatureVector(field, "1234.0")+"'}"}
};
final String fstore = "testThatDateValuesAreCorrectlyParsed"+field;
loadFeature(field, FieldValueFeature.class.getName(), fstore,
"{\"field\":\"" + field + "\"}");
loadModel(field+"-model", LinearModel.class.getName(),
new String[]{field}, fstore,
"{\"weights\":{\""+field+"\":1.0}}");
for (String[] inputAndTest : inputsAndTests) {
assertU(adoc("id", "21", field, inputAndTest[0]));
assertU(commit());
final SolrQuery query = new SolrQuery("id:21");
query.add("rq", "{!ltr model=" + field + "-model reRankDocs=4}");
query.add("fl", "[fv]");
assertJQ("/query" + query.toQueryString(), inputAndTest[1]);
}
}
}
/**
* This class is used to track which specific FieldValueFeature is used so that we can test, whether the
* fallback mechanism works correctly.
*/
final public static class ObservingFieldValueFeature extends FieldValueFeature {
static String usedScorerClass;
public ObservingFieldValueFeature(String name, Map<String, Object> params) {
super(name, params);
}
@Override
public Feature.FeatureWeight createWeight(IndexSearcher searcher, boolean needsScores, SolrQueryRequest request,
Query originalQuery, Map<String, String[]> efi) throws IOException {
return new ObservingFieldValueFeatureWeight(searcher, request, originalQuery, efi);
}
public class ObservingFieldValueFeatureWeight extends FieldValueFeatureWeight {
public ObservingFieldValueFeatureWeight(IndexSearcher searcher, SolrQueryRequest request,
Query originalQuery, Map<String, String[]> efi) {
super(searcher, request, originalQuery, efi);
}
@Override
public FeatureScorer scorer(LeafReaderContext context) throws IOException {
FeatureScorer scorer = super.scorer(context);
usedScorerClass = scorer.getClass().getName();
return scorer;
}
}
}
}