Skip to content

[SOLR-15030] Add score() function that returns the score of the current hit #3340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -142,4 +148,24 @@ protected void setValue(SolrDocument doc, Object val) {
doc.setField(name, val);
}
}

private static class ScoreAndDoc extends Scorable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are three other ScoreAndDoc helper classes (with identical implementation) scattered throughout the codebase. Maybe it's worth consolidating into one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One top level class makes sense!

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions solr/core/src/java/org/apache/solr/search/FunctionRangeQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,6 +69,14 @@ class FunctionRangeCollector extends DelegatingCollector {
public FunctionRangeCollector(Map<Object, Object> 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
Expand Down Expand Up @@ -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();
}
}
}
10 changes: 8 additions & 2 deletions solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> okFieldNames = new HashSet<>();
private final Set<String> okFieldNames = new HashSet<>();

// The list of explicitly requested fields
// Order is important for CSVResponseWriter
Expand Down Expand Up @@ -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
Comment on lines +521 to +522
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused or challenge what you say in this sentence. A DocTransformer could want the score. Granted, a DocTransformer doesn't have a wantsScore / needsScore so... :-/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nor does a ValueSource have needsScores. But DoubleValuesSource (Lucene's modern replacement over legacy VS) does.

Addressing that is perhaps for another issue/PR. Progress not perfection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that the work-around to DocTransformer.needsScores not existing is that the user should explicitly add score to fl?

Lucene standardizes on needsScores terminology. Solr ReturnFields & ResultContext has "wants" terminology.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I thought was that for the query with fl=id,a,b,s:sum(x,y) - even though it has a pseudo-field, that alone is not enough to conclude that the query requires scores.

scoreDependentFields.put(key, "");
}
}

Expand Down Expand Up @@ -576,7 +582,7 @@ public boolean wantsScore() {

@Override
public Map<String, String> getScoreDependentReturnFields() {
return scoreDependentFields;
return _wantsScore ? scoreDependentFields : Map.of();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object, Object> 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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))");
}
Expand Down
Loading
Loading