mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-24 17:09:48 +00:00
While function scores using scripts do allow explanations, they are only creatable with an expert plugin. This commit improves the situation for the newer script score query by adding the ability to set the explanation from the script itself. To set the explanation, a user would check for `explanation != null` to indicate an explanation is needed, and then call `explanation.set("some description")`.
This commit is contained in:
parent
d371f9d44d
commit
f32692208e
@ -329,3 +329,34 @@ The `script_score` query has equivalent <<decay-functions, decay functions>>
|
||||
that can be used in script.
|
||||
|
||||
include::{es-repo-dir}/vectors/vector-functions.asciidoc[]
|
||||
|
||||
[[score-explanation]]
|
||||
====== Explain request
|
||||
Using an <<search-explain, explain request>> provides an explanation of how the parts of a score were computed. The `script_score` query can add its own explanation by setting the `explanation` parameter:
|
||||
|
||||
[source,console]
|
||||
--------------------------------------------------
|
||||
GET /twitter/_explain/0
|
||||
{
|
||||
"query" : {
|
||||
"script_score" : {
|
||||
"query" : {
|
||||
"match": { "message": "elasticsearch" }
|
||||
},
|
||||
"script" : {
|
||||
"source" : """
|
||||
long likes = doc['likes'].value;
|
||||
double normalizedLikes = likes / 10;
|
||||
if (explanation != null) {
|
||||
explanation.set('normalized likes = likes / 10 = ' + likes + ' / 10 = ' + normalizedLikes);
|
||||
}
|
||||
return normalizedLikes;
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
--------------------------------------------------
|
||||
// TEST[setup:twitter]
|
||||
|
||||
Note that the `explanation` will be null when using in a normal `_search` request, so having a conditional guard is best practice.
|
||||
|
@ -68,7 +68,7 @@ class ExpressionScoreScript implements ScoreScript.LeafFactory {
|
||||
});
|
||||
|
||||
@Override
|
||||
public double execute() {
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
try {
|
||||
return values.doubleValue();
|
||||
} catch (Exception exception) {
|
||||
|
@ -280,7 +280,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
|
||||
return new FilterScript(vars, lookup, ctx) {
|
||||
@Override
|
||||
public boolean execute() {
|
||||
return script.execute() != 0.0;
|
||||
return script.execute(null) != 0.0;
|
||||
}
|
||||
@Override
|
||||
public void setDocument(int docid) {
|
||||
|
@ -546,7 +546,7 @@ public class PainlessExecuteAction extends ActionType<PainlessExecuteAction.Resp
|
||||
scoreScript.setScorer(scorer);
|
||||
}
|
||||
|
||||
double result = scoreScript.execute();
|
||||
double result = scoreScript.execute(null);
|
||||
return new Response(result);
|
||||
}, indexService);
|
||||
} else {
|
||||
|
@ -230,3 +230,7 @@ class org.elasticsearch.index.query.IntervalFilterScript$Interval {
|
||||
int getEnd()
|
||||
int getGaps()
|
||||
}
|
||||
|
||||
class org.elasticsearch.script.ScoreScript$ExplanationHolder {
|
||||
void set(String)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
|
||||
*/
|
||||
return new ScoreScript(params, lookup, context) {
|
||||
@Override
|
||||
public double execute() {
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
return 0.0d;
|
||||
}
|
||||
};
|
||||
@ -138,7 +138,7 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
|
||||
currentDocid = docid;
|
||||
}
|
||||
@Override
|
||||
public double execute() {
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
if (postings.docID() != currentDocid) {
|
||||
/*
|
||||
* advance moved past the current doc, so this doc
|
||||
|
@ -55,15 +55,6 @@ public class ScriptScoreFunction extends ScoreFunction {
|
||||
private final String indexName;
|
||||
private final Version indexVersion;
|
||||
|
||||
public ScriptScoreFunction(Script sScript, ScoreScript.LeafFactory script) {
|
||||
super(CombineFunction.REPLACE);
|
||||
this.sScript = sScript;
|
||||
this.script = script;
|
||||
this.indexName = null;
|
||||
this.shardId = -1;
|
||||
this.indexVersion = null;
|
||||
}
|
||||
|
||||
public ScriptScoreFunction(Script sScript, ScoreScript.LeafFactory script, String indexName, int shardId, Version indexVersion) {
|
||||
super(CombineFunction.REPLACE);
|
||||
this.sScript = sScript;
|
||||
@ -87,7 +78,7 @@ public class ScriptScoreFunction extends ScoreFunction {
|
||||
leafScript.setDocument(docId);
|
||||
scorer.docid = docId;
|
||||
scorer.score = subQueryScore;
|
||||
double result = leafScript.execute();
|
||||
double result = leafScript.execute(null);
|
||||
if (result < 0f) {
|
||||
throw new IllegalArgumentException("script score function must not produce negative scores, but got: [" + result + "]");
|
||||
}
|
||||
|
@ -25,13 +25,17 @@ import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.search.BooleanClause;
|
||||
import org.apache.lucene.search.DocIdSetIterator;
|
||||
import org.apache.lucene.search.Explanation;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.QueryVisitor;
|
||||
import org.apache.lucene.search.Weight;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.Scorer;
|
||||
import org.apache.lucene.search.Weight;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.script.ScoreScript;
|
||||
import org.elasticsearch.script.ScoreScript.ExplanationHolder;
|
||||
import org.elasticsearch.script.Script;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
@ -41,22 +45,30 @@ import java.util.Set;
|
||||
* A query that uses a script to compute documents' scores.
|
||||
*/
|
||||
public class ScriptScoreQuery extends Query {
|
||||
final Query subQuery;
|
||||
final ScriptScoreFunction function;
|
||||
private final Query subQuery;
|
||||
private final Script script;
|
||||
private final ScoreScript.LeafFactory scriptBuilder;
|
||||
private final Float minScore;
|
||||
private final String indexName;
|
||||
private final int shardId;
|
||||
private final Version indexVersion;
|
||||
|
||||
public ScriptScoreQuery(Query subQuery, ScriptScoreFunction function, Float minScore) {
|
||||
public ScriptScoreQuery(Query subQuery, Script script, ScoreScript.LeafFactory scriptBuilder,
|
||||
Float minScore, String indexName, int shardId, Version indexVersion) {
|
||||
this.subQuery = subQuery;
|
||||
this.function = function;
|
||||
this.script = script;
|
||||
this.scriptBuilder = scriptBuilder;
|
||||
this.minScore = minScore;
|
||||
this.indexName = indexName;
|
||||
this.shardId = shardId;
|
||||
this.indexVersion = indexVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query rewrite(IndexReader reader) throws IOException {
|
||||
Query newQ = subQuery.rewrite(reader);
|
||||
ScriptScoreFunction newFunction = (ScriptScoreFunction) function.rewrite(reader);
|
||||
if ((newQ != subQuery) || (newFunction != function)) {
|
||||
return new ScriptScoreQuery(newQ, newFunction, minScore);
|
||||
if (newQ != subQuery) {
|
||||
return new ScriptScoreQuery(newQ, script, scriptBuilder, minScore, indexName, shardId, indexVersion);
|
||||
}
|
||||
return super.rewrite(reader);
|
||||
}
|
||||
@ -66,7 +78,8 @@ public class ScriptScoreQuery extends Query {
|
||||
if (scoreMode == ScoreMode.COMPLETE_NO_SCORES && minScore == null) {
|
||||
return subQuery.createWeight(searcher, scoreMode, boost);
|
||||
}
|
||||
ScoreMode subQueryScoreMode = function.needsScores() ? ScoreMode.COMPLETE : ScoreMode.COMPLETE_NO_SCORES;
|
||||
boolean needsScore = scriptBuilder.needs_score();
|
||||
ScoreMode subQueryScoreMode = needsScore ? ScoreMode.COMPLETE : ScoreMode.COMPLETE_NO_SCORES;
|
||||
Weight subQueryWeight = subQuery.createWeight(searcher, subQueryScoreMode, boost);
|
||||
|
||||
return new Weight(this){
|
||||
@ -81,13 +94,59 @@ public class ScriptScoreQuery extends Query {
|
||||
if (subQueryScorer == null) {
|
||||
return null;
|
||||
}
|
||||
final LeafScoreFunction leafFunction = function.getLeafScoreFunction(context);
|
||||
Scorer scriptScorer = new Scorer(this) {
|
||||
Scorer scriptScorer = makeScriptScorer(subQueryScorer, context, null);
|
||||
|
||||
if (minScore != null) {
|
||||
scriptScorer = new MinScoreScorer(this, scriptScorer, minScore);
|
||||
}
|
||||
return scriptScorer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Explanation explain(LeafReaderContext context, int doc) throws IOException {
|
||||
Explanation subQueryExplanation = subQueryWeight.explain(context, doc);
|
||||
if (subQueryExplanation.isMatch() == false) {
|
||||
return subQueryExplanation;
|
||||
}
|
||||
ExplanationHolder explanationHolder = new ExplanationHolder();
|
||||
Scorer scorer = makeScriptScorer(subQueryWeight.scorer(context), context, explanationHolder);
|
||||
int newDoc = scorer.iterator().advance(doc);
|
||||
assert doc == newDoc; // subquery should have already matched above
|
||||
float score = scorer.score();
|
||||
|
||||
Explanation explanation = explanationHolder.get(score, needsScore ? subQueryExplanation : null);
|
||||
if (explanation == null) {
|
||||
// no explanation provided by user; give a simple one
|
||||
String desc = "script score function, computed with script:\"" + script + "\"";
|
||||
if (needsScore) {
|
||||
Explanation scoreExp = Explanation.match(subQueryExplanation.getValue(), "_score: ", subQueryExplanation);
|
||||
explanation = Explanation.match(score, desc, scoreExp);
|
||||
} else {
|
||||
explanation = Explanation.match(score, desc);
|
||||
}
|
||||
}
|
||||
|
||||
if (minScore != null && minScore > explanation.getValue().floatValue()) {
|
||||
explanation = Explanation.noMatch("Score value is too low, expected at least " + minScore +
|
||||
" but got " + explanation.getValue(), explanation);
|
||||
}
|
||||
return explanation;
|
||||
}
|
||||
|
||||
private Scorer makeScriptScorer(Scorer subQueryScorer, LeafReaderContext context,
|
||||
ExplanationHolder explanation) throws IOException {
|
||||
final ScoreScript scoreScript = scriptBuilder.newInstance(context);
|
||||
scoreScript.setScorer(subQueryScorer);
|
||||
scoreScript._setIndexName(indexName);
|
||||
scoreScript._setShard(shardId);
|
||||
scoreScript._setIndexVersion(indexVersion);
|
||||
|
||||
return new Scorer(this) {
|
||||
@Override
|
||||
public float score() throws IOException {
|
||||
int docId = docID();
|
||||
float subQueryScore = subQueryScoreMode == ScoreMode.COMPLETE ? subQueryScorer.score() : 0f;
|
||||
float score = (float) leafFunction.score(docId, subQueryScore);
|
||||
scoreScript.setDocument(docId);
|
||||
float score = (float) scoreScript.execute(explanation);
|
||||
if (score == Float.NEGATIVE_INFINITY || Float.isNaN(score)) {
|
||||
throw new ElasticsearchException(
|
||||
"script score query returned an invalid score: " + score + " for doc: " + docId);
|
||||
@ -109,25 +168,6 @@ public class ScriptScoreQuery extends Query {
|
||||
return Float.MAX_VALUE; // TODO: what would be a good upper bound?
|
||||
}
|
||||
};
|
||||
|
||||
if (minScore != null) {
|
||||
scriptScorer = new MinScoreScorer(this, scriptScorer, minScore);
|
||||
}
|
||||
return scriptScorer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Explanation explain(LeafReaderContext context, int doc) throws IOException {
|
||||
Explanation queryExplanation = subQueryWeight.explain(context, doc);
|
||||
if (queryExplanation.isMatch() == false) {
|
||||
return queryExplanation;
|
||||
}
|
||||
Explanation explanation = function.getLeafScoreFunction(context).explainScore(doc, queryExplanation);
|
||||
if (minScore != null && minScore > explanation.getValue().floatValue()) {
|
||||
explanation = Explanation.noMatch("Score value is too low, expected at least " + minScore +
|
||||
" but got " + explanation.getValue(), explanation);
|
||||
}
|
||||
return explanation;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -147,27 +187,26 @@ public class ScriptScoreQuery extends Query {
|
||||
@Override
|
||||
public String toString(String field) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("script score (").append(subQuery.toString(field)).append(", function: ");
|
||||
sb.append("{" + (function == null ? "" : function.toString()) + "}");
|
||||
sb.append("script score (").append(subQuery.toString(field)).append(", script: ");
|
||||
sb.append("{" + script.toString() + "}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (sameClassAs(o) == false) {
|
||||
return false;
|
||||
}
|
||||
ScriptScoreQuery other = (ScriptScoreQuery) o;
|
||||
return Objects.equals(this.subQuery, other.subQuery) &&
|
||||
Objects.equals(this.minScore, other.minScore) &&
|
||||
Objects.equals(this.function, other.function);
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ScriptScoreQuery that = (ScriptScoreQuery) o;
|
||||
return shardId == that.shardId &&
|
||||
subQuery.equals(that.subQuery) &&
|
||||
script.equals(that.script) &&
|
||||
Objects.equals(minScore, that.minScore) &&
|
||||
indexName.equals(that.indexName) &&
|
||||
indexVersion.equals(that.indexVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(classHash(), subQuery, minScore, function);
|
||||
return Objects.hash(subQuery, script, minScore, indexName, shardId, indexVersion);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import org.elasticsearch.geometry.Geometry;
|
||||
import org.elasticsearch.index.query.MoreLikeThisQueryBuilder.Item;
|
||||
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder;
|
||||
import org.elasticsearch.indices.TermsLookup;
|
||||
import org.elasticsearch.script.Script;
|
||||
@ -448,10 +447,10 @@ public final class QueryBuilders {
|
||||
* A query that allows to define a custom scoring function through script.
|
||||
*
|
||||
* @param queryBuilder The query to custom score
|
||||
* @param function The script score function builder used to custom score
|
||||
* @param script The script used to score the query
|
||||
*/
|
||||
public static ScriptScoreQueryBuilder scriptScoreQuery(QueryBuilder queryBuilder, ScriptScoreFunctionBuilder function) {
|
||||
return new ScriptScoreQueryBuilder(queryBuilder, function);
|
||||
public static ScriptScoreQueryBuilder scriptScoreQuery(QueryBuilder queryBuilder, Script script) {
|
||||
return new ScriptScoreQueryBuilder(queryBuilder, script);
|
||||
}
|
||||
|
||||
|
||||
|
@ -20,10 +20,10 @@
|
||||
package org.elasticsearch.index.query.functionscore;
|
||||
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.io.stream.StreamInput;
|
||||
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||
import org.elasticsearch.common.lucene.search.function.ScriptScoreFunction;
|
||||
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
|
||||
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
@ -33,6 +33,7 @@ import org.elasticsearch.index.query.InnerHitContextBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryRewriteContext;
|
||||
import org.elasticsearch.index.query.QueryShardContext;
|
||||
import org.elasticsearch.script.ScoreScript;
|
||||
import org.elasticsearch.script.Script;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -54,8 +55,7 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
|
||||
private static ConstructingObjectParser<ScriptScoreQueryBuilder, Void> PARSER = new ConstructingObjectParser<>(NAME, false,
|
||||
args -> {
|
||||
ScriptScoreFunctionBuilder ssFunctionBuilder = new ScriptScoreFunctionBuilder((Script) args[1]);
|
||||
ScriptScoreQueryBuilder ssQueryBuilder = new ScriptScoreQueryBuilder((QueryBuilder) args[0], ssFunctionBuilder);
|
||||
ScriptScoreQueryBuilder ssQueryBuilder = new ScriptScoreQueryBuilder((QueryBuilder) args[0], (Script) args[1]);
|
||||
if (args[2] != null) ssQueryBuilder.setMinScore((Float) args[2]);
|
||||
if (args[3] != null) ssQueryBuilder.boost((Float) args[3]);
|
||||
if (args[4] != null) ssQueryBuilder.queryName((String) args[4]);
|
||||
@ -76,25 +76,25 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
|
||||
private final QueryBuilder query;
|
||||
private Float minScore = null;
|
||||
private final ScriptScoreFunctionBuilder scriptScoreFunctionBuilder;
|
||||
private final Script script;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a script_score query that executes the provided script function on documents that match a query.
|
||||
*
|
||||
* @param query the query that defines which documents the script_score query will be executed on.
|
||||
* @param scriptScoreFunctionBuilder defines script function
|
||||
* @param script the script to run for computing the query score
|
||||
*/
|
||||
public ScriptScoreQueryBuilder(QueryBuilder query, ScriptScoreFunctionBuilder scriptScoreFunctionBuilder) {
|
||||
public ScriptScoreQueryBuilder(QueryBuilder query, Script script) {
|
||||
// require the supply of the query, even the explicit supply of "match_all" query
|
||||
if (query == null) {
|
||||
throw new IllegalArgumentException("script_score: query must not be null");
|
||||
}
|
||||
if (scriptScoreFunctionBuilder == null) {
|
||||
if (script == null) {
|
||||
throw new IllegalArgumentException("script_score: script must not be null");
|
||||
}
|
||||
this.query = query;
|
||||
this.scriptScoreFunctionBuilder = scriptScoreFunctionBuilder;
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,14 +103,22 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
public ScriptScoreQueryBuilder(StreamInput in) throws IOException {
|
||||
super(in);
|
||||
query = in.readNamedWriteable(QueryBuilder.class);
|
||||
scriptScoreFunctionBuilder = in.readNamedWriteable(ScriptScoreFunctionBuilder.class);
|
||||
if (in.getVersion().onOrAfter(Version.V_7_5_0)) {
|
||||
script = new Script(in);
|
||||
} else {
|
||||
script = in.readNamedWriteable(ScriptScoreFunctionBuilder.class).getScript();
|
||||
}
|
||||
minScore = in.readOptionalFloat();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWriteTo(StreamOutput out) throws IOException {
|
||||
out.writeNamedWriteable(query);
|
||||
out.writeNamedWriteable(scriptScoreFunctionBuilder);
|
||||
if (out.getVersion().onOrAfter(Version.V_7_5_0)) {
|
||||
script.writeTo(out);
|
||||
} else {
|
||||
out.writeNamedWriteable(new ScriptScoreFunctionBuilder(script));
|
||||
}
|
||||
out.writeOptionalFloat(minScore);
|
||||
}
|
||||
|
||||
@ -126,7 +134,7 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
builder.startObject(NAME);
|
||||
builder.field(QUERY_FIELD.getPreferredName());
|
||||
query.toXContent(builder, params);
|
||||
builder.field(SCRIPT_FIELD.getPreferredName(), scriptScoreFunctionBuilder.getScript());
|
||||
builder.field(SCRIPT_FIELD.getPreferredName(), script);
|
||||
if (minScore != null) {
|
||||
builder.field(MIN_SCORE_FIELD.getPreferredName(), minScore);
|
||||
}
|
||||
@ -151,20 +159,22 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
@Override
|
||||
protected boolean doEquals(ScriptScoreQueryBuilder other) {
|
||||
return Objects.equals(this.query, other.query) &&
|
||||
Objects.equals(this.scriptScoreFunctionBuilder, other.scriptScoreFunctionBuilder) &&
|
||||
Objects.equals(this.script, other.script) &&
|
||||
Objects.equals(this.minScore, other.minScore) ;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int doHashCode() {
|
||||
return Objects.hash(this.query, this.scriptScoreFunctionBuilder, this.minScore);
|
||||
return Objects.hash(this.query, this.script, this.minScore);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Query doToQuery(QueryShardContext context) throws IOException {
|
||||
ScriptScoreFunction function = (ScriptScoreFunction) scriptScoreFunctionBuilder.toFunction(context);
|
||||
ScoreScript.Factory factory = context.getScriptService().compile(script, ScoreScript.CONTEXT);
|
||||
ScoreScript.LeafFactory scoreScriptFactory = factory.newFactory(script.getParams(), context.lookup());
|
||||
Query query = this.query.toQuery(context);
|
||||
return new ScriptScoreQuery(query, function, minScore);
|
||||
return new ScriptScoreQuery(query, script, scoreScriptFactory, minScore,
|
||||
context.index().getName(), context.getShardId(), context.indexVersionCreated());
|
||||
}
|
||||
|
||||
|
||||
@ -172,7 +182,7 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
|
||||
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
|
||||
QueryBuilder newQuery = this.query.rewrite(queryRewriteContext);
|
||||
if (newQuery != query) {
|
||||
ScriptScoreQueryBuilder newQueryBuilder = new ScriptScoreQueryBuilder(newQuery, scriptScoreFunctionBuilder);
|
||||
ScriptScoreQueryBuilder newQueryBuilder = new ScriptScoreQueryBuilder(newQuery, script);
|
||||
newQueryBuilder.setMinScore(minScore);
|
||||
return newQueryBuilder;
|
||||
}
|
||||
|
@ -19,11 +19,12 @@
|
||||
package org.elasticsearch.script;
|
||||
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.search.Explanation;
|
||||
import org.apache.lucene.search.Scorable;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.index.fielddata.ScriptDocValues;
|
||||
import org.elasticsearch.search.lookup.LeafSearchLookup;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
import org.elasticsearch.Version;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@ -37,6 +38,30 @@ import java.util.function.DoubleSupplier;
|
||||
*/
|
||||
public abstract class ScoreScript {
|
||||
|
||||
/** A helper to take in an explanation from a script and turn it into an {@link org.apache.lucene.search.Explanation} */
|
||||
public static class ExplanationHolder {
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Explain the current score.
|
||||
*
|
||||
* @param description A textual description of how the score was calculated
|
||||
*/
|
||||
public void set(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Explanation get(double score, Explanation subQueryExplanation) {
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
if (subQueryExplanation != null) {
|
||||
return Explanation.match(score, description, subQueryExplanation);
|
||||
}
|
||||
return Explanation.match(score, description);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, String> DEPRECATIONS;
|
||||
static {
|
||||
Map<String, String> deprecations = new HashMap<>();
|
||||
@ -53,7 +78,7 @@ public abstract class ScoreScript {
|
||||
DEPRECATIONS = Collections.unmodifiableMap(deprecations);
|
||||
}
|
||||
|
||||
public static final String[] PARAMETERS = new String[]{};
|
||||
public static final String[] PARAMETERS = new String[]{ "explanation" };
|
||||
|
||||
/** The generic runtime parameters for the script. */
|
||||
private final Map<String, Object> params;
|
||||
@ -86,7 +111,7 @@ public abstract class ScoreScript {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract double execute();
|
||||
public abstract double execute(ExplanationHolder explanation);
|
||||
|
||||
/** Return the parameters for this script. */
|
||||
public Map<String, Object> getParams() {
|
||||
|
@ -21,7 +21,6 @@ package org.elasticsearch.index.query;
|
||||
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
|
||||
import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder;
|
||||
import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder;
|
||||
import org.elasticsearch.script.MockScriptEngine;
|
||||
import org.elasticsearch.script.Script;
|
||||
@ -40,10 +39,7 @@ public class ScriptScoreQueryBuilderTests extends AbstractQueryTestCase<ScriptSc
|
||||
protected ScriptScoreQueryBuilder doCreateTestQueryBuilder() {
|
||||
String scriptStr = "1";
|
||||
Script script = new Script(ScriptType.INLINE, MockScriptEngine.NAME, scriptStr, Collections.emptyMap());
|
||||
ScriptScoreQueryBuilder queryBuilder = new ScriptScoreQueryBuilder(
|
||||
RandomQueryBuilder.createQuery(random()),
|
||||
new ScriptScoreFunctionBuilder(script)
|
||||
);
|
||||
ScriptScoreQueryBuilder queryBuilder = new ScriptScoreQueryBuilder(RandomQueryBuilder.createQuery(random()), script);
|
||||
if (randomBoolean()) {
|
||||
queryBuilder.setMinScore(randomFloat());
|
||||
}
|
||||
@ -74,7 +70,6 @@ public class ScriptScoreQueryBuilderTests extends AbstractQueryTestCase<ScriptSc
|
||||
public void testIllegalArguments() {
|
||||
String scriptStr = "1";
|
||||
Script script = new Script(ScriptType.INLINE, MockScriptEngine.NAME, scriptStr, Collections.emptyMap());
|
||||
ScriptScoreFunctionBuilder functionBuilder = new ScriptScoreFunctionBuilder(script);
|
||||
|
||||
expectThrows(
|
||||
IllegalArgumentException.class,
|
||||
@ -83,7 +78,7 @@ public class ScriptScoreQueryBuilderTests extends AbstractQueryTestCase<ScriptSc
|
||||
|
||||
expectThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new ScriptScoreQueryBuilder(null, functionBuilder)
|
||||
() -> new ScriptScoreQueryBuilder(null, script)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -103,11 +103,11 @@ public class ExplainableScriptIT extends ESIntegTestCase {
|
||||
@Override
|
||||
public Explanation explain(Explanation subQueryScore) throws IOException {
|
||||
Explanation scoreExp = Explanation.match(subQueryScore.getValue(), "_score: ", subQueryScore);
|
||||
return Explanation.match((float) (execute()), "This script returned " + execute(), scoreExp);
|
||||
return Explanation.match((float) (execute(null)), "This script returned " + execute(null), scoreExp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double execute() {
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
return ((Number) ((ScriptDocValues) getDoc().get("number_field")).get(0)).doubleValue();
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ package org.elasticsearch.search.query;
|
||||
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.index.fielddata.ScriptDocValues;
|
||||
import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder;
|
||||
import org.elasticsearch.plugins.Plugin;
|
||||
import org.elasticsearch.script.MockScriptPlugin;
|
||||
import org.elasticsearch.script.Script;
|
||||
@ -55,7 +54,7 @@ public class ScriptScoreQueryIT extends ESIntegTestCase {
|
||||
@Override
|
||||
protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
|
||||
Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();
|
||||
scripts.put("doc['field2'].value * param1", vars -> {
|
||||
scripts.put("doc['field2'].value * param1", vars -> {
|
||||
Map<?, ?> doc = (Map) vars.get("doc");
|
||||
ScriptDocValues.Doubles field2Values = (ScriptDocValues.Doubles) doc.get("field2");
|
||||
Double param1 = (Double) vars.get("param1");
|
||||
@ -86,7 +85,7 @@ public class ScriptScoreQueryIT extends ESIntegTestCase {
|
||||
Script script = new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, "doc['field2'].value * param1", params);
|
||||
SearchResponse resp = client()
|
||||
.prepareSearch("test-index")
|
||||
.setQuery(scriptScoreQuery(matchQuery("field1", "text0"), new ScriptScoreFunctionBuilder(script)))
|
||||
.setQuery(scriptScoreQuery(matchQuery("field1", "text0"), script))
|
||||
.get();
|
||||
assertNoFailures(resp);
|
||||
assertOrderedSearchHits(resp, "10", "8", "6", "4", "2");
|
||||
@ -97,7 +96,7 @@ public class ScriptScoreQueryIT extends ESIntegTestCase {
|
||||
// applying min score
|
||||
resp = client()
|
||||
.prepareSearch("test-index")
|
||||
.setQuery(scriptScoreQuery(matchQuery("field1", "text0"), new ScriptScoreFunctionBuilder(script)).setMinScore(0.6f))
|
||||
.setQuery(scriptScoreQuery(matchQuery("field1", "text0"), script).setMinScore(0.6f))
|
||||
.get();
|
||||
assertNoFailures(resp);
|
||||
assertOrderedSearchHits(resp, "10", "8", "6");
|
||||
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch 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.elasticsearch.search.query;
|
||||
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.TextField;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.LeafReaderContext;
|
||||
import org.apache.lucene.search.Explanation;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.ScoreMode;
|
||||
import org.apache.lucene.search.Weight;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.common.lucene.search.Queries;
|
||||
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
|
||||
import org.elasticsearch.script.ScoreScript;
|
||||
import org.elasticsearch.script.Script;
|
||||
import org.elasticsearch.search.lookup.LeafSearchLookup;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ScriptScoreQueryTests extends ESTestCase {
|
||||
|
||||
private Directory dir;
|
||||
private IndexWriter w;
|
||||
private DirectoryReader reader;
|
||||
private IndexSearcher searcher;
|
||||
private LeafReaderContext leafReaderContext;
|
||||
|
||||
@Before
|
||||
public void initSearcher() throws IOException {
|
||||
dir = newDirectory();
|
||||
w = new IndexWriter(dir, newIndexWriterConfig(new StandardAnalyzer()));
|
||||
Document d = new Document();
|
||||
d.add(new TextField("field", "some text in a field", Field.Store.YES));
|
||||
d.add(new TextField("_uid", "1", Field.Store.YES));
|
||||
w.addDocument(d);
|
||||
w.commit();
|
||||
reader = DirectoryReader.open(w);
|
||||
searcher = newSearcher(reader);
|
||||
leafReaderContext = reader.leaves().get(0);
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeAllTheReaders() throws IOException {
|
||||
reader.close();
|
||||
w.close();
|
||||
dir.close();
|
||||
}
|
||||
|
||||
public void testExplain() throws IOException {
|
||||
Script script = new Script("script using explain");
|
||||
ScoreScript.LeafFactory factory = newFactory(script, true, explanation -> {
|
||||
assertNotNull(explanation);
|
||||
explanation.set("this explains the score");
|
||||
return 1.0;
|
||||
});
|
||||
|
||||
ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
|
||||
null, "index", 0, Version.CURRENT);
|
||||
Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
|
||||
Explanation explanation = weight.explain(leafReaderContext, 0);
|
||||
assertNotNull(explanation);
|
||||
assertThat(explanation.getDescription(), equalTo("this explains the score"));
|
||||
assertThat(explanation.getValue(), equalTo(1.0));
|
||||
}
|
||||
|
||||
public void testExplainDefault() throws IOException {
|
||||
Script script = new Script("script without setting explanation");
|
||||
ScoreScript.LeafFactory factory = newFactory(script, true, explanation -> 1.5);
|
||||
|
||||
ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
|
||||
null, "index", 0, Version.CURRENT);
|
||||
Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
|
||||
Explanation explanation = weight.explain(leafReaderContext, 0);
|
||||
assertNotNull(explanation);
|
||||
String description = explanation.getDescription();
|
||||
assertThat(description, containsString("script score function, computed with script:"));
|
||||
assertThat(description, containsString("script without setting explanation"));
|
||||
assertThat(explanation.getDetails(), arrayWithSize(1));
|
||||
assertThat(explanation.getDetails()[0].getDescription(), containsString("_score"));
|
||||
assertThat(explanation.getValue(), equalTo(1.5f));
|
||||
}
|
||||
|
||||
public void testExplainDefaultNoScore() throws IOException {
|
||||
Script script = new Script("script without setting explanation and no score");
|
||||
ScoreScript.LeafFactory factory = newFactory(script, false, explanation -> 2.0);
|
||||
|
||||
ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
|
||||
null, "index", 0, Version.CURRENT);
|
||||
Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
|
||||
Explanation explanation = weight.explain(leafReaderContext, 0);
|
||||
assertNotNull(explanation);
|
||||
String description = explanation.getDescription();
|
||||
assertThat(description, containsString("script score function, computed with script:"));
|
||||
assertThat(description, containsString("script without setting explanation and no score"));
|
||||
assertThat(explanation.getDetails(), arrayWithSize(0));
|
||||
assertThat(explanation.getValue(), equalTo(2.0f));
|
||||
}
|
||||
|
||||
private ScoreScript.LeafFactory newFactory(Script script, boolean needsScore,
|
||||
Function<ScoreScript.ExplanationHolder, Double> function) {
|
||||
SearchLookup lookup = mock(SearchLookup.class);
|
||||
LeafSearchLookup leafLookup = mock(LeafSearchLookup.class);
|
||||
when(lookup.getLeafSearchLookup(any())).thenReturn(leafLookup);
|
||||
return new ScoreScript.LeafFactory() {
|
||||
@Override
|
||||
public boolean needs_score() {
|
||||
return needsScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
|
||||
return new ScoreScript(script.getParams(), lookup, leafReaderContext) {
|
||||
@Override
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
return function.apply(explanation);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -566,7 +566,7 @@ public class MockScriptEngine implements ScriptEngine {
|
||||
Scorable[] scorerHolder = new Scorable[1];
|
||||
return new ScoreScript(params, lookup, ctx) {
|
||||
@Override
|
||||
public double execute() {
|
||||
public double execute(ExplanationHolder explanation) {
|
||||
Map<String, Object> vars = new HashMap<>(getParams());
|
||||
vars.put("doc", getDoc());
|
||||
if (scorerHolder[0] != null) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user