Add a new query type - ScriptScoreQuery (#34533)

* Add a new query type - ScriptScoreQuery

script_score query uses script to calculate document scores.
Added as a substitute for function_score with an intentation
to deprecate function_scoreq query.

```http
GET /_search
{
    "query": {
        "script_score" : {
            "query": {
                "match": { "message": "elasticsearch" }
            },
            "script" : {
              "source": "Math.log(2 + doc['likes'].value)"
            },
            "min_score" : 2
        }
    }
}
```

Add several functions to painless to be used inside script_score:

double rational(double, double)
double sigmoid(double, double, double)
double randomNotReproducible() 
double randomReproducible(String, int) 

double decayGeoLinear(String, String, String, double, GeoPoint)
double decayGeoExp(String, String, String, double, GeoPoint)
double decayGeoGauss(String, String, String, double, GeoPoint)

double decayNumericLinear(String, String, String, double, double)
double decayNumericExp(String, String, String, double, double)
double decayNumericGauss(String, String, String, double, double)

double decayDateLinear(String, String, String, double, JodaCompatibleZonedDateTime)
double decayDateExp(String, String, String, double, JodaCompatibleZonedDateTime)
double decayDateGauss(String, String, String, double, JodaCompatibleZonedDateTime)

Date functions only works on dates in  the default format and default time zone
This commit is contained in:
Mayya Sharipova 2018-11-20 16:10:06 -05:00 committed by GitHub
parent c07ad67718
commit 643bb20137
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1688 additions and 10 deletions

View File

@ -0,0 +1,303 @@
[[query-dsl-script-score-query]]
=== Script Score Query
experimental[]
The `script_score` allows you to modify the score of documents that are
retrieved by a query. This can be useful if, for example, a score
function is computationally expensive and it is sufficient to compute
the score on a filtered set of documents.
To use `script_score`, you have to define a query and a script -
a function to be used to compute a new score for each document returned
by the query. For more information on scripting see
<<modules-scripting, scripting documentation>>.
Here is an example of using `script_score` to assign each matched document
a score equal to the number of likes divided by 10:
[source,js]
--------------------------------------------------
GET /_search
{
"query" : {
"script_score" : {
"query" : {
"match": { "message": "elasticsearch" }
},
"script" : {
"source" : "doc['likes'].value / 10 "
}
}
}
}
--------------------------------------------------
// CONSOLE
// TEST[setup:twitter]
==== Accessing the score of a document within a script
Within a script, you can
<<modules-scripting-fields#scripting-score, access>>
the `_score` variable which represents the current relevance score of a
document.
==== Predefined functions within a Painless script
You can use any of the available
<<painless-api-reference, painless functions>> in the painless script.
Besides these functions, there are a number of predefined functions
that can help you with scoring. We suggest you to use them instead of
rewriting equivalent functions of your own, as these functions try
to be the most efficient by using the internal mechanisms.
===== rational
latexmath:[rational(value,k) = value/(k + value)]
[source,js]
--------------------------------------------------
"script" : {
"source" : "rational(doc['likes'].value, 1)"
}
--------------------------------------------------
// NOTCONSOLE
===== sigmoid
latexmath:[sigmoid(value, k, a) = value^a/ (k^a + value^a)]
[source,js]
--------------------------------------------------
"script" : {
"source" : "sigmoid(doc['likes'].value, 2, 1)"
}
--------------------------------------------------
// NOTCONSOLE
[[random-functions]]
===== Random functions
There are two predefined ways to produce random values:
1. `randomNotReproducible()` uses `java.util.Random` class
to generate a random value of the type `long`.
The generated values are not reproducible between requests' invocations.
[source,js]
--------------------------------------------------
"script" : {
"source" : "randomNotReproducible()"
}
--------------------------------------------------
// NOTCONSOLE
2. `randomReproducible(String seedValue, int seed)` produces
reproducible random values of type `long`. This function requires
more computational time and memory than the non-reproducible version.
A good candidate for the `seedValue` is document field values that
are unique across documents and already pre-calculated and preloaded
in the memory. For example, values of the document's `_seq_no` field
is a good candidate, as documents on the same shard have unique values
for the `_seq_no` field.
[source,js]
--------------------------------------------------
"script" : {
"source" : "randomReproducible(Long.toString(doc['_seq_no'].value), 100)"
}
--------------------------------------------------
// NOTCONSOLE
A drawback of using `_seq_no` is that generated values change if
documents are updated. Another drawback is not absolute uniqueness, as
documents from different shards with the same sequence numbers
generate the same random values.
If you need random values to be distinct across different shards,
you can use a field with unique values across shards,
such as `_id`, but watch out for the memory usage as all
these unique values need to be loaded into memory.
[source,js]
--------------------------------------------------
"script" : {
"source" : "randomReproducible(doc['_id'].value, 100)"
}
--------------------------------------------------
// NOTCONSOLE
[[decay-functions]]
===== Decay functions for numeric fields
You can read more about decay functions
<<query-dsl-function-score-query#function-decay, here>>.
* `double decayNumericLinear(double origin, double scale, double offset, double decay, double docValue)`
* `double decayNumericExp(double origin, double scale, double offset, double decay, double docValue)`
* `double decayNumericGauss(double origin, double scale, double offset, double decay, double docValue)`
[source,js]
--------------------------------------------------
"script" : {
"source" : "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)",
"params": { <1>
"origin": 20,
"scale": 10,
"decay" : 0.5,
"offset" : 0
}
}
--------------------------------------------------
// NOTCONSOLE
<1> Use `params` to compile a script only once for different values of parameters
===== Decay functions for geo fields
* `double decayGeoLinear(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)`
* `double decayGeoExp(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)`
* `double decayGeoGauss(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)`
[source,js]
--------------------------------------------------
"script" : {
"source" : "decayGeoExp(params.origin, params.scale, params.offset, params.decay, doc['location'].value)",
"params": {
"origin": "40, -70.12",
"scale": "200km",
"offset": "0km",
"decay" : 0.2
}
}
--------------------------------------------------
// NOTCONSOLE
===== Decay functions for date fields
* `double decayDateLinear(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)`
* `double decayDateExp(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)`
* `double decayDateGauss(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)`
[source,js]
--------------------------------------------------
"script" : {
"source" : "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['date'].value)",
"params": {
"origin": "2008-01-01T01:00:00Z",
"scale": "1h",
"offset" : "0",
"decay" : 0.5
}
}
--------------------------------------------------
// NOTCONSOLE
NOTE: Decay functions on dates are limited to dates in the default format
and default time zone. Also calculations with `now` are not supported.
==== Faster alternatives
Script Score Query calculates the score for every hit (matching document).
There are faster alternative query types that can efficiently skip
non-competitive hits:
* If you want to boost documents on some static fields, use
<<query-dsl-feature-query, Feature Query>>.
==== Transition from Function Score Query
We are deprecating <<query-dsl-function-score-query, Function Score>>, and
Script Score Query will be a substitute for it.
Here we describe how Function Score Query's functions can be
equivalently implemented in Script Score Query:
===== `script_score`
What you used in `script_score` of the Function Score query, you
can copy into the Script Score query. No changes here.
===== `weight`
`weight` function can be implemented in the Script Score query through
the following script:
[source,js]
--------------------------------------------------
"script" : {
"source" : "params.weight * _score",
"params": {
"weight": 2
}
}
--------------------------------------------------
// NOTCONSOLE
===== `random_score`
Use `randomReproducible` and `randomNotReproducible` functions
as described in <<random-functions, random functions>>.
===== `field_value_factor`
`field_value_factor` function can be easily implemented through script:
[source,js]
--------------------------------------------------
"script" : {
"source" : "Math.log10(doc['field'].value * params.factor)",
params" : {
"factor" : 5
}
}
--------------------------------------------------
// NOTCONSOLE
For checking if a document has a missing value, you can use
`doc['field'].size() == 0`. For example, this script will use
a value `1` if a document doesn't have a field `field`:
[source,js]
--------------------------------------------------
"script" : {
"source" : "Math.log10((doc['field'].size() == 0 ? 1 : doc['field'].value()) * params.factor)",
params" : {
"factor" : 5
}
}
--------------------------------------------------
// NOTCONSOLE
This table lists how `field_value_factor` modifiers can be implemented
through a script:
[cols="<,<",options="header",]
|=======================================================================
| Modifier | Implementation in Script Score
| `none` | -
| `log` | `Math.log10(doc['f'].value)`
| `log1p` | `Math.log10(doc['f'].value + 1)`
| `log2p` | `Math.log10(doc['f'].value + 2)`
| `ln` | `Math.log(doc['f'].value)`
| `ln1p` | `Math.log(doc['f'].value + 1)`
| `ln2p` | `Math.log(doc['f'].value + 2)`
| `square` | `Math.pow(doc['f'].value, 2)`
| `sqrt` | `Math.sqrt(doc['f'].value)`
| `reciprocal` | `1.0 / doc['f'].value`
|=======================================================================
===== `decay functions`
Script Score query has equivalent <<decay-functions, decay functions>>
that can be used in script.

