f
, the field (required)
+ * func
, payload function (min, max, or average; required)
+ * includeSpanScore
, multiple payload function result by similarity score or not (default: false)
+ * {!payload_score f=weighted_terms_dpf}Foo Bar
creates a SpanNearQuery with "Foo" followed by "Bar"
+ */
+public class PayloadScoreQParserPlugin extends QParserPlugin {
+ public static final String NAME = "payload_score";
+
+ @Override
+ public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
+ return new QParser(qstr, localParams, params, req) {
+ @Override
+ public Query parse() throws SyntaxError {
+ String field = localParams.get(QueryParsing.F);
+ String value = localParams.get(QueryParsing.V);
+ String func = localParams.get("func");
+ boolean includeSpanScore = localParams.getBool("includeSpanScore", false);
+
+ if (field == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "'f' not specified");
+ }
+
+ if (value == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "query string missing");
+ }
+
+ FieldType ft = req.getCore().getLatestSchema().getFieldType(field);
+ Analyzer analyzer = ft.getQueryAnalyzer();
+ SpanQuery query = PayloadUtils.createSpanQuery(field, value, analyzer);
+
+ if (query == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "SpanQuery is null");
+ }
+
+ // note: this query(/parser) does not support func=first; 'first' is a payload() value source feature only
+ PayloadFunction payloadFunction = PayloadUtils.getPayloadFunction(func);
+ if (payloadFunction == null) throw new SyntaxError("Unknown payload function: " + func);
+
+ return new PayloadScoreQuery(query, payloadFunction, includeSpanScore);
+ }
+ };
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
index 872c618afaa..d8dc29f0722 100644
--- a/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
+++ b/solr/core/src/java/org/apache/solr/search/QParserPlugin.java
@@ -80,6 +80,8 @@ public abstract class QParserPlugin implements NamedListInitializedPlugin, SolrI
map.put(IGainTermsQParserPlugin.NAME, IGainTermsQParserPlugin.class);
map.put(TextLogisticRegressionQParserPlugin.NAME, TextLogisticRegressionQParserPlugin.class);
map.put(SignificantTermsQParserPlugin.NAME, SignificantTermsQParserPlugin.class);
+ map.put(PayloadScoreQParserPlugin.NAME, PayloadScoreQParserPlugin.class);
+ map.put(PayloadCheckQParserPlugin.NAME, PayloadCheckQParserPlugin.class);
standardPlugins = Collections.unmodifiableMap(map);
}
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 f268baa0359..6032e0d5629 100644
--- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
+++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
@@ -34,6 +34,7 @@ import org.apache.lucene.queries.function.docvalues.BoolDocValues;
import org.apache.lucene.queries.function.docvalues.DoubleDocValues;
import org.apache.lucene.queries.function.docvalues.LongDocValues;
import org.apache.lucene.queries.function.valuesource.*;
+import org.apache.lucene.queries.payloads.PayloadFunction;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
@@ -76,6 +77,8 @@ import org.apache.solr.search.function.distance.StringDistanceFunction;
import org.apache.solr.search.function.distance.VectorDistanceFunction;
import org.apache.solr.search.join.ChildFieldValueSourceParser;
import org.apache.solr.util.DateMathParser;
+import org.apache.solr.util.PayloadDecoder;
+import org.apache.solr.util.PayloadUtils;
import org.apache.solr.util.plugin.NamedListInitializedPlugin;
import org.locationtech.spatial4j.distance.DistanceUtils;
@@ -706,6 +709,47 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin {
}
});
+ addParser("payload", new ValueSourceParser() {
+ @Override
+ public ValueSource parse(FunctionQParser fp) throws SyntaxError {
+ // payload(field,value[,default, ['min|max|average|first']])
+ // defaults to "average" and 0.0 default value
+
+ TInfo tinfo = parseTerm(fp); // would have made this parser a new separate class and registered it, but this handy method is private :/
+
+ ValueSource defaultValueSource;
+ if (fp.hasMoreArguments()) {
+ defaultValueSource = fp.parseValueSource();
+ } else {
+ defaultValueSource = new ConstValueSource(0.0f);
+ }
+
+ PayloadFunction payloadFunction = null;
+ String func = "average";
+ if (fp.hasMoreArguments()) {
+ func = fp.parseArg();
+ }
+ payloadFunction = PayloadUtils.getPayloadFunction(func);
+
+ // Support func="first" by payloadFunction=null
+ if(payloadFunction == null && !"first".equals(func)) {
+ // not "first" (or average, min, or max)
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid payload function: " + func);
+ }
+
+ FieldType fieldType = fp.getReq().getCore().getLatestSchema().getFieldTypeNoEx(tinfo.field);
+ PayloadDecoder decoder = PayloadUtils.getPayloadDecoder(fieldType);
+ return new FloatPayloadValueSource(
+ tinfo.field,
+ tinfo.val,
+ tinfo.indexedField,
+ tinfo.indexedBytes.get(),
+ decoder,
+ payloadFunction,
+ defaultValueSource);
+ }
+ });
+
addParser("true", new ValueSourceParser() {
@Override
public ValueSource parse(FunctionQParser fp) {
@@ -1348,7 +1392,7 @@ abstract class Double2Parser extends NamedParser {
final FunctionValues aVals = a.getValues(context, readerContext);
final FunctionValues bVals = b.getValues(context, readerContext);
return new DoubleDocValues(this) {
- @Override
+ @Override
public double doubleVal(int doc) throws IOException {
return func(doc, aVals, bVals);
}
@@ -1419,7 +1463,7 @@ class BoolConstValueSource extends ConstNumberSource {
return this.constant == other.constant;
}
- @Override
+ @Override
public int getInt() {
return constant ? 1 : 0;
}
@@ -1491,4 +1535,4 @@ class TestValueSource extends ValueSource {
public SortField getSortField(boolean reverse) {
return super.getSortField(reverse);
}
-}
+}
\ No newline at end of file
diff --git a/solr/core/src/java/org/apache/solr/search/similarities/PayloadScoringSimilarityWrapper.java b/solr/core/src/java/org/apache/solr/search/similarities/PayloadScoringSimilarityWrapper.java
new file mode 100644
index 00000000000..4a86c42a7cb
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/similarities/PayloadScoringSimilarityWrapper.java
@@ -0,0 +1,80 @@
+/*
+ * 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.similarities;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.FieldInvertState;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.CollectionStatistics;
+import org.apache.lucene.search.TermStatistics;
+import org.apache.lucene.search.similarities.Similarity;
+import org.apache.lucene.util.BytesRef;
+import org.apache.solr.util.PayloadDecoder;
+
+/**
+ * The computation Lucene's PayloadScoreQuery uses is SimScorer#computePayloadFactor.
+ * This wrapper delegates to a main similarity except for this one method.
+ */
+public class PayloadScoringSimilarityWrapper extends Similarity {
+ private Similarity delegate;
+ private PayloadDecoder decoder;
+
+ public PayloadScoringSimilarityWrapper(Similarity delegate, PayloadDecoder decoder) {
+ this.delegate = delegate;
+ this.decoder = decoder;
+ }
+
+ @Override
+ public String toString() {
+ return "PayloadScoring(" + delegate.toString() + ", decoder=" + decoder.toString() + ")";
+ }
+
+ @Override
+ public long computeNorm(FieldInvertState state) {
+ return delegate.computeNorm(state);
+ }
+
+ @Override
+ public SimWeight computeWeight(float boost, CollectionStatistics collectionStats, TermStatistics... termStats) {
+ return delegate.computeWeight(boost, collectionStats, termStats);
+ }
+
+ @Override
+ public SimScorer simScorer(SimWeight weight, LeafReaderContext context) throws IOException {
+ final SimScorer simScorer = delegate.simScorer(weight,context);
+ SimScorer payloadSimScorer = new SimScorer() {
+ @Override
+ public float score(int doc, float freq) throws IOException {
+ return simScorer.score(doc,freq);
+ }
+
+ @Override
+ public float computeSlopFactor(int distance) {
+ return simScorer.computeSlopFactor(distance);
+ }
+
+ @Override
+ public float computePayloadFactor(int doc, int start, int end, BytesRef payload) {
+ return decoder.decode(simScorer, doc, start, end, payload);
+ }
+ };
+
+ return payloadSimScorer;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/search/similarities/SchemaSimilarityFactory.java b/solr/core/src/java/org/apache/solr/search/similarities/SchemaSimilarityFactory.java
index a71de189d5a..3c942d25b1b 100644
--- a/solr/core/src/java/org/apache/solr/search/similarities/SchemaSimilarityFactory.java
+++ b/solr/core/src/java/org/apache/solr/search/similarities/SchemaSimilarityFactory.java
@@ -16,6 +16,8 @@
*/
package org.apache.solr.search.similarities;
+import java.util.HashMap;
+
import org.apache.lucene.search.similarities.ClassicSimilarity;
import org.apache.lucene.search.similarities.BM25Similarity;
import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper;
@@ -28,12 +30,14 @@ import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.SimilarityFactory;
+import org.apache.solr.util.PayloadDecoder;
+import org.apache.solr.util.PayloadUtils;
import org.apache.solr.util.plugin.SolrCoreAware;
/**
*
* SimilarityFactory
that returns a global {@link PerFieldSimilarityWrapper}
- * that delegates to the field type, if it's configured. For field type's that
+ * that delegates to the field type, if it's configured. For field types that
* do not have a Similarity
explicitly configured, the global Similarity
* will use per fieldtype defaults -- either based on an explicitly configured
* defaultSimFromFieldType
a sensible default depending on the {@link Version}
@@ -45,7 +49,7 @@ import org.apache.solr.util.plugin.SolrCoreAware;
*
*
* The defaultSimFromFieldType
option accepts the name of any fieldtype, and uses
- * whatever Similarity
is explicitly configured for that fieldType as thedefault for
+ * whatever Similarity
is explicitly configured for that fieldType as the default for
* all other field types. For example:
*
@@ -136,6 +140,7 @@ public class SchemaSimilarityFactory extends SimilarityFactory implements SolrCo private class SchemaSimilarity extends PerFieldSimilarityWrapper { private Similarity defaultSimilarity; + private HashMapdecoders; // cache to avoid scanning token filters repeatedly, unnecessarily public SchemaSimilarity(Similarity defaultSimilarity) { this.defaultSimilarity = defaultSimilarity; @@ -148,7 +153,19 @@ public class SchemaSimilarityFactory extends SimilarityFactory implements SolrCo return defaultSimilarity; } else { Similarity similarity = fieldType.getSimilarity(); - return similarity == null ? defaultSimilarity : similarity; + similarity = similarity == null ? defaultSimilarity : similarity; + + // Payload score handling: if field type has index-time payload encoding, wrap and computePayloadFactor accordingly + if (decoders == null) decoders = new HashMap<>(); + PayloadDecoder decoder; + if (!decoders.containsKey(fieldType)) { + decoders.put(fieldType, PayloadUtils.getPayloadDecoder(fieldType)); + } + decoder = decoders.get(fieldType); + + if (decoder != null) similarity = new PayloadScoringSimilarityWrapper(similarity, decoder); + + return similarity; } } diff --git a/solr/core/src/java/org/apache/solr/util/PayloadDecoder.java b/solr/core/src/java/org/apache/solr/util/PayloadDecoder.java new file mode 100644 index 00000000000..f9495b102fc --- /dev/null +++ b/solr/core/src/java/org/apache/solr/util/PayloadDecoder.java @@ -0,0 +1,28 @@ +/* + * 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.util; + +import org.apache.lucene.search.similarities.Similarity; +import org.apache.lucene.util.BytesRef; + +/** + * Mirrors SimScorer#computePayloadFactor's signature + */ +public interface PayloadDecoder { + float decode(Similarity.SimScorer simScorer, int doc, int start, int end, BytesRef payload); +} diff --git a/solr/core/src/java/org/apache/solr/util/PayloadUtils.java b/solr/core/src/java/org/apache/solr/util/PayloadUtils.java new file mode 100644 index 00000000000..79275945da0 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/util/PayloadUtils.java @@ -0,0 +1,134 @@ +/* + * 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.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.payloads.DelimitedPayloadTokenFilterFactory; +import org.apache.lucene.analysis.payloads.NumericPayloadTokenFilterFactory; +import org.apache.lucene.analysis.payloads.PayloadHelper; +import org.apache.lucene.analysis.tokenattributes.TermToBytesRefAttribute; +import org.apache.lucene.analysis.util.TokenFilterFactory; +import org.apache.lucene.index.Term; +import org.apache.lucene.queries.payloads.AveragePayloadFunction; +import org.apache.lucene.queries.payloads.MaxPayloadFunction; +import org.apache.lucene.queries.payloads.MinPayloadFunction; +import org.apache.lucene.queries.payloads.PayloadFunction; +import org.apache.lucene.search.similarities.Similarity; +import org.apache.lucene.search.spans.SpanNearQuery; +import org.apache.lucene.search.spans.SpanQuery; +import org.apache.lucene.search.spans.SpanTermQuery; +import org.apache.lucene.util.BytesRef; +import org.apache.solr.analysis.TokenizerChain; +import org.apache.solr.common.SolrException; +import org.apache.solr.schema.FieldType; + +public class PayloadUtils { + public static String getPayloadEncoder(FieldType fieldType) { + // TODO: support custom payload encoding fields too somehow - maybe someone has a custom component that encodes payloads as floats + String encoder = null; + Analyzer a = fieldType.getIndexAnalyzer(); + if (a instanceof TokenizerChain) { + // examine the indexing analysis chain for DelimitedPayloadTokenFilterFactory or NumericPayloadTokenFilterFactory + TokenizerChain tc = (TokenizerChain)a; + TokenFilterFactory[] factories = tc.getTokenFilterFactories(); + for (TokenFilterFactory factory : factories) { + if (factory instanceof DelimitedPayloadTokenFilterFactory) { + encoder = factory.getOriginalArgs().get(DelimitedPayloadTokenFilterFactory.ENCODER_ATTR); + break; + } + + if (factory instanceof NumericPayloadTokenFilterFactory) { + // encodes using `PayloadHelper.encodeFloat(payload)` + encoder = "float"; + break; + } + } + } + + return encoder; + } + + public static PayloadDecoder getPayloadDecoder(FieldType fieldType) { + PayloadDecoder decoder = Similarity.SimScorer::computePayloadFactor; // default to SimScorer's + + String encoder = getPayloadEncoder(fieldType); + + if ("integer".equals(encoder)) { + decoder = (Similarity.SimScorer simScorer, int doc, int start, int end, BytesRef payload) -> PayloadHelper.decodeInt(payload.bytes, payload.offset); + } + if ("float".equals(encoder)) { + decoder = (Similarity.SimScorer simScorer, int doc, int start, int end, BytesRef payload) -> PayloadHelper.decodeFloat(payload.bytes, payload.offset); + } + // encoder could be "identity" at this point, in the case of DelimitedTokenFilterFactory encoder="identity" + + // TODO: support pluggable payload decoders? + + return decoder; + } + + public static PayloadFunction getPayloadFunction(String func) { + PayloadFunction payloadFunction = null; + if ("min".equals(func)) { + payloadFunction = new MinPayloadFunction(); + } + if ("max".equals(func)) { + payloadFunction = new MaxPayloadFunction(); + } + if ("average".equals(func)) { + payloadFunction = new AveragePayloadFunction(); + } + + return payloadFunction; + } + + public static SpanQuery createSpanQuery(String field, String value, Analyzer analyzer) { + SpanQuery query; + try { + // adapted this from QueryBuilder.createSpanQuery (which isn't currently public) and added reset(), end(), and close() calls + TokenStream in = analyzer.tokenStream(field, value); + in.reset(); + + TermToBytesRefAttribute termAtt = in.getAttribute(TermToBytesRefAttribute.class); + + List terms = new ArrayList<>(); + while (in.incrementToken()) { + terms.add(new SpanTermQuery(new Term(field, termAtt.getBytesRef()))); + } + in.end(); + in.close(); + + if (terms.isEmpty()) { + query = null; + } else if (terms.size() == 1) { + query = terms.get(0); + } else { + query = new SpanNearQuery(terms.toArray(new SpanTermQuery[terms.size()]), 0, true); + } + } catch (IOException e) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); + } + return query; + } + + +} diff --git a/solr/core/src/test-files/solr/collection1/conf/schema11.xml b/solr/core/src/test-files/solr/collection1/conf/schema11.xml index cccf79a8dca..179f804d8ad 100644 --- a/solr/core/src/test-files/solr/collection1/conf/schema11.xml +++ b/solr/core/src/test-files/solr/collection1/conf/schema11.xml @@ -459,11 +459,33 @@ valued. --> unknown fields indexed and/or stored by default --> + + + + + + ++ ++ + + ++ ++ + + - -+ ++ + id + +id text diff --git a/solr/core/src/test-files/solr/collection1/conf/schema15.xml b/solr/core/src/test-files/solr/collection1/conf/schema15.xml index 57c6bf10535..9b4f0b11572 100644 --- a/solr/core/src/test-files/solr/collection1/conf/schema15.xml +++ b/solr/core/src/test-files/solr/collection1/conf/schema15.xml @@ -618,6 +618,14 @@+ + + ++ ++ + text id diff --git a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java index c44cc1ec64b..7745817fae6 100644 --- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java +++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java @@ -20,6 +20,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import junit.framework.AssertionFailedError; import org.apache.lucene.search.Query; import org.apache.lucene.search.QueryUtils; import org.apache.solr.SolrTestCaseJ4; @@ -1131,4 +1132,45 @@ public class QueryEqualityTest extends SolrTestCaseJ4 { req.close(); } } -} + + public void testPayloadScoreQuery() throws Exception { + // I don't see a precedent to test query inequality in here, so doing a `try` + // There was a bug with PayloadScoreQuery's .equals() method that said two queries were equal with different includeSpanScore settings + + try { + assertQueryEquals + ("payload_score" + , "{!payload_score f=foo_dpf v=query func=min includeSpanScore=false}" + , "{!payload_score f=foo_dpf v=query func=min includeSpanScore=true}" + ); + fail("queries should not have been equal"); + } catch(AssertionFailedError e) { + assertTrue("queries were not equal, as expected", true); + } + } + + public void testPayloadCheckQuery() throws Exception { + try { + assertQueryEquals + ("payload_check" + , "{!payload_check f=foo_dpf payloads=2}one" + , "{!payload_check f=foo_dpf payloads=2}two" + ); + fail("queries should not have been equal"); + } catch(AssertionFailedError e) { + assertTrue("queries were not equal, as expected", true); + } + } + + public void testPayloadFunction() throws Exception { + SolrQueryRequest req = req("myField","bar_f"); + + try { + assertFuncEquals(req, + "payload(foo_dpf,some_term)", + "payload(foo_dpf,some_term)"); + } finally { + req.close(); + } + } +} \ No newline at end of file diff --git a/solr/core/src/test/org/apache/solr/search/TestPayloadCheckQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestPayloadCheckQParserPlugin.java new file mode 100644 index 00000000000..14bd833e804 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/TestPayloadCheckQParserPlugin.java @@ -0,0 +1,73 @@ +/* + * 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 org.apache.solr.SolrTestCaseJ4; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestPayloadCheckQParserPlugin extends SolrTestCaseJ4 { + @BeforeClass + public static void beforeClass() throws Exception { + initCore("solrconfig.xml", "schema11.xml"); + createIndex(); + } + + public static void createIndex() { + assertU(adoc("id","1", "vals_dpi","A|1 B|2 C|3")); + assertU(adoc("id","2", "vals_dpf","one|1.0 two|2.0 three|3.0")); + assertU(adoc("id","3", "vals_dps","the|ARTICLE cat|NOUN jumped|VERB")); + assertU(commit()); + } + + @Test + public void test() { + clearIndex(); + + String[] should_matches = new String[] { + "{!payload_check f=vals_dpi v=A payloads=1}", + "{!payload_check f=vals_dpi v=B payloads=2}", + "{!payload_check f=vals_dpi v=C payloads=3}", + "{!payload_check f=vals_dpi payloads='1 2'}A B", + // "{!payload_check f=vals_dpi payloads='1 2.0'}A B", // ideally this should pass, but IntegerEncoder can't handle "2.0" + "{!payload_check f=vals_dpi payloads='1 2 3'}A B C", + + "{!payload_check f=vals_dpf payloads='1 2'}one two", + "{!payload_check f=vals_dpf payloads='1 2.0'}one two", // shows that FloatEncoder can handle "1" + + "{!payload_check f=vals_dps payloads='NOUN VERB'}cat jumped" + }; + + String[] should_not_matches = new String[] { + "{!payload_check f=vals_dpi v=A payloads=2}", + "{!payload_check f=vals_dpi payloads='1 2'}B C", + "{!payload_check f=vals_dpi payloads='1 2 3'}A B", + "{!payload_check f=vals_dpi payloads='1 2'}A B C", + "{!payload_check f=vals_dpf payloads='1 2.0'}two three", + "{!payload_check f=vals_dps payloads='VERB NOUN'}cat jumped" + }; + + for(String should_match : should_matches) { + assertQ(should_match, req("fl","*,score", "q", should_match), "//result[@numFound='1']"); + } + + for(String should_not_match : should_not_matches) { + assertQ(should_not_match, req("fl","*,score", "q", should_not_match), "//result[@numFound='0']"); + } + } +} diff --git a/solr/core/src/test/org/apache/solr/search/TestPayloadScoreQParserPlugin.java b/solr/core/src/test/org/apache/solr/search/TestPayloadScoreQParserPlugin.java new file mode 100644 index 00000000000..34017c15b76 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/TestPayloadScoreQParserPlugin.java @@ -0,0 +1,54 @@ +/* + * 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 org.apache.solr.SolrTestCaseJ4; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestPayloadScoreQParserPlugin extends SolrTestCaseJ4 { + @BeforeClass + public static void beforeClass() throws Exception { + initCore("solrconfig.xml", "schema11.xml"); + createIndex(); + } + + public static void createIndex() { + assertU(adoc("id","1", "vals_dpf","A|1.0 B|2.0 C|3.0 mult|50 mult|100 x|22 x|37 x|19")); + assertU(commit()); + } + + @Test + public void test() { + clearIndex(); + + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=B func=min}"), "//float[@name='score']='2.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=mult func=min}"), "//float[@name='score']='50.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=mult func=max}"), "//float[@name='score']='100.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=mult func=average}"), "//float[@name='score']='75.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf func=min}A B"), "//float[@name='score']='1.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf func=min}B C"), "//float[@name='score']='2.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf func=max}B C"), "//float[@name='score']='3.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf func=average}B C"), "//float[@name='score']='2.5'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf func=max}A B C"), "//float[@name='score']='3.0'"); + + // TODO: fix this includeSpanScore test to be less brittle - score result is score of "A" (via BM25) multipled by 1.0 (payload value) + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=A func=min}"), "//float[@name='score']='1.0'"); + assertQ(req("fl","*,score", "q", "{!payload_score f=vals_dpf v=A func=min includeSpanScore=true}"), "//float[@name='score']='0.25811607'"); + } +} diff --git a/solr/core/src/test/org/apache/solr/search/function/TestFunctionQuery.java b/solr/core/src/test/org/apache/solr/search/function/TestFunctionQuery.java index 92f28639353..f218bd902e2 100644 --- a/solr/core/src/test/org/apache/solr/search/function/TestFunctionQuery.java +++ b/solr/core/src/test/org/apache/solr/search/function/TestFunctionQuery.java @@ -461,6 +461,34 @@ public class TestFunctionQuery extends SolrTestCaseJ4 { assertQ(req("fl","*,score","q", "{!func}sttf(a_t)", "fq","id:6"), "//float[@name='score']='11.0'"); } + @Test + public void testPayloadFunction() { + clearIndex(); + + assertU(adoc("id","1", "vals_dp","A|1.0 B|2.0 C|3.0 mult|50 mult|100 x|22 x|37 x|19", "default_f", "42.0")); + assertU(commit()); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,A)"), "//float[@name='score']='1.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,B)"), "//float[@name='score']='2.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,C,0)"), "//float[@name='score']='3.0'"); + + // Test defaults, constant, field, and function value sources + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,D,37.0)"), "//float[@name='score']='37.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,E,default_f)"), "//float[@name='score']='42.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,E,mul(2,default_f))"), "//float[@name='score']='84.0'"); + + // Test PayloadFunction's for multiple terms, average being the default + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,mult,0.0,min)"), "//float[@name='score']='50.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,mult,0.0,max)"), "//float[@name='score']='100.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,mult,0.0,average)"), "//float[@name='score']='75.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,mult)"), "//float[@name='score']='75.0'"); + + // Test special "first" function, by checking the other functions with same term too + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,x,0.0,min)"), "//float[@name='score']='19.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,x,0.0,max)"), "//float[@name='score']='37.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,x,0.0,average)"), "//float[@name='score']='26.0'"); + assertQ(req("fl","*,score","q", "{!func}payload(vals_dp,x,0.0,first)"), "//float[@name='score']='22.0'"); + } + @Test public void testSortByFunc() throws Exception { assertU(adoc("id", "1", "const_s", "xx", @@ -772,7 +800,7 @@ public class TestFunctionQuery extends SolrTestCaseJ4 { assertU(commit()); // it's important that these functions not only use fields that - // out doc have no values for, but also that that no other doc ever added + // our doc has no values for, but also that no other doc ever added // to the index might have ever had a value for, so that the segment // term metadata doesn't exist diff --git a/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema b/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema index 68f686760b1..80a58fbe312 100644 --- a/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema +++ b/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema @@ -177,6 +177,11 @@+ + + + + @@ -600,6 +605,26 @@ + + + ++ ++ + + ++ ++ + + ++ ++ +