Script score query
editScript score query
editUses a script to provide a custom score for returned documents.
The script_score
query is useful if, for example, a scoring function is expensive and you only need to calculate the score of a filtered set of documents.
Example request
editThe following script_score
query assigns each returned document a score equal to the my-int
field value divided by 10
.
resp = client.search( query={ "script_score": { "query": { "match": { "message": "elasticsearch" } }, "script": { "source": "doc['my-int'].value / 10 " } } }, ) print(resp)
response = client.search( body: { query: { script_score: { query: { match: { message: 'elasticsearch' } }, script: { source: "doc['my-int'].value / 10 " } } } } ) puts response
const response = await client.search({ query: { script_score: { query: { match: { message: "elasticsearch", }, }, script: { source: "doc['my-int'].value / 10 ", }, }, }, }); console.log(response);
GET /_search { "query": { "script_score": { "query": { "match": { "message": "elasticsearch" } }, "script": { "source": "doc['my-int'].value / 10 " } } } }
Top-level parameters for script_score
edit-
query
- (Required, query object) Query used to return documents.
-
script
-
(Required, script object) Script used to compute the score of documents returned by the
query
.Final relevance scores from the
script_score
query cannot be negative. To support certain search optimizations, Lucene requires scores be positive or0
. -
min_score
- (Optional, float) Documents with a score lower than this floating point number are excluded from the search results.
-
boost
-
(Optional, float) Documents' scores produced by
script
are multiplied byboost
to produce final documents' scores. Defaults to1.0
.
Notes
editUse relevance scores in a script
editWithin a script, you can
access
the _score
variable which represents the current relevance score of a
document.
Predefined functions
editYou can use any of the available painless
functions in your script
. You can also use the following predefined functions
to customize scoring:
We suggest using these predefined functions instead of writing your own. These functions take advantage of efficiencies from Elasticsearch' internal mechanisms.
Saturation
editsaturation(value,k) = value/(k + value)
"script" : { "source" : "saturation(doc['my-int'].value, 1)" }
Sigmoid
editsigmoid(value, k, a) = value^a/ (k^a + value^a)
"script" : { "source" : "sigmoid(doc['my-int'].value, 2, 1)" }
Random score function
editrandom_score
function generates scores that are uniformly distributed
from 0 up to but not including 1.
randomScore
function has the following syntax:
randomScore(<seed>, <fieldName>)
.
It has a required parameter - seed
as an integer value,
and an optional parameter - fieldName
as a string value.
"script" : { "source" : "randomScore(100, '_seq_no')" }
If the fieldName
parameter is omitted, the internal Lucene
document ids will be used as a source of randomness. This is very efficient,
but unfortunately not reproducible since documents might be renumbered
by merges.
"script" : { "source" : "randomScore(100)" }
Note that documents that are within the same shard and have the
same value for field will get the same score, so it is usually desirable
to use a field that has unique values for all documents across a shard.
A good default choice might be to use the _seq_no
field, whose only drawback is that scores will change if the document is
updated since update operations also update the value of the _seq_no
field.
Decay functions for numeric fields
editYou can read more about decay functions 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)
Decay functions for geo fields
edit-
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)
"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 } }
Decay functions for date fields
edit-
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)
"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 } }
Decay functions on dates are limited to dates in the default format
and default time zone. Also calculations with now
are not supported.
Functions for vector fields
editFunctions for vector fields are accessible through
script_score
query.
Allow expensive queries
editScript score queries will not be executed if search.allow_expensive_queries
is set to false.
Faster alternatives
editThe script_score
query calculates the score for
every matching document, or hit. There are faster alternative query types that
can efficiently skip non-competitive hits:
-
If you want to boost documents on some static fields, use the
rank_feature
query. -
If you want to boost documents closer to a date or geographic point, use the
distance_feature
query.
Transition from the function score query
editWe recommend using the script_score
query instead of
function_score
query for the simplicity
of the script_score
query.
You can implement the following functions of the function_score
query using
the script_score
query:
script_score
editWhat you used in script_score
of the Function Score query, you
can copy into the Script Score query. No changes here.
weight
editweight
function can be implemented in the Script Score query through
the following script:
"script" : { "source" : "params.weight * _score", "params": { "weight": 2 } }
random_score
editUse randomScore
function
as described in random score function.
field_value_factor
editfield_value_factor
function can be easily implemented through script:
"script" : { "source" : "Math.log10(doc['field'].value * params.factor)", "params" : { "factor" : 5 } }
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
:
"script" : { "source" : "Math.log10((doc['field'].size() == 0 ? 1 : doc['field'].value()) * params.factor)", "params" : { "factor" : 5 } }
This table lists how field_value_factor
modifiers can be implemented
through a script:
Modifier | Implementation in Script Score |
---|---|
|
- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
decay
functions
editThe script_score
query has equivalent decay
functions that can be used in scripts.
Functions for vector fields
editDuring vector functions' calculation, all matched documents are
linearly scanned. Thus, expect the query time grow linearly
with the number of matched documents. For this reason, we recommend
to limit the number of matched documents with a query
parameter.
This is the list of available vector functions and vector access methods:
-
cosineSimilarity
– calculates cosine similarity -
dotProduct
– calculates dot product -
l1norm
– calculates L1 distance -
hamming
– calculates Hamming distance -
l2norm
- calculates L2 distance -
doc[<field>].vectorValue
– returns a vector’s value as an array of floats -
doc[<field>].magnitude
– returns a vector’s magnitude
The cosineSimilarity
and dotProduct
functions are not supported for bit
vectors.
The recommended way to access dense vectors is through the
cosineSimilarity
, dotProduct
, l1norm
or l2norm
functions. Please note
however, that you should call these functions only once per script. For example,
don’t use these functions in a loop to calculate the similarity between a
document vector and multiple other vectors. If you need that functionality,
reimplement these functions yourself by
accessing vector values directly.
Let’s create an index with a dense_vector
mapping and index a couple
of documents into it.
resp = client.indices.create( index="my-index-000001", mappings={ "properties": { "my_dense_vector": { "type": "dense_vector", "index": False, "dims": 3 }, "my_byte_dense_vector": { "type": "dense_vector", "index": False, "dims": 3, "element_type": "byte" }, "status": { "type": "keyword" } } }, ) print(resp) resp1 = client.index( index="my-index-000001", id="1", document={ "my_dense_vector": [ 0.5, 10, 6 ], "my_byte_dense_vector": [ 0, 10, 6 ], "status": "published" }, ) print(resp1) resp2 = client.index( index="my-index-000001", id="2", document={ "my_dense_vector": [ -0.5, 10, 10 ], "my_byte_dense_vector": [ 0, 10, 10 ], "status": "published" }, ) print(resp2) resp3 = client.indices.refresh( index="my-index-000001", ) print(resp3)
const response = await client.indices.create({ index: "my-index-000001", mappings: { properties: { my_dense_vector: { type: "dense_vector", index: false, dims: 3, }, my_byte_dense_vector: { type: "dense_vector", index: false, dims: 3, element_type: "byte", }, status: { type: "keyword", }, }, }, }); console.log(response); const response1 = await client.index({ index: "my-index-000001", id: 1, document: { my_dense_vector: [0.5, 10, 6], my_byte_dense_vector: [0, 10, 6], status: "published", }, }); console.log(response1); const response2 = await client.index({ index: "my-index-000001", id: 2, document: { my_dense_vector: [-0.5, 10, 10], my_byte_dense_vector: [0, 10, 10], status: "published", }, }); console.log(response2); const response3 = await client.indices.refresh({ index: "my-index-000001", }); console.log(response3);
PUT my-index-000001 { "mappings": { "properties": { "my_dense_vector": { "type": "dense_vector", "index": false, "dims": 3 }, "my_byte_dense_vector": { "type": "dense_vector", "index": false, "dims": 3, "element_type": "byte" }, "status" : { "type" : "keyword" } } } } PUT my-index-000001/_doc/1 { "my_dense_vector": [0.5, 10, 6], "my_byte_dense_vector": [0, 10, 6], "status" : "published" } PUT my-index-000001/_doc/2 { "my_dense_vector": [-0.5, 10, 10], "my_byte_dense_vector": [0, 10, 10], "status" : "published" } POST my-index-000001/_refresh
Cosine similarity
editThe cosineSimilarity
function calculates the measure of
cosine similarity between a given query vector and document vectors.
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", "params": { "query_vector": [ 4, 3.4, -0.2 ] } } } }, ) print(resp)
response = client.search( index: 'my-index-000001', body: { query: { script_score: { query: { bool: { filter: { term: { status: 'published' } } } }, script: { source: "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", params: { query_vector: [ 4, 3.4, -0.2 ] } } } } } ) puts response
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", params: { query_vector: [4, 3.4, -0.2], }, }, }, }, }); console.log(response);
GET my-index-000001/_search { "query": { "script_score": { "query" : { "bool" : { "filter" : { "term" : { "status" : "published" } } } }, "script": { "source": "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", "params": { "query_vector": [4, 3.4, -0.2] } } } } }
To restrict the number of documents on which script score calculation is applied, provide a filter. |
|
The script adds 1.0 to the cosine similarity to prevent the score from being negative. |
|
To take advantage of the script optimizations, provide a query vector as a script parameter. |
If a document’s dense vector field has a number of dimensions different from the query’s vector, an error will be thrown.
Dot product
editThe dotProduct
function calculates the measure of
dot product between a given query vector and document vectors.
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "\n double value = dotProduct(params.query_vector, 'my_dense_vector');\n return sigmoid(1, Math.E, -value); \n ", "params": { "query_vector": [ 4, 3.4, -0.2 ] } } } }, ) print(resp)
response = client.search( index: 'my-index-000001', body: { query: { script_score: { query: { bool: { filter: { term: { status: 'published' } } } }, script: { source: "\n double value = dotProduct(params.query_vector, 'my_dense_vector');\n return sigmoid(1, Math.E, -value); \n ", params: { query_vector: [ 4, 3.4, -0.2 ] } } } } } ) puts response
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "\n double value = dotProduct(params.query_vector, 'my_dense_vector');\n return sigmoid(1, Math.E, -value); \n ", params: { query_vector: [4, 3.4, -0.2], }, }, }, }, }); console.log(response);
GET my-index-000001/_search { "query": { "script_score": { "query" : { "bool" : { "filter" : { "term" : { "status" : "published" } } } }, "script": { "source": """ double value = dotProduct(params.query_vector, 'my_dense_vector'); return sigmoid(1, Math.E, -value); """, "params": { "query_vector": [4, 3.4, -0.2] } } } } }
L1 distance (Manhattan distance)
editThe l1norm
function calculates L1 distance
(Manhattan distance) between a given query vector and
document vectors.
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", "params": { "queryVector": [ 4, 3.4, -0.2 ] } } } }, ) print(resp)
response = client.search( index: 'my-index-000001', body: { query: { script_score: { query: { bool: { filter: { term: { status: 'published' } } } }, script: { source: "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", params: { "queryVector": [ 4, 3.4, -0.2 ] } } } } } ) puts response
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", params: { queryVector: [4, 3.4, -0.2], }, }, }, }, }); console.log(response);
GET my-index-000001/_search { "query": { "script_score": { "query" : { "bool" : { "filter" : { "term" : { "status" : "published" } } } }, "script": { "source": "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", "params": { "queryVector": [4, 3.4, -0.2] } } } } }
Unlike |
Hamming distance
editThe hamming
function calculates Hamming distance between a given query vector and
document vectors. It is only available for byte and bit vectors.
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "(24 - hamming(params.queryVector, 'my_byte_dense_vector')) / 24", "params": { "queryVector": [ 4, 3, 0 ] } } } }, ) print(resp)
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "(24 - hamming(params.queryVector, 'my_byte_dense_vector')) / 24", params: { queryVector: [4, 3, 0], }, }, }, }, }); console.log(response);
L2 distance (Euclidean distance)
editThe l2norm
function calculates L2 distance
(Euclidean distance) between a given query vector and
document vectors.
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))", "params": { "queryVector": [ 4, 3.4, -0.2 ] } } } }, ) print(resp)
response = client.search( index: 'my-index-000001', body: { query: { script_score: { query: { bool: { filter: { term: { status: 'published' } } } }, script: { source: "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))", params: { "queryVector": [ 4, 3.4, -0.2 ] } } } } } ) puts response
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))", params: { queryVector: [4, 3.4, -0.2], }, }, }, }, }); console.log(response);
GET my-index-000001/_search { "query": { "script_score": { "query" : { "bool" : { "filter" : { "term" : { "status" : "published" } } } }, "script": { "source": "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))", "params": { "queryVector": [4, 3.4, -0.2] } } } } }
Checking for missing values
editIf a document doesn’t have a value for a vector field on which a vector function is executed, an error will be thrown.
You can check if a document has a value for the field my_vector
with
doc['my_vector'].size() == 0
. Your overall script can look like this:
"source": "doc['my_vector'].size() == 0 ? 0 : cosineSimilarity(params.queryVector, 'my_vector')"
Accessing vectors directly
editYou can access vector values directly through the following functions:
-
doc[<field>].vectorValue
– returns a vector’s value as an array of floats
For bit
vectors, it does return a float[]
, where each element represents 8 bits.
-
doc[<field>].magnitude
– returns a vector’s magnitude as a float (for vectors created prior to version 7.5 the magnitude is not stored. So this function calculates it anew every time it is called).
For bit
vectors, this is just the square root of the sum of 1
bits.
For example, the script below implements a cosine similarity using these two functions:
resp = client.search( index="my-index-000001", query={ "script_score": { "query": { "bool": { "filter": { "term": { "status": "published" } } } }, "script": { "source": "\n float[] v = doc['my_dense_vector'].vectorValue;\n float vm = doc['my_dense_vector'].magnitude;\n float dotProduct = 0;\n for (int i = 0; i < v.length; i++) {\n dotProduct += v[i] * params.queryVector[i];\n }\n return dotProduct / (vm * (float) params.queryVectorMag);\n ", "params": { "queryVector": [ 4, 3.4, -0.2 ], "queryVectorMag": 5.25357 } } } }, ) print(resp)
response = client.search( index: 'my-index-000001', body: { query: { script_score: { query: { bool: { filter: { term: { status: 'published' } } } }, script: { source: "\n float[] v = doc['my_dense_vector'].vectorValue;\n float vm = doc['my_dense_vector'].magnitude;\n float dotProduct = 0;\n for (int i = 0; i < v.length; i++) {\n dotProduct += v[i] * params.queryVector[i];\n }\n return dotProduct / (vm * (float) params.queryVectorMag);\n ", params: { "queryVector": [ 4, 3.4, -0.2 ], "queryVectorMag": 5.25357 } } } } } ) puts response
const response = await client.search({ index: "my-index-000001", query: { script_score: { query: { bool: { filter: { term: { status: "published", }, }, }, }, script: { source: "\n float[] v = doc['my_dense_vector'].vectorValue;\n float vm = doc['my_dense_vector'].magnitude;\n float dotProduct = 0;\n for (int i = 0; i < v.length; i++) {\n dotProduct += v[i] * params.queryVector[i];\n }\n return dotProduct / (vm * (float) params.queryVectorMag);\n ", params: { queryVector: [4, 3.4, -0.2], queryVectorMag: 5.25357, }, }, }, }, }); console.log(response);
GET my-index-000001/_search { "query": { "script_score": { "query" : { "bool" : { "filter" : { "term" : { "status" : "published" } } } }, "script": { "source": """ float[] v = doc['my_dense_vector'].vectorValue; float vm = doc['my_dense_vector'].magnitude; float dotProduct = 0; for (int i = 0; i < v.length; i++) { dotProduct += v[i] * params.queryVector[i]; } return dotProduct / (vm * (float) params.queryVectorMag); """, "params": { "queryVector": [4, 3.4, -0.2], "queryVectorMag": 5.25357 } } } } }
Bit vectors and vector functions
editWhen using bit
vectors, not all the vector functions are available. The supported functions are:
Currently, the cosineSimilarity
and dotProduct
functions are not supported for bit
vectors.
Explain request
editUsing an 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:
resp = client.explain( index="my-index-000001", id="0", query={ "script_score": { "query": { "match": { "message": "elasticsearch" } }, "script": { "source": "\n long count = doc['count'].value;\n double normalizedCount = count / 10;\n if (explanation != null) {\n explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n }\n return normalizedCount;\n " } } }, ) print(resp)
response = client.explain( index: 'my-index-000001', id: 0, body: { query: { script_score: { query: { match: { message: 'elasticsearch' } }, script: { source: "\n long count = doc['count'].value;\n double normalizedCount = count / 10;\n if (explanation != nil) {\n explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n }\n return normalizedCount;\n " } } } } ) puts response
const response = await client.explain({ index: "my-index-000001", id: 0, query: { script_score: { query: { match: { message: "elasticsearch", }, }, script: { source: "\n long count = doc['count'].value;\n double normalizedCount = count / 10;\n if (explanation != null) {\n explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n }\n return normalizedCount;\n ", }, }, }, }); console.log(response);
GET /my-index-000001/_explain/0 { "query": { "script_score": { "query": { "match": { "message": "elasticsearch" } }, "script": { "source": """ long count = doc['count'].value; double normalizedCount = count / 10; if (explanation != null) { explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount); } return normalizedCount; """ } } } }
Note that the explanation
will be null when using in a normal _search
request, so having a conditional guard is best practice.