View File

@ -38,6 +38,7 @@ import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScoreScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.search.aggregations.pipeline.MovingFunctionScript;
@ -72,6 +73,11 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
movFn.add(WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.aggs.movfn.txt"));
map.put(MovingFunctionScript.CONTEXT, movFn);
// Functions used for scoring docs
List<Whitelist> scoreFn = new ArrayList<>(Whitelist.BASE_WHITELISTS);
scoreFn.add(WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.score.txt"));
map.put(ScoreScript.CONTEXT, scoreFn);
whitelists = map;
}

View File

@ -0,0 +1,38 @@
#
# 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.
#
# This file contains a whitelist for functions to be used in Score context
static_import {
double rational(double, double) from_class org.elasticsearch.script.ScoreScriptUtils
double sigmoid(double, double, double) from_class org.elasticsearch.script.ScoreScriptUtils
double randomReproducible(String, int) from_class org.elasticsearch.script.ScoreScriptUtils
double randomNotReproducible() bound_to org.elasticsearch.script.ScoreScriptUtils$RandomNotReproducible
double decayGeoLinear(String, String, String, double, GeoPoint) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayGeoLinear
double decayGeoExp(String, String, String, double, GeoPoint) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayGeoExp
double decayGeoGauss(String, String, String, double, GeoPoint) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayGeoGauss
double decayNumericLinear(double, double, double, double, double) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayNumericLinear
double decayNumericExp(double, double, double, double, double) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayNumericExp
double decayNumericGauss(double, double, double, double, double) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayNumericGauss
double decayDateLinear(String, String, String, double, JodaCompatibleZonedDateTime) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayDateLinear
double decayDateExp(String, String, String, double, JodaCompatibleZonedDateTime) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayDateExp
double decayDateGauss(String, String, String, double, JodaCompatibleZonedDateTime) bound_to org.elasticsearch.script.ScoreScriptUtils$DecayDateGauss
}

View File

@ -0,0 +1,484 @@
# Integration tests for ScriptScoreQuery using Painless
setup:
- skip:
version: " - 6.99.99"
reason: "script score query was introduced in 7.0.0"
---
"Random functions":
- do:
indices.create:
index: test
body:
settings:
number_of_shards: 2
mappings:
_doc:
properties:
f1:
type: keyword
- do:
index:
index: test
type: _doc
id: 1
body: {"f1": "v1"}
- do:
index:
index: test
type: _doc
id: 2
body: {"f1": "v2"}
- do:
index:
index: test
type: _doc
id: 3
body: {"f1": "v3"}
- do:
indices.refresh: {}
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "randomReproducible(Long.toString(doc['_seq_no'].value), 100)"
- match: { hits.total: 3 }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "randomNotReproducible()"
- match: { hits.total: 3 }
---
"Decay geo functions":
- do:
indices.create:
index: test
body:
settings:
number_of_shards: 1
mappings:
_doc:
properties:
text-location:
type: keyword
location:
type: geo_point
- do:
index:
index: test
type: _doc
id: 1
body: { "text-location": "location1", "location" : {"lat" : 40.24, "lon" : -70.24} }
- do:
index:
index: test
type: _doc
id: 2
body: { "text-location": "location2", "location" : {"lat" : 40.12, "lon" : -70.12} }
- do:
indices.refresh: {}
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayGeoLinear(params.origin, params.scale, params.offset, params.decay, doc['location'].value)"
params:
origin: "40, -70"
scale: "200km"
offset: "0km"
decay: 0.5
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayGeoExp(params.origin, params.scale, params.offset, params.decay, doc['location'].value)"
params:
origin: "40, -70"
scale: "200km"
offset: "0km"
decay: 0.5
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayGeoGauss(params.origin, params.scale, params.offset, params.decay, doc['location'].value)"
params:
origin: "40, -70"
scale: "200km"
offset: "0km"
decay: 0.5
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
---
"Decay date functions":
- do:
indices.create:
index: test
body:
settings:
number_of_shards: 1
mappings:
_doc:
properties:
date:
type: date
- do:
index:
index: test
type: _doc
id: 1
body: { "date": "2018-01-01T02:00:00Z"}
- do:
index:
index: test
type: _doc
id: 2
body: { "date": "2018-01-01T01:00:00Z" }
- do:
indices.refresh: {}
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayDateLinear(params.origin, params.scale, params.offset, params.decay, doc['date'].value)"
params:
origin: "2018-01-01T00:00:00Z"
scale: "1h"
offset: "0"
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayDateExp(params.origin, params.scale, params.offset, params.decay, doc['date'].value)"
params:
origin: "2018-01-01T00:00:00Z"
scale: "1h"
offset: "0"
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['date'].value)"
params:
origin: "2018-01-01T00:00:00Z"
scale: "1h"
offset: "0"
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
---
"Decay numeric functions":
- do:
indices.create:
index: test
body:
settings:
number_of_shards: 1
mappings:
_doc:
properties:
ival:
type: integer
lval:
type: long
fval:
type: float
dval:
type: double
- do:
index:
index: test
type: _doc
id: 1
body: { "ival" : 40, "lval" : 40, "fval": 40.0, "dval": 40.0}
# for this document, the smallest value in the array is chosen, which will be the closest to the origin
- do:
index:
index: test
type: _doc
id: 2
body: { "ival" : [50, 40, 20], "lval" : [50, 40, 20], "fval" : [50.0, 40.0, 20.0], "dval" : [50.0, 40.0, 20.0] }
- do:
indices.refresh: {}
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['ival'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['lval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['fval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericExp(params.origin, params.scale, params.offset, params.decay, doc['ival'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericExp(params.origin, params.scale, params.offset, params.decay, doc['lval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericExp(params.origin, params.scale, params.offset, params.decay, doc['fval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericExp(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericGauss(params.origin, params.scale, params.offset, params.decay, doc['ival'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericGauss(params.origin, params.scale, params.offset, params.decay, doc['lval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericGauss(params.origin, params.scale, params.offset, params.decay, doc['fval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }
- do:
search:
index: test
body:
query:
script_score:
query: {match_all: {} }
script:
source: "decayNumericGauss(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)"
params:
origin: 20
scale: 10
offset: 0
decay: 0.9
- match: { hits.total: 2 }
- match: { hits.hits.0._id : "2" }
- match: { hits.hits.1._id : "1" }

View File

@ -82,16 +82,14 @@ public class ScriptScoreFunction extends ScoreFunction {
exp = ((ExplainableScoreScript) leafScript).explain(subQueryScore);
} else {
double score = score(docId, subQueryScore.getValue().floatValue());
// info about params already included in sScript
String explanation = "script score function, computed with script:\"" + sScript + "\"";
if (sScript.getParams() != null) {
explanation += " and parameters: \n" + sScript.getParams().toString();
}
Explanation scoreExp = Explanation.match(
subQueryScore.getValue(), "_score: ",
subQueryScore);
subQueryScore.getValue(), "_score: ",
subQueryScore);
return Explanation.match(
(float) score, explanation,
scoreExp);
(float) score, explanation,
scoreExp);
}
return exp;
}

View File

@ -0,0 +1,167 @@
/*
* 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.common.lucene.search.function;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Query;
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.elasticsearch.ElasticsearchException;
import java.io.IOException;
import java.util.Objects;
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 Float minScore;
public ScriptScoreQuery(Query subQuery, ScriptScoreFunction function, Float minScore) {
this.subQuery = subQuery;
this.function = function;
this.minScore = minScore;
}
@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);
}
return super.rewrite(reader);
}
@Override
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
if (scoreMode == ScoreMode.COMPLETE_NO_SCORES && minScore == null) {
return subQuery.createWeight(searcher, scoreMode, boost);
}
ScoreMode subQueryScoreMode = function.needsScores() ? ScoreMode.COMPLETE : ScoreMode.COMPLETE_NO_SCORES;
Weight subQueryWeight = subQuery.createWeight(searcher, subQueryScoreMode, boost);
return new Weight(this){
@Override
public void extractTerms(Set<Term> terms) {
subQueryWeight.extractTerms(terms);
}
@Override
public Scorer scorer(LeafReaderContext context) throws IOException {
Scorer subQueryScorer = subQueryWeight.scorer(context);
if (subQueryScorer == null) {
return null;
}
final LeafScoreFunction leafFunction = function.getLeafScoreFunction(context);
Scorer scriptScorer = 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);
if (score == Float.NEGATIVE_INFINITY || Float.isNaN(score)) {
throw new ElasticsearchException(
"script score query returned an invalid score: " + score + " for doc: " + docId);
}
return score;
}
@Override
public int docID() {
return subQueryScorer.docID();
}
@Override
public DocIdSetIterator iterator() {
return subQueryScorer.iterator();
}
@Override
public float getMaxScore(int upTo) {
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
public boolean isCacheable(LeafReaderContext ctx) {
// If minScore is not null, then matches depend on statistics of the top-level reader.
return minScore == null;
}
};
}
@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()) + "}");
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);
}
@Override
public int hashCode() {
return Objects.hash(classHash(), subQuery, minScore, function);
}
}

View File

@ -27,6 +27,8 @@ import org.elasticsearch.common.geo.builders.ShapeBuilder;
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;
@ -435,6 +437,17 @@ public final class QueryBuilders {
return (new FunctionScoreQueryBuilder(queryBuilder, function));
}
/**
* 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
*/
public static ScriptScoreQueryBuilder scriptScoreQuery(QueryBuilder queryBuilder, ScriptScoreFunctionBuilder function) {
return new ScriptScoreQueryBuilder(queryBuilder, function);
}
/**
* A more like this query that finds documents that are "like" the provided texts or documents
* which is checked against the fields the query is constructed with.

View File

@ -102,7 +102,7 @@ public abstract class ScoreFunctionBuilder<FB extends ScoreFunctionBuilder<FB>>
protected abstract void doXContent(XContentBuilder builder, Params params) throws IOException;
@Override
public final String getWriteableName() {
public String getWriteableName() {
return getName();
}
@ -116,8 +116,7 @@ public abstract class ScoreFunctionBuilder<FB extends ScoreFunctionBuilder<FB>>
}
@SuppressWarnings("unchecked")
FB other = (FB) obj;
return Objects.equals(weight, other.getWeight()) &&
doEquals(other);
return Objects.equals(weight, other.getWeight()) && doEquals(other);
}
/**

View File

@ -0,0 +1,187 @@
/*
* 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.index.query.functionscore;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.AbstractQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.common.lucene.search.function.ScriptScoreFunction;
import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
import org.elasticsearch.index.query.InnerHitContextBuilder;
import org.elasticsearch.index.query.QueryRewriteContext;
import org.elasticsearch.index.query.QueryShardContext;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
/**
* A query that computes a document score based on the provided script
*/
public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQueryBuilder> {
public static final String NAME = "script_score";
public static final ParseField QUERY_FIELD = new ParseField("query");
public static final ParseField SCRIPT_FIELD = new ParseField("script");
public static final ParseField MIN_SCORE_FIELD = new ParseField("min_score");
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);
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]);
return ssQueryBuilder;
});
static {
PARSER.declareObject(constructorArg(), (p,c) -> parseInnerQueryBuilder(p), QUERY_FIELD);
PARSER.declareObject(constructorArg(), (p,c) -> Script.parse(p), SCRIPT_FIELD);
PARSER.declareFloat(optionalConstructorArg(), MIN_SCORE_FIELD);
PARSER.declareFloat(optionalConstructorArg(), AbstractQueryBuilder.BOOST_FIELD);
PARSER.declareString(optionalConstructorArg(), AbstractQueryBuilder.NAME_FIELD);
}
public static ScriptScoreQueryBuilder fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
private final QueryBuilder query;
private Float minScore = null;
private final ScriptScoreFunctionBuilder scriptScoreFunctionBuilder;
/**
* 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
*/
public ScriptScoreQueryBuilder(QueryBuilder query, ScriptScoreFunctionBuilder scriptScoreFunctionBuilder) {
// 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) {
throw new IllegalArgumentException("script_score: script must not be null");
}
this.query = query;
this.scriptScoreFunctionBuilder = scriptScoreFunctionBuilder;
}
/**
* Read from a stream.
*/
public ScriptScoreQueryBuilder(StreamInput in) throws IOException {
super(in);
query = in.readNamedWriteable(QueryBuilder.class);
scriptScoreFunctionBuilder = in.readNamedWriteable(ScriptScoreFunctionBuilder.class);
minScore = in.readOptionalFloat();
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeNamedWriteable(query);
out.writeNamedWriteable(scriptScoreFunctionBuilder);
out.writeOptionalFloat(minScore);
}
/**
* Returns the query builder that defines which documents the script_score query will be executed on.
*/
public QueryBuilder query() {
return this.query;
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.field(QUERY_FIELD.getPreferredName());
query.toXContent(builder, params);
builder.field(SCRIPT_FIELD.getPreferredName(), scriptScoreFunctionBuilder.getScript());
if (minScore != null) {
builder.field(MIN_SCORE_FIELD.getPreferredName(), minScore);
}
printBoostAndQueryName(builder);
builder.endObject();
}
public ScriptScoreQueryBuilder setMinScore(float minScore) {
this.minScore = minScore;
return this;
}
public Float getMinScore() {
return this.minScore;
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
protected boolean doEquals(ScriptScoreQueryBuilder other) {
return Objects.equals(this.query, other.query) &&
Objects.equals(this.scriptScoreFunctionBuilder, other.scriptScoreFunctionBuilder) &&
Objects.equals(this.minScore, other.minScore) ;
}
@Override
protected int doHashCode() {
return Objects.hash(this.query, this.scriptScoreFunctionBuilder, this.minScore);
}
@Override
protected Query doToQuery(QueryShardContext context) throws IOException {
ScriptScoreFunction function = (ScriptScoreFunction) scriptScoreFunctionBuilder.toFunction(context);
Query query = this.query.toQuery(context);
return new ScriptScoreQuery(query, function, minScore);
}
@Override
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
QueryBuilder newQuery = this.query.rewrite(queryRewriteContext);
if (newQuery != query) {
ScriptScoreQueryBuilder newQueryBuilder = new ScriptScoreQueryBuilder(newQuery, scriptScoreFunctionBuilder);
newQueryBuilder.setMinScore(minScore);
return newQueryBuilder;
}
return this;
}
@Override
protected void extractInnerHitBuilders(Map<String, InnerHitContextBuilder> innerHits) {
InnerHitContextBuilder.extractInnerHits(query(), innerHits);
}
}

View File

@ -0,0 +1,276 @@
/*
* 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.script;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.StringHelper;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.joda.JodaDateMathParser;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.mapper.DateFieldMapper;
import java.time.ZoneId;
import java.util.Random;
/**
* ScoringScriptImpl can be used as {@link ScoreScript}
* to run a previously compiled Painless script.
*/
public final class ScoreScriptUtils {
/****** STATIC FUNCTIONS that can be used by users for score calculations **/
public static double rational(double value, double k) {
return value/ (k + value);
}
/**
* Calculate a sigmoid of <code>value</code>
* with scaling parameters <code>k</code> and <code>a</code>
*/
public static double sigmoid(double value, double k, double a){
return Math.pow(value,a) / (Math.pow(k,a) + Math.pow(value,a));
}
// reproducible random
public static double randomReproducible(String seedValue, int seed) {
int hash = StringHelper.murmurhash3_x86_32(new BytesRef(seedValue), seed);
return (hash & 0x00FFFFFF) / (float)(1 << 24); // only use the lower 24 bits to construct a float from 0.0-1.0
}
// not reproducible random
public static final class RandomNotReproducible {
private final Random rnd;
public RandomNotReproducible() {
this.rnd = Randomness.get();
}
public double randomNotReproducible() {
return rnd.nextDouble();
}
}
// **** Decay functions on geo field
public static final class DecayGeoLinear {
// cached variables calculated once per script execution
double originLat;
double originLon;
double offset;
double scaling;
public DecayGeoLinear(String originStr, String scaleStr, String offsetStr, double decay) {
GeoPoint origin = GeoUtils.parseGeoPoint(originStr, false);
double scale = DistanceUnit.DEFAULT.parse(scaleStr, DistanceUnit.DEFAULT);
this.originLat = origin.lat();
this.originLon = origin.lon();
this.offset = DistanceUnit.DEFAULT.parse(offsetStr, DistanceUnit.DEFAULT);
this.scaling = scale / (1.0 - decay);
}
public double decayGeoLinear(GeoPoint docValue) {
double distance = GeoDistance.ARC.calculate(originLat, originLon, docValue.lat(), docValue.lon(), DistanceUnit.METERS);
distance = Math.max(0.0d, distance - offset);
return Math.max(0.0, (scaling - distance) / scaling);
}
}
public static final class DecayGeoExp {
double originLat;
double originLon;
double offset;
double scaling;
public DecayGeoExp(String originStr, String scaleStr, String offsetStr, double decay) {
GeoPoint origin = GeoUtils.parseGeoPoint(originStr, false);
double scale = DistanceUnit.DEFAULT.parse(scaleStr, DistanceUnit.DEFAULT);
this.originLat = origin.lat();
this.originLon = origin.lon();
this.offset = DistanceUnit.DEFAULT.parse(offsetStr, DistanceUnit.DEFAULT);
this.scaling = Math.log(decay) / scale;
}
public double decayGeoExp(GeoPoint docValue) {
double distance = GeoDistance.ARC.calculate(originLat, originLon, docValue.lat(), docValue.lon(), DistanceUnit.METERS);
distance = Math.max(0.0d, distance - offset);
return Math.exp(scaling * distance);
}
}
public static final class DecayGeoGauss {
double originLat;
double originLon;
double offset;
double scaling;
public DecayGeoGauss(String originStr, String scaleStr, String offsetStr, double decay) {
GeoPoint origin = GeoUtils.parseGeoPoint(originStr, false);
double scale = DistanceUnit.DEFAULT.parse(scaleStr, DistanceUnit.DEFAULT);
this.originLat = origin.lat();
this.originLon = origin.lon();
this.offset = DistanceUnit.DEFAULT.parse(offsetStr, DistanceUnit.DEFAULT);
this.scaling = 0.5 * Math.pow(scale, 2.0) / Math.log(decay);;
}
public double decayGeoGauss(GeoPoint docValue) {
double distance = GeoDistance.ARC.calculate(originLat, originLon, docValue.lat(), docValue.lon(), DistanceUnit.METERS);
distance = Math.max(0.0d, distance - offset);
return Math.exp(0.5 * Math.pow(distance, 2.0) / scaling);
}
}
// **** Decay functions on numeric field
public static final class DecayNumericLinear {
double origin;
double offset;
double scaling;
public DecayNumericLinear(double origin, double scale, double offset, double decay) {
this.origin = origin;
this.offset = offset;
this.scaling = scale / (1.0 - decay);
}
public double decayNumericLinear(double docValue) {
double distance = Math.max(0.0d, Math.abs(docValue - origin) - offset);
return Math.max(0.0, (scaling - distance) / scaling);
}
}
public static final class DecayNumericExp {
double origin;
double offset;
double scaling;
public DecayNumericExp(double origin, double scale, double offset, double decay) {
this.origin = origin;
this.offset = offset;
this.scaling = Math.log(decay) / scale;
}
public double decayNumericExp(double docValue) {
double distance = Math.max(0.0d, Math.abs(docValue - origin) - offset);
return Math.exp(scaling * distance);
}
}
public static final class DecayNumericGauss {
double origin;
double offset;
double scaling;
public DecayNumericGauss(double origin, double scale, double offset, double decay) {
this.origin = origin;
this.offset = offset;
this.scaling = 0.5 * Math.pow(scale, 2.0) / Math.log(decay);
}
public double decayNumericGauss(double docValue) {
double distance = Math.max(0.0d, Math.abs(docValue - origin) - offset);
return Math.exp(0.5 * Math.pow(distance, 2.0) / scaling);
}
}
// **** Decay functions on date field
/**
* Limitations: since script functions don't have access to DateFieldMapper,
* decay functions on dates are limited to dates in the default format and default time zone,
* Also, using calculations with <code>now</code> are not allowed.
*
*/
private static final ZoneId defaultZoneId = ZoneId.of("UTC");
private static final JodaDateMathParser dateParser = new JodaDateMathParser(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER);
public static final class DecayDateLinear {
long origin;
long offset;
double scaling;
public DecayDateLinear(String originStr, String scaleStr, String offsetStr, double decay) {
this.origin = dateParser.parse(originStr, null, false, defaultZoneId);
long scale = TimeValue.parseTimeValue(scaleStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".scale")
.getMillis();
this.offset = TimeValue.parseTimeValue(offsetStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".offset")
.getMillis();
this.scaling = scale / (1.0 - decay);
}
public double decayDateLinear(JodaCompatibleZonedDateTime docValueDate) {
long docValue = docValueDate.toInstant().toEpochMilli();
// as java.lang.Math#abs(long) is a forbidden API, have to use this comparison instead
long diff = (docValue >= origin) ? (docValue - origin) : (origin - docValue);
long distance = Math.max(0, diff - offset);
return Math.max(0.0, (scaling - distance) / scaling);
}
}
public static final class DecayDateExp {
long origin;
long offset;
double scaling;
public DecayDateExp(String originStr, String scaleStr, String offsetStr, double decay) {
this.origin = dateParser.parse(originStr, null, false, defaultZoneId);
long scale = TimeValue.parseTimeValue(scaleStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".scale")
.getMillis();
this.offset = TimeValue.parseTimeValue(offsetStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".offset")
.getMillis();
this.scaling = Math.log(decay) / scale;
}
public double decayDateExp(JodaCompatibleZonedDateTime docValueDate) {
long docValue = docValueDate.toInstant().toEpochMilli();
long diff = (docValue >= origin) ? (docValue - origin) : (origin - docValue);
long distance = Math.max(0, diff - offset);
return Math.exp(scaling * distance);
}
}
public static final class DecayDateGauss {
long origin;
long offset;
double scaling;
public DecayDateGauss(String originStr, String scaleStr, String offsetStr, double decay) {
this.origin = dateParser.parse(originStr, null, false, defaultZoneId);
long scale = TimeValue.parseTimeValue(scaleStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".scale")
.getMillis();
this.offset = TimeValue.parseTimeValue(offsetStr, TimeValue.timeValueHours(24), getClass().getSimpleName() + ".offset")
.getMillis();
this.scaling = 0.5 * Math.pow(scale, 2.0) / Math.log(decay);
}
public double decayDateGauss(JodaCompatibleZonedDateTime docValueDate) {
long docValue = docValueDate.toInstant().toEpochMilli();
long diff = (docValue >= origin) ? (docValue - origin) : (origin - docValue);
long distance = Math.max(0, diff - offset);
return Math.exp(0.5 * Math.pow(distance, 2.0) / scaling);
}
}
}

View File

@ -81,6 +81,7 @@ import org.elasticsearch.index.query.functionscore.LinearDecayFunctionBuilder;
import org.elasticsearch.index.query.functionscore.RandomScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScriptScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScriptScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.WeightBuilder;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.plugins.SearchPlugin.AggregationSpec;
@ -634,8 +635,12 @@ public class SearchModule {
}
private void registerScoreFunctions(List<SearchPlugin> plugins) {
// ScriptScoreFunctionBuilder has it own named writable because of a new script_score query
namedWriteables.add(new NamedWriteableRegistry.Entry(
ScriptScoreFunctionBuilder.class, ScriptScoreFunctionBuilder.NAME, ScriptScoreFunctionBuilder::new));
registerScoreFunction(new ScoreFunctionSpec<>(ScriptScoreFunctionBuilder.NAME, ScriptScoreFunctionBuilder::new,
ScriptScoreFunctionBuilder::fromXContent));
registerScoreFunction(
new ScoreFunctionSpec<>(GaussDecayFunctionBuilder.NAME, GaussDecayFunctionBuilder::new, GaussDecayFunctionBuilder.PARSER));
registerScoreFunction(new ScoreFunctionSpec<>(LinearDecayFunctionBuilder.NAME, LinearDecayFunctionBuilder::new,
@ -786,6 +791,7 @@ public class SearchModule {
new QuerySpec<>(SpanMultiTermQueryBuilder.NAME, SpanMultiTermQueryBuilder::new, SpanMultiTermQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(FunctionScoreQueryBuilder.NAME, FunctionScoreQueryBuilder::new,
FunctionScoreQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(ScriptScoreQueryBuilder.NAME, ScriptScoreQueryBuilder::new, ScriptScoreQueryBuilder::fromXContent));
registerQuery(
new QuerySpec<>(SimpleQueryStringBuilder.NAME, SimpleQueryStringBuilder::new, SimpleQueryStringBuilder::fromXContent));
registerQuery(new QuerySpec<>(TypeQueryBuilder.NAME, TypeQueryBuilder::new, TypeQueryBuilder::fromXContent));

View File

@ -0,0 +1,95 @@
/*
* 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.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;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;
import java.io.IOException;
import java.util.Collections;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.hamcrest.CoreMatchers.instanceOf;
public class ScriptScoreQueryBuilderTests extends AbstractQueryTestCase<ScriptScoreQueryBuilder> {
@Override
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)
);
if (randomBoolean()) {
queryBuilder.setMinScore(randomFloat());
}
return queryBuilder;
}
@Override
protected void doAssertLuceneQuery(ScriptScoreQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
assertThat(query, instanceOf(ScriptScoreQuery.class));
}
public void testFromJson() throws IOException {
String json =
"{\n" +
" \"script_score\" : {\n" +
" \"query\" : { \"match_all\" : {} },\n" +
" \"script\" : {\n" +
" \"source\" : \"doc['field'].value\" \n" +
" },\n" +
" \"min_score\" : 2.0\n" +
" }\n" +
"}";
ScriptScoreQueryBuilder parsed = (ScriptScoreQueryBuilder) parseQuery(json);
assertEquals(json, 2, parsed.getMinScore(), 0.0001);
}
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,
() -> new ScriptScoreQueryBuilder(matchAllQuery(), null)
);
expectThrows(
IllegalArgumentException.class,
() -> new ScriptScoreQueryBuilder(null, functionBuilder)
);
}
@Override
protected boolean isCachable(ScriptScoreQueryBuilder queryBuilder) {
return false;
}
}

View File

@ -341,6 +341,7 @@ public class SearchModuleTests extends ESTestCase {
"range",
"regexp",
"script",
"script_score",
"simple_query_string",
"span_containing",
"span_first",

View File

@ -0,0 +1,105 @@
/*
* 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.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;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.test.ESIntegTestCase;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
import static org.elasticsearch.index.query.QueryBuilders.scriptScoreQuery;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFirstHit;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertOrderedSearchHits;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSecondHit;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertThirdHit;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasScore;
public class ScriptScoreQueryIT extends ESIntegTestCase {
@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return Collections.singleton(CustomScriptPlugin.class);
}
public static class CustomScriptPlugin extends MockScriptPlugin {
@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 -> {
Map<?, ?> doc = (Map) vars.get("doc");
ScriptDocValues.Doubles field2Values = (ScriptDocValues.Doubles) doc.get("field2");
Double param1 = (Double) vars.get("param1");
return field2Values.getValue() * param1;
});
return scripts;
}
}
// test that script_score works as expected:
// 1) only matched docs retrieved
// 2) score is calculated based on a script with params
// 3) min score applied
public void testScriptScore() {
assertAcked(
prepareCreate("test-index").addMapping("_doc", "field1", "type=text", "field2", "type=double")
);
int docCount = 10;
for (int i = 1; i <= docCount; i++) {
client().prepareIndex("test-index", "_doc", "" + i)
.setSource("field1", "text" + (i % 2), "field2", i )
.get();
}
refresh();
Map<String, Object> params = new HashMap<>();
params.put("param1", 0.1);
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)))
.get();
assertNoFailures(resp);
assertOrderedSearchHits(resp, "10", "8", "6", "4", "2");
assertFirstHit(resp, hasScore(1.0f));
assertSecondHit(resp, hasScore(0.8f));
assertThirdHit(resp, hasScore(0.6f));
// applying min score
resp = client()
.prepareSearch("test-index")
.setQuery(scriptScoreQuery(matchQuery("field1", "text0"), new ScriptScoreFunctionBuilder(script)).setMinScore(0.6f))
.get();
assertNoFailures(resp);
assertOrderedSearchHits(resp, "10", "8", "6");
}
}