Lucene expression language
The expression language compiles a single JavaScript expression into JVM bytecode. It evaluates numeric formulas over doc values and supports no statements, variable declarations, or string handling.
An expression is resolved to direct doc values accessors at compile time, so evaluating it for one document reduces to arithmetic, with no type resolution, map lookups, or method dispatch. A formula that both the Lucene expression language and Painless can express typically runs faster as an expression than as a Painless script.
The language is sandboxed and enabled for inline and stored scripts by default, and it can be used in the score, field, filter, number_sort, aggs, bucket_aggregation, aggregation_selector, and terms_set contexts. Set lang to expression to select it.
Syntax
An expression script consists of a single expression that evaluates to a number, and the conditional ?: operator is its only form of control flow. An expression can use the arithmetic, comparison, and bitwise operators of JavaScript along with the functions in the Lucene expressions module, including abs, min, max, sqrt, pow, log, the trigonometric functions, and haversin for geographic distance.
An expression can reference the following values:
- A document field, through
doc['field_name'].value. - A property or method of a field, such as
doc['field_name'].emptyordoc['field_name'].sum(). - A script parameter, by its name alone. An expression references a parameter as
discount, not asparams.discount; using theparamsprefix fails withUnknown variable [params]. - The relevance score,
_score, which is available only in ascript_scorecontext.
Referencing a parameter without the params prefix is the syntax difference most likely to be overlooked when a script is moved from Painless to expression.
Numeric field API
The following table lists the properties and methods available on a numeric field.
| Expression | Description |
|---|---|
doc['field_name'].value | The field’s value, returned as a double. |
doc['field_name'].empty | Whether the field has no value in this document. |
doc['field_name'].length | The number of values that the field has in this document. |
doc['field_name'].min() | The smallest of the field’s values in this document. |
doc['field_name'].max() | The largest of the field’s values in this document. |
doc['field_name'].median() | The median of the field’s values in this document. |
doc['field_name'].avg() | The mean of the field’s values in this document. |
doc['field_name'].sum() | The sum of the field’s values in this document. |
A field that is absent from the document evaluates to 0. To substitute a different value, test empty first, as in doc['ratings'].empty ? 3 : doc['ratings'].value.
A multi-valued field evaluates to its smallest value. To select a different one, call the corresponding method, as in doc['ratings'].sum().
Boolean fields are exposed as numbers, with true as 1 and false as 0, so a Boolean can gate a calculation directly: doc['on_sale'].value ? doc['price'].value - doc['price'].value * discount : doc['price'].value.
The following search returns each product’s mean rating, substituting -1 for the one product that has no ratings. It uses the scripting-products index, which is created in Test setup:
GET scripting-products/_search
{
"_source": ["name"],
"sort": [{ "sku": "asc" }],
"script_fields": {
"average_rating": {
"script": {
"lang": "expression",
"source": "doc['ratings'].empty ? unrated : doc['ratings'].avg()",
"params": { "unrated": -1 }
}
}
}
}
The ultrawide monitor, which has no ratings values, returns the substitute value:
Response
{
"took": 71,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "scripting-products",
"_id": "3",
"_score": null,
"_source": {
"name": "Mechanical keyboard"
},
"fields": {
"average_rating": [
5.0
]
},
"sort": [
"ACC-2001"
]
},
{
"_index": "scripting-products",
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless noise cancelling headphones"
},
"fields": {
"average_rating": [
4.25
]
},
"sort": [
"AUD-1001"
]
},
{
"_index": "scripting-products",
"_id": "2",
"_score": null,
"_source": {
"name": "Wireless earbuds"
},
"fields": {
"average_rating": [
4.0
]
},
"sort": [
"AUD-1002"
]
},
{
"_index": "scripting-products",
"_id": "4",
"_score": null,
"_source": {
"name": "Ultrawide monitor"
},
"fields": {
"average_rating": [
-1.0
]
},
"sort": [
"DSP-3001"
]
}
]
}
}
Every value that an expression returns is a double, which is why the ratings appear as 5.0 and -1.0 rather than as integers.
Date field API
A date field is exposed as a number of milliseconds elapsed since the Unix epoch, so the entire numeric field API applies to it. In addition, the date property gives access to individual calendar components.
The following table lists the components available on a date field.
| Expression | Description |
|---|---|
doc['field_name'].date.centuryOfEra | The century, from 1 to 2920000. |
doc['field_name'].date.dayOfMonth | The day of the month, from 1 to 31. |
doc['field_name'].date.dayOfWeek | The day of the week, from 1 for Monday to 7 for Sunday. |
doc['field_name'].date.dayOfYear | The day of the year, where January 1 is 1. |
doc['field_name'].date.era | The era, where 0 is BCE and 1 is CE. |
doc['field_name'].date.hourOfDay | The hour, from 0 to 23. |
doc['field_name'].date.millisOfDay | The number of milliseconds elapsed in the day, from 0 to 86399999. |
doc['field_name'].date.millisOfSecond | The number of milliseconds elapsed in the second, from 0 to 999. |
doc['field_name'].date.minuteOfDay | The number of minutes elapsed in the day, from 0 to 1439. |
doc['field_name'].date.minuteOfHour | The minute, from 0 to 59. |
doc['field_name'].date.monthOfYear | The month, from 1 for January to 12 for December. |
doc['field_name'].date.secondOfDay | The number of seconds elapsed in the day, from 0 to 86399. |
doc['field_name'].date.secondOfMinute | The second, from 0 to 59. |
doc['field_name'].date.year | The year, from -292000000 to 292000000. |
doc['field_name'].date.yearOfCentury | The year within the century, from 1 to 100. |
doc['field_name'].date.yearOfEra | The year within the era, from 1 to 292000000. |
To find the number of whole years between two date fields, subtract their years: doc['release_date'].date.year - doc['discontinued_date'].date.year.
The geopoint field API
The following table lists the properties available on a geo_point field.
| Expression | Description |
|---|---|
doc['field_name'].empty | Whether the field has no value in this document. |
doc['field_name'].lat | The latitude of the point. |
doc['field_name'].lon | The longitude of the point. |
Combine these with haversin to compute a great-circle distance in kilometers. The following search orders products by how far their warehouse is from Washington, DC, with the origin supplied as parameters so that the same compiled script serves every user location:
GET scripting-products/_search
{
"_source": ["name", "warehouse"],
"sort": {
"_script": {
"type": "number",
"script": {
"lang": "expression",
"source": "haversin(lat, lon, doc['warehouse'].lat, doc['warehouse'].lon)",
"params": { "lat": 38.9072, "lon": -77.0369 }
},
"order": "asc"
}
}
}
The sort value on each result is the computed distance in kilometers:
Response
{
"took": 12,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "scripting-products",
"_id": "3",
"_score": null,
"_source": {
"name": "Mechanical keyboard",
"warehouse": { "lon": -87.6298, "lat": 41.8781 }
},
"sort": [
955.1850203827704
]
},
{
"_index": "scripting-products",
"_id": "4",
"_score": null,
"_source": {
"name": "Ultrawide monitor",
"warehouse": { "lon": -97.7431, "lat": 30.2672 }
},
"sort": [
2118.1759666556795
]
},
{
"_index": "scripting-products",
"_id": "1",
"_score": null,
"_source": {
"name": "Wireless noise cancelling headphones",
"warehouse": { "lon": -122.3321, "lat": 47.6062 }
},
"sort": [
3736.260216407672
]
},
{
"_index": "scripting-products",
"_id": "2",
"_score": null,
"_source": {
"name": "Wireless earbuds",
"warehouse": { "lon": -122.4194, "lat": 37.7749 }
},
"sort": [
3918.550756602828
]
}
]
}
}
Limitations
The expression language has the following limitations:
- Only numeric, Boolean, date, and
geo_pointfields are readable, so an expression cannot compare or concatenate strings. Reading akeywordfield fails with alink errorcaused byField [sku] must be numeric, date, or geopoint. Enablingfielddataon atextfield makes it readable in Painless but not in an expression, which reports the same error. - Stored fields and the
_sourceare unavailable, so an expression cannot read a value that is not in doc values. Both are reached throughparams, which an expression does not receive, soparams._sourceandparams._fieldsfail withUnknown variable [params]. - There is no way to test whether a field exists in the mapping.
doc['field'].emptyreports only whether a mapped field has a value in the current document. Referencing an unmapped field fails with alink errorcaused byField [discount] does not exist in mappings, and wrapping the reference inemptyproduces the same failure. - Every result is a
double, so an expression cannot return a string, a Boolean, or a structured value.
When a formula needs any of these, use Painless instead.