Skip to content

Commit 491adbe

Browse files
ES|QL query builder (#2997) (#3009)
* ES|QL query builder * add missing esql api documentation * add FORK command * initial attempt at generating all functions * unit tests * more operators * documentation * integration tests * add new COMPLETION command * show ES|QL in all docs examples * Docstring fixes * add technical preview warning (cherry picked from commit 9eadabb) Co-authored-by: Miguel Grinberg <[email protected]>
1 parent 33eb66a commit 491adbe

File tree

13 files changed

+4283
-18
lines changed

13 files changed

+4283
-18
lines changed

docs/reference/esql-query-builder.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# ES|QL Query Builder
2+
3+
::::{warning}
4+
This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.
5+
::::
6+
7+
The ES|QL Query Builder allows you to construct ES|QL queries using Python syntax. Consider the following example:
8+
9+
```python
10+
>>> from elasticsearch.esql import ESQL
11+
>>> query = (
12+
ESQL.from_("employees")
13+
.sort("emp_no")
14+
.keep("first_name", "last_name", "height")
15+
.eval(height_feet="height * 3.281", height_cm="height * 100")
16+
.limit(3)
17+
)
18+
```
19+
20+
You can then see the assembled ES|QL query by printing the resulting query object:
21+
22+
```python
23+
>>> query
24+
FROM employees
25+
| SORT emp_no
26+
| KEEP first_name, last_name, height
27+
| EVAL height_feet = height * 3.281, height_cm = height * 100
28+
| LIMIT 3
29+
```
30+
31+
To execute this query, you can cast it to a string and pass the string to the `client.esql.query()` endpoint:
32+
33+
```python
34+
>>> from elasticsearch import Elasticsearch
35+
>>> client = Elasticsearch(hosts=[os.environ['ELASTICSEARCH_URL']])
36+
>>> response = client.esql.query(query=str(query))
37+
```
38+
39+
The response body contains a `columns` attribute with the list of columns included in the results, and a `values` attribute with the list of results for the query, each given as a list of column values. Here is a possible response body returned by the example query given above:
40+
41+
```python
42+
>>> from pprint import pprint
43+
>>> pprint(response.body)
44+
{'columns': [{'name': 'first_name', 'type': 'text'},
45+
{'name': 'last_name', 'type': 'text'},
46+
{'name': 'height', 'type': 'double'},
47+
{'name': 'height_feet', 'type': 'double'},
48+
{'name': 'height_cm', 'type': 'double'}],
49+
'is_partial': False,
50+
'took': 11,
51+
'values': [['Adrian', 'Wells', 2.424, 7.953144, 242.4],
52+
['Aaron', 'Gonzalez', 1.584, 5.1971, 158.4],
53+
['Miranda', 'Kramer', 1.55, 5.08555, 155]]}
54+
```
55+
56+
## Creating an ES|QL query
57+
58+
To construct an ES|QL query you start from one of the ES|QL source commands:
59+
60+
### `ESQL.from_`
61+
62+
The `FROM` command selects the indices, data streams or aliases to be queried.
63+
64+
Examples:
65+
66+
```python
67+
from elasticsearch.esql import ESQL
68+
69+
# FROM employees
70+
query1 = ESQL.from_("employees")
71+
72+
# FROM <logs-{now/d}>
73+
query2 = ESQL.from_("<logs-{now/d}>")
74+
75+
# FROM employees-00001, other-employees-*
76+
query3 = ESQL.from_("employees-00001", "other-employees-*")
77+
78+
# FROM cluster_one:employees-00001, cluster_two:other-employees-*
79+
query4 = ESQL.from_("cluster_one:employees-00001", "cluster_two:other-employees-*")
80+
81+
# FROM employees METADATA _id
82+
query5 = ESQL.from_("employees").metadata("_id")
83+
```
84+
85+
Note how in the last example the optional `METADATA` clause of the `FROM` command is added as a chained method.
86+
87+
### `ESQL.row`
88+
89+
The `ROW` command produces a row with one or more columns, with the values that you specify.
90+
91+
Examples:
92+
93+
```python
94+
from elasticsearch.esql import ESQL, functions
95+
96+
# ROW a = 1, b = "two", c = null
97+
query1 = ESQL.row(a=1, b="two", c=None)
98+
99+
# ROW a = [1, 2]
100+
query2 = ESQL.row(a=[1, 2])
101+
102+
# ROW a = ROUND(1.23, 0)
103+
query3 = ESQL.row(a=functions.round(1.23, 0))
104+
```
105+
106+
### `ESQL.show`
107+
108+
The `SHOW` command returns information about the deployment and its capabilities.
109+
110+
Example:
111+
112+
```python
113+
from elasticsearch.esql import ESQL
114+
115+
# SHOW INFO
116+
query = ESQL.show("INFO")
117+
```
118+
119+
## Adding processing commands
120+
121+
Once you have a query object, you can add one or more processing commands to it. The following
122+
example shows how to create a query that uses the `WHERE` and `LIMIT` commands to filter the
123+
results:
124+
125+
```python
126+
from elasticsearch.esql import ESQL
127+
128+
# FROM employees
129+
# | WHERE still_hired == true
130+
# | LIMIT 10
131+
query = ESQL.from_("employees").where("still_hired == true").limit(10)
132+
```
133+
134+
For a complete list of available commands, review the methods of the [`ESQLBase` class](https://elasticsearch-py.readthedocs.io/en/stable/esql.html) in the Elasticsearch Python API documentation.
135+
136+
## Creating ES|QL Expressions and Conditions
137+
138+
The ES|QL query builder for Python provides two ways to create expressions and conditions in ES|QL queries.
139+
140+
The simplest option is to provide all ES|QL expressions and conditionals as strings. The following example uses this approach to add two calculated columns to the results using the `EVAL` command:
141+
142+
```python
143+
from elasticsearch.esql import ESQL
144+
145+
# FROM employees
146+
# | SORT emp_no
147+
# | KEEP first_name, last_name, height
148+
# | EVAL height_feet = height * 3.281, height_cm = height * 100
149+
query = (
150+
ESQL.from_("employees")
151+
.sort("emp_no")
152+
.keep("first_name", "last_name", "height")
153+
.eval(height_feet="height * 3.281", height_cm="height * 100")
154+
)
155+
```
156+
157+
A more advanced alternative is to replace the strings with Python expressions, which are automatically translated to ES|QL when the query object is rendered to a string. The following example is functionally equivalent to the one above:
158+
159+
```python
160+
from elasticsearch.esql import ESQL, E
161+
162+
# FROM employees
163+
# | SORT emp_no
164+
# | KEEP first_name, last_name, height
165+
# | EVAL height_feet = height * 3.281, height_cm = height * 100
166+
query = (
167+
ESQL.from_("employees")
168+
.sort("emp_no")
169+
.keep("first_name", "last_name", "height")
170+
.eval(height_feet=E("height") * 3.281, height_cm=E("height") * 100)
171+
)
172+
```
173+
174+
Here the `E()` helper function is used as a wrapper to the column name that initiates an ES|QL expression. The `E()` function transforms the given column into an ES|QL expression that can be modified with Python operators.
175+
176+
Here is a second example, which uses a conditional expression in the `WHERE` command:
177+
178+
```python
179+
from elasticsearch.esql import ESQL
180+
181+
# FROM employees
182+
# | KEEP first_name, last_name, height
183+
# | WHERE first_name == "Larry"
184+
query = (
185+
ESQL.from_("employees")
186+
.keep("first_name", "last_name", "height")
187+
.where('first_name == "Larry"')
188+
)
189+
```
190+
191+
Using Python syntax, the condition can be rewritten as follows:
192+
193+
```python
194+
from elasticsearch.esql import ESQL, E
195+
196+
# FROM employees
197+
# | KEEP first_name, last_name, height
198+
# | WHERE first_name == "Larry"
199+
query = (
200+
ESQL.from_("employees")
201+
.keep("first_name", "last_name", "height")
202+
.where(E("first_name") == "Larry")
203+
)
204+
```
205+
206+
## Using ES|QL functions
207+
208+
The ES|QL language includes a rich set of functions that can be used in expressions and conditionals. These can be included in expressions given as strings, as shown in the example below:
209+
210+
```python
211+
from elasticsearch.esql import ESQL
212+
213+
# FROM employees
214+
# | KEEP first_name, last_name, height
215+
# | WHERE LENGTH(first_name) < 4"
216+
query = (
217+
ESQL.from_("employees")
218+
.keep("first_name", "last_name", "height")
219+
.where("LENGTH(first_name) < 4")
220+
)
221+
```
222+
223+
All available ES|QL functions have Python wrappers in the `elasticsearch.esql.functions` module, which can be used when building expressions using Python syntax. Below is the example above coded using Python syntax:
224+
225+
```python
226+
from elasticsearch.esql import ESQL, functions
227+
228+
# FROM employees
229+
# | KEEP first_name, last_name, height
230+
# | WHERE LENGTH(first_name) < 4"
231+
query = (
232+
ESQL.from_("employees")
233+
.keep("first_name", "last_name", "height")
234+
.where(functions.length(E("first_name")) < 4)
235+
)
236+
```
237+
238+
Note that arguments passed to functions are assumed to be literals. When passing field names, it is necessary to wrap them with the `E()` helper function so that they are interpreted correctly.
239+
240+
You can find the complete list of available functions in the Python client's [ES|QL API reference documentation](https://elasticsearch-py.readthedocs.io/en/stable/esql.html#module-elasticsearch.esql.functions).

docs/reference/toc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ toc:
55
- file: connecting.md
66
- file: configuration.md
77
- file: querying.md
8+
- file: esql-query-builder.md
89
- file: async.md
910
- file: integrations.md
1011
children:

docs/sphinx/esql.rst

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
ES|QL Query Builder
2+
===================
3+
4+
Commands
5+
--------
6+
7+
.. autoclass:: elasticsearch.esql.ESQL
8+
:inherited-members:
9+
:members:
10+
11+
.. autoclass:: elasticsearch.esql.esql.ESQLBase
12+
:inherited-members:
13+
:members:
14+
:exclude-members: __init__
15+
16+
.. autoclass:: elasticsearch.esql.esql.From
17+
:members:
18+
:exclude-members: __init__
19+
20+
.. autoclass:: elasticsearch.esql.esql.Row
21+
:members:
22+
:exclude-members: __init__
23+
24+
.. autoclass:: elasticsearch.esql.esql.Show
25+
:members:
26+
:exclude-members: __init__
27+
28+
.. autoclass:: elasticsearch.esql.esql.ChangePoint
29+
:members:
30+
:exclude-members: __init__
31+
32+
.. autoclass:: elasticsearch.esql.esql.Completion
33+
:members:
34+
:exclude-members: __init__
35+
36+
.. autoclass:: elasticsearch.esql.esql.Dissect
37+
:members:
38+
:exclude-members: __init__
39+
40+
.. autoclass:: elasticsearch.esql.esql.Drop
41+
:members:
42+
:exclude-members: __init__
43+
44+
.. autoclass:: elasticsearch.esql.esql.Enrich
45+
:members:
46+
:exclude-members: __init__
47+
48+
.. autoclass:: elasticsearch.esql.esql.Eval
49+
:members:
50+
:exclude-members: __init__
51+
52+
.. autoclass:: elasticsearch.esql.esql.Fork
53+
:members:
54+
:exclude-members: __init__
55+
56+
.. autoclass:: elasticsearch.esql.esql.Grok
57+
:members:
58+
:exclude-members: __init__
59+
60+
.. autoclass:: elasticsearch.esql.esql.Keep
61+
:members:
62+
:exclude-members: __init__
63+
64+
.. autoclass:: elasticsearch.esql.esql.Limit
65+
:members:
66+
:exclude-members: __init__
67+
68+
.. autoclass:: elasticsearch.esql.esql.LookupJoin
69+
:members:
70+
:exclude-members: __init__
71+
72+
.. autoclass:: elasticsearch.esql.esql.MvExpand
73+
:members:
74+
:exclude-members: __init__
75+
76+
.. autoclass:: elasticsearch.esql.esql.Rename
77+
:members:
78+
:exclude-members: __init__
79+
80+
.. autoclass:: elasticsearch.esql.esql.Sample
81+
:members:
82+
:exclude-members: __init__
83+
84+
.. autoclass:: elasticsearch.esql.esql.Sort
85+
:members:
86+
:exclude-members: __init__
87+
88+
.. autoclass:: elasticsearch.esql.esql.Stats
89+
:members:
90+
:exclude-members: __init__
91+
92+
.. autoclass:: elasticsearch.esql.esql.Where
93+
:members:
94+
:exclude-members: __init__
95+
96+
Functions
97+
---------
98+
99+
.. automodule:: elasticsearch.esql.functions
100+
:members:

docs/sphinx/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ High-level documentation for this client is `also available <https://www.elastic
1111
:maxdepth: 2
1212

1313
es_api
14+
esql
1415
dsl
1516
api_helpers
1617
exceptions

elasticsearch/dsl/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from .aggs import A, Agg
2020
from .analysis import analyzer, char_filter, normalizer, token_filter, tokenizer
2121
from .document import AsyncDocument, Document
22-
from .document_base import InnerDoc, M, MetaField, mapped_field
22+
from .document_base import E, InnerDoc, M, MetaField, mapped_field
2323
from .exceptions import (
2424
ElasticsearchDslException,
2525
IllegalOperation,
@@ -135,6 +135,7 @@
135135
"Double",
136136
"DoubleRange",
137137
"DslBase",
138+
"E",
138139
"ElasticsearchDslException",
139140
"EmptySearch",
140141
"Facet",

0 commit comments

Comments
 (0)