The Elasticsearch script_score query wraps an inner query and replaces its score with the value returned by a Painless script, evaluated per matching document. The script must return a non-negative double. It is the most flexible scoring tool in the DSL and supports built-in vector similarity functions (cosineSimilarity, dotProduct, l1norm, l2norm) for dense_vector fields. Use it when scoring requires custom math, field combinations, or a vector similarity contribution.
Syntax
{
"query": {
"script_score": {
"query": { /* inner query selects candidate docs */ },
"script": {
"source": "<Painless expression returning a non-negative double>",
"params": { }
},
"min_score": 0.0,
"boost": 1.0
}
}
}
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
query | - | Inner query that selects candidate documents. Required. |
script.source |
string | - | Painless expression returning a non-negative double. Required (or script.id). |
script.params |
object | {} |
Variables referenced inside the script as params.x. Keeps source stable so it caches. |
min_score |
float | - | Exclude documents whose script score is below this value. |
boost |
float | 1.0 |
Multiplier applied to the final script score. |
Inside the script:
_score- the inner query's BM25 score. Available only when the inner query produces a score.doc['field'].value- doc-values access (cheap, columnar).params._source.field- raw source access (expensive, avoid in scoring).- Vector functions:
cosineSimilarity(params.queryVector, 'fieldName'),dotProduct(...),l1norm(...),l2norm(...).
Scripts that return negative values throw at query time. Wrap subtractions with Math.max(0, ...) if necessary.
Examples
Combine BM25 score with a field value:
GET /articles/_search
{
"query": {
"script_score": {
"query": { "match": { "title": "elasticsearch" } },
"script": {
"source": "_score + Math.log(1 + doc['views'].value)"
}
}
}
}
Parameterized weighted blend:
GET /articles/_search
{
"query": {
"script_score": {
"query": { "match": { "title": "elasticsearch" } },
"script": {
"source": "params.a * _score + params.b * doc['popularity'].value",
"params": { "a": 1.2, "b": 0.5 }
}
}
}
}
Vector similarity over a dense_vector field with a +1.0 offset (scores must be non-negative):
GET /docs/_search
{
"query": {
"script_score": {
"query": { "match_all": {} },
"script": {
"source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
"params": { "query_vector": [0.1, 0.2, 0.3, /* ... */] }
}
}
}
}
Time decay:
GET /events/_search
{
"query": {
"script_score": {
"query": { "match_all": {} },
"script": {
"source": "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['ts'].value)",
"params": {
"origin": "now",
"scale": "30d",
"offset": "0",
"decay": 0.5
}
}
}
}
}
Performance and Use Notes
script_score runs the script once per document that matches the inner query. Cost is linear in inner-query match count, so the inner clause should already narrow the candidate set. Prefer doc values (doc['x'].value) over params._source - doc values are columnar and live in OS cache; _source parses JSON per document.
Always pass per-request values via params. Painless compiles each distinct source string and caches it (default 200 entries per context). If you template values into the script body, every request looks new and Elasticsearch will eventually throw circuit_breaking_exception for script compilation rate. With params, the same compiled script handles every request.
For simple boosts (decay, popularity, weight), the function_score query is faster and easier to maintain - it uses native functions instead of a script. Reach for script_score when scoring genuinely requires arbitrary math or vector similarity.
Custom scoring is a common origin of latency regressions and CPU spikes in Elasticsearch clusters. The manual triage - matching slow-log entries to deployed scoring scripts, checking whether each script is parameterized, deciding which could collapse into function_score recipes - is exactly the loop Pulse runs continuously.
Common Mistakes
- Returning a negative score - the script throws and the entire query fails. Use
Math.max(0, ...)or shift the score by adding a constant (e.g.cosineSimilarity(...) + 1.0). - Building the script source dynamically per request and triggering compilation rate limits.
- Reading
params._source.fieldinstead ofdoc['field'].value. - Using
_scoreinside the script when the inner query ismatch_allor in a filter context -_scoreis not meaningful there. - Solving with
script_scorewhatfunction_scorecovers with a native function (decay,field_value_factor).
Find Slow script_score Queries with Pulse
Pulse is an AI DBA for Elasticsearch and OpenSearch that continuously profiles production query traffic. For script_score queries specifically, Pulse:
- Identifies
script_scorequeries whose inner clause matches too many documents, so the Painless body runs per-doc over an inflated candidate set - Flags scripts that template per-request values directly into
sourceinstead of usingparams, the pattern that thrashes the compiled-script cache and triggerscircuit_breaking_exceptionat the script compilation rate limit - Detects
params._source.fieldaccess on fields that have doc values available, plus negative-score throws caused by missingMath.max(0, ...)guards or vector helpers used without a+1.0offset - Traces each slow
script_scoreback to the calling service via slow-log and APM correlation - Recommends concrete rewrites: parameterize the script, collapse the scoring into function_score with
field_value_factoror a decay function, narrow the inner query with abool.filter, or move vector scoring into a dedicated kNN query withscript_scorereserved for rescoring - Tracks latency and script-compilation-rate improvement after the change
This converts the manual scoring-debug loop into a continuous optimization workflow.
Frequently Asked Questions
Q: How do I access the inner query's BM25 score inside a script_score script?
A: Use the _score variable in Painless. It evaluates to the BM25 score from the inner query. If the inner query is match_all, _score is 1.0 and provides no useful signal.
Q: What is the difference between script_score query and function_score query?
A: function_score composes a small set of native scoring functions (decay, field_value_factor, random_score, weight, script_score) with explicit score_mode and boost_mode. The standalone script_score query lets a Painless script compute the score directly. Use function_score for standard recipes; use script_score when scoring is pure custom math or includes vector similarity.
Q: Can script_score return a negative number?
A: No. Elasticsearch requires non-negative scores. If your math can go negative, wrap with Math.max(0, ...) or shift by adding a constant. The vector similarity helpers (cosineSimilarity, dotProduct) can return negative values, so callers typically add +1.0.
Q: How can I avoid script compilation rate-limit errors with script_score?
A: Move all per-request values into params so the source string stays constant across requests. Elasticsearch caches a limited number of compiled scripts per context; changing source on every request thrashes the cache and triggers compilation circuit breaker exceptions.
Q: Does script_score support vector similarity?
A: Yes. Painless exposes cosineSimilarity, dotProduct, l1norm, and l2norm for dense_vector fields. For pure vector retrieval, the dedicated knn query is faster because it uses HNSW; use script_score for vector-based rescoring of a smaller candidate set.
Q: Why is my script_score query slow?
A: Typical causes are an inner query that matches too many documents, _source access inside the script, or a non-parameterized source that defeats compilation caching. Narrow the inner query, switch to doc values, and pass variables via params.
Q: What is the best tool to find slow script_score queries in production?
A: Pulse profiles Elasticsearch and OpenSearch slow logs, isolates script_score queries whose inner clause matches too many documents or whose source is not parameterized, attributes each to the calling service, and recommends a params rewrite, a function_score replacement, or a narrower inner query.
Related Reading
- Elasticsearch Function Score Query: native scoring functions composed without scripts.
- Elasticsearch kNN Query: HNSW vector retrieval (use for primary vector search).
- Elasticsearch Script Query: script-based filter (returns boolean, not a score).
- Elasticsearch Bool Query: typical wrapper for the inner
query. - Elasticsearch Match Query: inner full-text query for hybrid scoring.