diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index dbd1109de08..9433c94bf6c 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -17,6 +17,8 @@ New Features * SOLR-14414: Introduce a new, experimental Admin UI that can be used aside the current Admin UI. The new module is loaded together with the existing Admin UI and available under the URL path /solr/compose. (Christos Malliaridis) +* SOLR-15030: Add score() function that returns the score of the current hit. The function can be used in pseudo-fields and post-filters. (Andrey Bozhko) + Improvements --------------------- diff --git a/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java b/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java index 636212bc185..81db54c14d9 100644 --- a/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java +++ b/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java @@ -25,6 +25,7 @@ import org.apache.lucene.internal.hppc.IntObjectHashMap; import org.apache.lucene.queries.function.FunctionValues; import org.apache.lucene.queries.function.ValueSource; +import org.apache.lucene.search.Scorable; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrException; import org.apache.solr.response.ResultContext; @@ -119,8 +120,13 @@ public void transform(SolrDocument doc, int docid, DocIterationInfo docIteration try { int idx = ReaderUtil.subIndex(docid, readerContexts); LeafReaderContext rcontext = readerContexts.get(idx); - FunctionValues values = valueSource.getValues(fcontext, rcontext); int localId = docid - rcontext.docBase; + + if (context.wantsScores()) { + fcontext.put("scorer", new ScoreAndDoc(localId, docIterationInfo.score())); + } + + FunctionValues values = valueSource.getValues(fcontext, rcontext); setValue(doc, values.objectVal(localId)); } catch (IOException e) { throw new SolrException( @@ -142,4 +148,24 @@ protected void setValue(SolrDocument doc, Object val) { doc.setField(name, val); } } + + private static class ScoreAndDoc extends Scorable { + private final int docId; + private final float score; + + ScoreAndDoc(int docId, float score) { + this.docId = docId; + this.score = score; + } + + @Override + public int docID() { + return docId; + } + + @Override + public float score() { + return score; + } + } } diff --git a/solr/core/src/java/org/apache/solr/search/FunctionQParser.java b/solr/core/src/java/org/apache/solr/search/FunctionQParser.java index 5f5bcf17b7a..cbd91da567a 100644 --- a/solr/core/src/java/org/apache/solr/search/FunctionQParser.java +++ b/solr/core/src/java/org/apache/solr/search/FunctionQParser.java @@ -34,6 +34,7 @@ import org.apache.solr.schema.SchemaField; import org.apache.solr.search.facet.AggValueSource; import org.apache.solr.search.function.FieldNameValueSource; +import org.apache.solr.search.function.ScoreFunction; /** * Does "function query" parsing of function-call like strings, producing a {@link ValueSource}. As @@ -515,6 +516,8 @@ protected ValueSource parseValueSource(int flags) throws SyntaxError { valueSource = ValueSourceParser.BoolConstValueSource.TRUE; } else if ("false".equals(id)) { valueSource = ValueSourceParser.BoolConstValueSource.FALSE; + } else if ("score".equals(id)) { + valueSource = ScoreFunction.INSTANCE; } else { if ((flags & FLAG_USE_FIELDNAME_SOURCE) != 0) { // Don't try to create a ValueSource for the field, just use a placeholder. diff --git a/solr/core/src/java/org/apache/solr/search/FunctionRangeQuery.java b/solr/core/src/java/org/apache/solr/search/FunctionRangeQuery.java index db71c7d6589..efe76dc521c 100644 --- a/solr/core/src/java/org/apache/solr/search/FunctionRangeQuery.java +++ b/solr/core/src/java/org/apache/solr/search/FunctionRangeQuery.java @@ -25,6 +25,7 @@ import org.apache.lucene.queries.function.ValueSourceScorer; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.QueryVisitor; +import org.apache.lucene.search.Scorable; import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Weight; import org.apache.solr.common.SolrException; @@ -68,6 +69,14 @@ class FunctionRangeCollector extends DelegatingCollector { public FunctionRangeCollector(Map fcontext, Weight weight) { this.fcontext = fcontext; this.weight = weight; + // It would make sense to put this.scorer into the context, + // so that the context can be consumed in the call to + // doSetNextReader. But this.scorer is null, and will only be + // set at a later point by DelegatingCollector#setScorer + // (which happens *after* the call to doSetNextReader). + // So instead of using this.scorer directly, we wrap this collector + // as scorable - to work around the late initialization of this.scorer. + this.fcontext.put("scorer", new ScorableView(this)); } @Override @@ -113,4 +122,26 @@ public boolean equals(Object obj) { public int hashCode() { return 31 * classHash() + rangeFilt.hashCode(); } + + private static class ScorableView extends Scorable { + private final DelegatingCollector collector; + + ScorableView(DelegatingCollector collector) { + this.collector = collector; + } + + @Override + public float score() throws IOException { + assert collector.scorer != null + : "scorer must first be set via DelegatingCollector#setScorer"; + return collector.scorer.score(); + } + + @Override + public int docID() { + assert collector.scorer != null + : "scorer must first be set via DelegatingCollector#setScorer"; + return collector.scorer.docID(); + } + } } diff --git a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java index 939cf9b778c..2c0d9e756a7 100644 --- a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java +++ b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java @@ -59,7 +59,7 @@ public class SolrReturnFields extends ReturnFields { // Field names that are OK to include in the response. // This will include pseudo fields, lucene fields, and matching globs - private Set okFieldNames = new HashSet<>(); + private final Set okFieldNames = new HashSet<>(); // The list of explicitly requested fields // Order is important for CSVResponseWriter @@ -515,6 +515,12 @@ private void addField( String disp = (key == null) ? field : key; augmenters.addTransformer(new ScoreAugmenter(disp)); scoreDependentFields.put(disp, disp.equals(SCORE) ? "" : SCORE); + } else if (key != null && isPseudoField) { + // SOLR-15030: a pseudo-field based on the function query may need scores, + // so we consider all pseudo-fields as potentially requiring scores. + // At the same time, we don't set _wantScore = true because the field + // list must explicitly include 'score' field to enable scores + scoreDependentFields.put(key, ""); } } @@ -576,7 +582,7 @@ public boolean wantsScore() { @Override public Map getScoreDependentReturnFields() { - return scoreDependentFields; + return _wantsScore ? scoreDependentFields : Map.of(); } @Override diff --git a/solr/core/src/java/org/apache/solr/search/function/ScoreFunction.java b/solr/core/src/java/org/apache/solr/search/function/ScoreFunction.java new file mode 100644 index 00000000000..64e57321f2a --- /dev/null +++ b/solr/core/src/java/org/apache/solr/search/function/ScoreFunction.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.function; + +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.FloatDocValues; +import org.apache.lucene.search.Scorable; +import org.apache.solr.common.SolrException; + +/** Returns the score of the current hit. */ +public final class ScoreFunction extends ValueSource { + + public static final ScoreFunction INSTANCE = new ScoreFunction(); + + @Override + public FunctionValues getValues(Map context, LeafReaderContext readerContext) + throws IOException { + Scorable scorer = (Scorable) context.get("scorer"); + if (scorer == null) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, "score function cannot access the document scores"); + } + + return new FloatDocValues(this) { + @Override + public float floatVal(int doc) throws IOException { + assert scorer.docID() == doc + : "Expected scorer to be positioned on doc: " + doc + ", but was: " + scorer.docID(); + return scorer.score(); + } + + @Override + public boolean exists(int doc) { + assert scorer.docID() == doc + : "Expected scorer to be positioned on doc: " + doc + ", but was: " + scorer.docID(); + return true; + } + }; + } + + @Override + public boolean equals(Object o) { + return o instanceof ScoreFunction; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String description() { + return "score"; + } +} 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 8df761740ae..6f6bd95fcbc 100644 --- a/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java +++ b/solr/core/src/test/org/apache/solr/search/QueryEqualityTest.java @@ -858,6 +858,10 @@ public void testFuncCscore() throws Exception { assertFuncEquals("cscore()", "cscore( )"); } + public void testFuncScore() throws Exception { + assertFuncEquals("score", "score "); + } + public void testFuncTop() throws Exception { assertFuncEquals("top(sum(3,foo_i))"); } diff --git a/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionDistribTest.java b/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionDistribTest.java new file mode 100644 index 00000000000..5be71e8b3ba --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionDistribTest.java @@ -0,0 +1,194 @@ +package org.apache.solr.search.function; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.UpdateRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.SolrException; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ScoreFunctionDistribTest extends SolrCloudTestCase { + + private static final String COLLECTION = "coll1"; + + @BeforeClass + public static void configureSolr() throws Exception { + configureCluster(2).addConfig("config1", configset("cloud-minimal")).configure(); + + prepareCollection(); + } + + private static void prepareCollection() throws Exception { + var client = cluster.getSolrClient(); + var createResponse = + CollectionAdminRequest.createCollection(COLLECTION, "config1", 2, 1).process(client); + assertEquals(0, createResponse.getStatus()); + + cluster.waitForActiveCollection(COLLECTION, 10, TimeUnit.SECONDS); + + var updateResponse = + new UpdateRequest() + .add( + List.of( + sdoc("id", "1", "text_s", "foo"), + sdoc("id", "2", "text_s", "bar"), + sdoc("id", "3", "text_s", "qux"), + sdoc("id", "4", "text_s", "asd"), + sdoc("id", "101", "text_s", "random text"), + sdoc("id", "102", "text_s", "random text"), + sdoc("id", "103", "text_s", "random text"), + sdoc("id", "104", "text_s", "random text"), + sdoc("id", "105", "text_s", "random text"), + sdoc("id", "106", "text_s", "random text"), + sdoc("id", "107", "text_s", "random text"), + sdoc("id", "108", "text_s", "random text"), + sdoc("id", "109", "text_s", "random text"))) + .commit(client, COLLECTION); + assertEquals(0, updateResponse.getStatus()); + } + + @Test + public void testScoreFunction_boostQuery() throws Exception { + var resp = + cluster + .getSolrClient() + .query( + COLLECTION, + params( + "q", + "{!boost b=if(lte(score,2),2.5,1)}foo^=1 bar^=2 qux^=3 asd^=4", + "df", + "text_s", + "fl", + "id,score")); + + assertJSONEquals( + """ + { + "numFound": 4, + "start": 0, + "maxScore": 5.0, + "numFoundExact": true, + "docs": [ + { + "id": "2", + "score": 5.0 + }, + { + "id": "4", + "score": 4.0 + }, + { + "id": "3", + "score": 3.0 + }, + { + "id": "1", + "score": 2.5 + } + ] + }""", + resp.getResults().jsonStr()); + } + + @Test + public void testScoreFunction_postFilter() throws Exception { + var resp = + cluster + .getSolrClient() + .query( + COLLECTION, + params( + "q", + "foo^=1 bar^=2 qux^=3 asd^=4", + "df", + "text_s", + "fl", + "id,score", + "fq", + "{!frange l=2 u=3 cache=false}score")); + + assertJSONEquals( + """ + { + "numFound": 2, + "start": 0, + "maxScore": 3.0, + "numFoundExact": true, + "docs": [ + { + "id": "3", + "score": 3.0 + }, + { + "id": "2", + "score": 2.0 + } + ] + }""", + resp.getResults().jsonStr()); + } + + @Test + public void testScoreFunction_pseudoField() throws Exception { + var resp = + cluster + .getSolrClient() + .query( + COLLECTION, + params( + "q", + "foo^=1 bar^=2 qux^=3", + "df", + "text_s", + "preFetchDocs", + "0", // TODO FIXME handle pre-fetching introduced in SOLR-17775 + "fl", + "id,score,custom:add(1,score,score)")); + + assertJSONEquals( + """ + { + "numFound": 3, + "start": 0, + "maxScore": 3.0, + "numFoundExact": true, + "docs": [ + { + "id": "3", + "score": 3.0, + "custom": 7.0 + }, + { + "id": "2", + "score": 2.0, + "custom": 5.0 + }, + { + "id": "1", + "score": 1.0, + "custom": 3.0 + } + ] + }""", + resp.getResults().jsonStr()); + + // error if scores not enabled + assertThrows( + SolrException.class, + () -> + cluster + .getSolrClient() + .query( + COLLECTION, + params( + "q", + "foo^=1 bar^=2 qux^=3", + "df", + "text_s", + "fl", + "id,custom:add(1,score,score)"))); + } +} diff --git a/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionTest.java b/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionTest.java new file mode 100644 index 00000000000..9691b321fdf --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/function/ScoreFunctionTest.java @@ -0,0 +1,307 @@ +/* + * 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 static org.hamcrest.Matchers.isA; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.response.transform.DocTransformers; +import org.apache.solr.response.transform.RenameFieldTransformer; +import org.apache.solr.response.transform.ScoreAugmenter; +import org.apache.solr.response.transform.ValueSourceAugmenter; +import org.junit.BeforeClass; +import org.junit.Test; + +public class ScoreFunctionTest extends SolrTestCaseJ4 { + + @BeforeClass + public static void beforeClass() throws Exception { + initCore("solrconfig-functionquery.xml", "schema11.xml"); + + addDocsInRandomOrder(); + } + + private static void addDocsInRandomOrder() { + // add documents to the collection, but randomize + // the number of segments, and which specific docs + // are added to each segment - this is especially + // useful for testing score function in pseudo fields + List updates = + Arrays.asList( + commit(), + commit(), + commit(), + adoc("id", "1", "text", "foo"), + adoc("id", "2", "text", "bar"), + adoc("id", "3", "text", "qux"), + adoc("id", "4", "text", "asd"), + adoc("id", "101", "text", "random text"), + adoc("id", "102", "text", "random text"), + adoc("id", "103", "text", "random text"), + adoc("id", "104", "text", "random text"), + adoc("id", "105", "text", "random text"), + adoc("id", "106", "text", "random text"), + adoc("id", "107", "text", "random text"), + adoc("id", "108", "text", "random text"), + adoc("id", "109", "text", "random text")); + + Collections.shuffle(updates, random()); + + for (var update : updates) { + assertU(update); + } + + assertU(commit()); + } + + @Test + public void testScoreFunction_boostQuery() throws Exception { + assertJQ( + req("q", "foo^=1 bar^=2 qux^=3 asd^=4", "df", "text", "fl", "id,score"), + "/response/numFound==4", + "/response/docs/[0]/id=='4'", + "/response/docs/[0]/score==4.0", + "/response/docs/[1]/id=='3'", + "/response/docs/[1]/score==3.0", + "/response/docs/[2]/id=='2'", + "/response/docs/[2]/score==2.0", + "/response/docs/[3]/id=='1'", + "/response/docs/[3]/score==1.0"); + + // boost function that relies on score + assertJQ( + req( + "q", + "{!boost b=if(lte(score,2),2.5,1)}foo^=1 bar^=2 qux^=3 asd^=4", + "df", + "text", + "fl", + "id,score"), + "/response/numFound==4", + "/response/docs/[0]/id=='2'", + "/response/docs/[0]/score==5.0", + "/response/docs/[1]/id=='4'", + "/response/docs/[1]/score==4.0", + "/response/docs/[2]/id=='3'", + "/response/docs/[2]/score==3.0", + "/response/docs/[3]/id=='1'", + "/response/docs/[3]/score==2.5"); + } + + @Test + public void testScoreFunction_postFilter() throws Exception { + // frange query as postfilter + assertJQ( + req( + "q", + "foo^=1 bar^=2 qux^=3 asd^=4", + "df", + "text", + "fl", + "id,score", + "fq", + "{!frange l=2 u=3 cache=false}score"), + "/response/numFound==2", + "/response/docs/[0]/id=='3'", + "/response/docs/[0]/score==3.0", + "/response/docs/[1]/id=='2'", + "/response/docs/[1]/score==2.0"); + + // doesn't work if not a postfilter + assertThrows( + SolrException.class, + () -> + assertJQ( + req( + "q", + "foo^=1 bar^=2 qux^=3 asd^=4", + "df", + "text", + "fl", + "id,score", + "fq", + "{!frange l=2 u=3}score"))); + } + + @Test + public void testScoreFunction_pseudoField() throws Exception { + assertJQ( + req( + "q", "foo^=1 bar^=2 qux^=3", + "df", "text", + "preFetchDocs", "0", // TODO FIXME handle pre-fetching introduced in SOLR-17775 + "fl", "id,score,custom:add(1,score,score)"), + "/response/numFound==3", + "/response/docs/[0]/id=='3'", + "/response/docs/[0]/score==3.0", + "/response/docs/[0]/custom==7.0", + "/response/docs/[1]/id=='2'", + "/response/docs/[1]/score==2.0", + "/response/docs/[1]/custom==5.0", + "/response/docs/[2]/id=='1'", + "/response/docs/[2]/score==1.0", + "/response/docs/[2]/custom==3.0"); + + // error if scores not enabled + assertThrows( + SolrException.class, + () -> + assertJQ( + req( + "q", "foo^=1 bar^=2 qux^=3", + "df", "text", + "fl", "id,custom:add(1,score,score)"))); + } + + @Test + public void testScoreFunction_nested() throws Exception { + assertJQ( + req( + "json", + """ + { + "query": { + "boost": { + "b": "if(lte(score,6),1,0.1)", + "query": { + "boost": { + "b": "if(gte(score,3),2,1)", + "query": "foo^=1 bar^=2 qux^=3 asd^=4" + } + } + } + } + }""", + "df", "text", + "fl", "id,score"), + "/response/numFound==4", + "/response/docs/[0]/id=='3'", + "/response/docs/[0]/score==6.0", + "/response/docs/[1]/id=='2'", + "/response/docs/[1]/score==2.0", + "/response/docs/[2]/id=='1'", + "/response/docs/[2]/score==1.0", + "/response/docs/[3]/id=='4'", + "/response/docs/[3]/score==0.8"); + + assertJQ( + req( + "json", + """ + { + "query": { + "boost": { + "b": "if(gte(score,10),0.1,1)", + "query": { + "boost": { + "b": "if(eq(query($q1),score),100,1)", + "query": "foo^=1 bar^=2 qux^=3 asd^=4" + } + } + } + }, + "queries": { + "q1": { + "boost": { + "b": "if(eq(score,3),1,2)", + "query": "qux^=3 asd^=4" + } + } + } + }""", + "df", "text", + "fl", "id,score"), + "/response/numFound==4", + "/response/docs/[0]/id=='3'", + "/response/docs/[0]/score==30.0", + "/response/docs/[1]/id=='4'", + "/response/docs/[1]/score==4.0", + "/response/docs/[2]/id=='2'", + "/response/docs/[2]/score==2.0", + "/response/docs/[3]/id=='1'", + "/response/docs/[3]/score==1.0"); + } + + @Test + public void testScoreFunction_combined() throws Exception { + assertJQ( + req( + "q", "{!boost b=if(gte(score,3),2,1) v=$qq}", + "qq", "foo^=1 bar^=2 qux^=3 asd^=4", + "fq", "{!frange cache=false u=7}score", + "df", "text", + "preFetchDocs", "0", // TODO FIXME handle pre-fetching introduced in SOLR-17775 + "fl", "id,score,score_plus_one:add(1,score)"), + "/response/numFound==3", + "/response/docs/[0]/id=='3'", + "/response/docs/[0]/score==6.0", + "/response/docs/[0]/score_plus_one==7.0", + "/response/docs/[1]/id=='2'", + "/response/docs/[1]/score==2.0", + "/response/docs/[1]/score_plus_one==3.0", + "/response/docs/[2]/id=='1'", + "/response/docs/[2]/score==1.0", + "/response/docs/[2]/score_plus_one==2.0"); + } + + @Test + public void testScoreFunction_renameFieldAndValueSource() throws Exception { + // The pseudo-field definition 'renamed_score:score' can be + // parsed as either the rename field or the value source. + // Solr attempts to parse rename fields first, + // so 'renamed_score' is treated as a rename field. + var request = + req( + "q", "asd^=4", + "df", "text", + "fl", "id,score,renamed_score:score,score_plus_one:add(1,score)"); + + try (request) { + var response = h.queryAndResponse("/select", request); + var transformer = (DocTransformers) response.getReturnFields().getTransformer(); + + assertEquals(4, transformer.size()); + + // score + assertThat(transformer.getTransformer(0), isA(ScoreAugmenter.class)); + // renamed_score + assertThat(transformer.getTransformer(1), isA(ScoreAugmenter.class)); + // score_plus_one + assertThat(transformer.getTransformer(2), isA(ValueSourceAugmenter.class)); + // renamed_score + assertThat(transformer.getTransformer(3), isA(RenameFieldTransformer.class)); + } + } + + // @Ignore // TODO would like this to work someday + @Test + public void testSortFunction_sort() throws Exception { + // TODO incorporate a docValues value, and some assertions on the order + assertQEx( + "unsupported", + "score function cannot access the document scores", + req( + "q", "*:*", + "sort", "div(1,score) desc"), + ErrorCode.BAD_REQUEST); + } +} diff --git a/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc b/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc index 7c6f1a9d0ea..b28ef48066f 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc @@ -447,6 +447,17 @@ In these cases, an appropriate `map()` function could be used as a workaround to * `scale(x, minTarget, maxTarget)` * `scale(x,1,2)`: scales the values of x such that all values will be between 1 and 2 inclusive. +=== score Function +Returns the score of the current document. + +The function can only be used in a few specific ways - see examples below for what is currently supported. + +*Syntax Examples* + +* `fl=*,score,score_plus_one:add(1,score)`: a xref:common-query-parameters.adoc#functions-with-fl[pseudo-field] that computes the field value based on the document score, +* `fq={!frange cache=false l=4}score`: a xref:common-query-parameters.adoc#cache-local-parameter[post filter] that filters the hits based on their score, +* `q={!boost b=if(gte(score,10),2,1)}solrrocks`: a boost query that determines the boost factor from the document score produced by the underlying query. + === sqedist Function The Square Euclidean distance calculates the 2-norm (Euclidean distance) but does not take the square root, thus saving a fairly expensive operation. It is often the case that applications that care about Euclidean distance do not need the actual distance, but instead can use the square of the distance.