Skip to content

Commit 124fa31

Browse files
add esql+dsl example
1 parent 89e00b4 commit 124fa31

File tree

10 files changed

+352
-13
lines changed

10 files changed

+352
-13
lines changed

docs/reference/dsl_how_to_guides.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,12 +1479,12 @@ Katherine Ramirez from Kimberlyton is 1.83m tall
14791479
...
14801480
```
14811481

1482-
To search for specific documents you can extend the base query with additional ES|QL commands that narrow the search criteria. The next example searches for documents that include only employees that are 2m tall or more, sorted by their last name. It also limits the results to 4 people:
1482+
To search for specific documents you can extend the base query with additional ES|QL commands that narrow the search criteria. The next example searches for documents that include only employees that are taller than 2 meters, sorted by their last name. It also limits the results to 4 people:
14831483

14841484
```python
14851485
query = (
14861486
Employee.esql_from()
1487-
.where(Employee.height >= 2)
1487+
.where(Employee.height > 2)
14881488
.sort(Employee.last_name)
14891489
.limit(4)
14901490
)

docs/reference/esql-query-builder.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ FROM employees
2828
| LIMIT 3
2929
```
3030

31-
To execute this query, you can cast it to a string and pass the string to the `client.esql.query()` endpoint:
31+
To execute this query, you can pass it to the `client.esql.query()` endpoint:
3232

3333
```python
3434
>>> from elasticsearch import Elasticsearch
3535
>>> client = Elasticsearch(hosts=[os.environ['ELASTICSEARCH_URL']])
36-
>>> response = client.esql.query(query=str(query))
36+
>>> response = client.esql.query(query=query)
3737
```
3838

3939
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:
@@ -216,7 +216,7 @@ def find_employee_by_name(name):
216216
.keep("first_name", "last_name", "height")
217217
.where(E("first_name") == E("?"))
218218
)
219-
return client.esql.query(query=str(query), params=[name])
219+
return client.esql.query(query=query, params=[name])
220220
```
221221

222222
Here the part of the query in which the untrusted data needs to be inserted is replaced with a parameter, which in ES|QL is defined by the question mark. When using Python expressions, the parameter must be given as `E("?")` so that it is treated as an expression and not as a literal string.

elasticsearch/esql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
# under the License.
1717

1818
from ..dsl import E # noqa: F401
19-
from .esql import ESQL, and_, not_, or_ # noqa: F401
19+
from .esql import ESQL, ESQLBase, and_, not_, or_ # noqa: F401

elasticsearch/esql/functions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ def min_over_time(field: ExpressionType) -> InstrumentedExpression:
649649

650650

651651
def multi_match(
652-
query: ExpressionType, fields: ExpressionType, options: ExpressionType = None
652+
query: ExpressionType, *fields: ExpressionType, options: ExpressionType = None
653653
) -> InstrumentedExpression:
654654
"""Use `MULTI_MATCH` to perform a multi-match query on the specified field.
655655
The multi_match query builds on the match query to allow multi-field queries.
@@ -661,11 +661,11 @@ def multi_match(
661661
"""
662662
if options is not None:
663663
return InstrumentedExpression(
664-
f"MULTI_MATCH({_render(query)}, {_render(fields)}, {_render(options)})"
664+
f'MULTI_MATCH({_render(query)}, {", ".join([_render(c) for c in fields])}, {_render(options)})'
665665
)
666666
else:
667667
return InstrumentedExpression(
668-
f"MULTI_MATCH({_render(query)}, {_render(fields)})"
668+
f'MULTI_MATCH({_render(query)}, {", ".join([_render(c) for c in fields])})'
669669
)
670670

671671

examples/dsl/async/esql_employees.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Licensed to Elasticsearch B.V. under one or more contributor
2+
# license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright
4+
# ownership. Elasticsearch B.V. licenses this file to you under
5+
# the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""
19+
# ES|QL query builder example
20+
21+
Requirements:
22+
23+
$ pip install "elasticsearch[async]" faker
24+
25+
To run the example:
26+
27+
$ python esql_employees.py "name to search"
28+
29+
The index will be created automatically with a list of 1000 randomly generated
30+
employees if it does not exist. Add `--recreate-index` or `-r` to the command
31+
to regenerate it.
32+
33+
Examples:
34+
35+
$ python esql_employees "Mark" # employees named Mark (first or last names)
36+
$ python esql_employees "Sarah" --limit 10 # up to 10 employees named Sarah
37+
$ python esql_employees "Sam" --sort height # sort results by height
38+
$ python esql_employees "Sam" --sort name # sort results by last name
39+
"""
40+
41+
import argparse
42+
import asyncio
43+
import os
44+
import random
45+
46+
from faker import Faker
47+
48+
from elasticsearch.dsl import AsyncDocument, InnerDoc, M, async_connections
49+
from elasticsearch.esql import ESQLBase
50+
from elasticsearch.esql.functions import concat, multi_match
51+
52+
fake = Faker()
53+
54+
55+
class Address(InnerDoc):
56+
address: M[str]
57+
city: M[str]
58+
zip_code: M[str]
59+
60+
61+
class Employee(AsyncDocument):
62+
emp_no: M[int]
63+
first_name: M[str]
64+
last_name: M[str]
65+
height: M[float]
66+
still_hired: M[bool]
67+
address: M[Address]
68+
69+
class Index:
70+
name = "employees"
71+
72+
@property
73+
def name(self) -> str:
74+
return f"{self.first_name} {self.last_name}"
75+
76+
def __repr__(self) -> str:
77+
return f"<Employee[{self.meta.id}]: {self.first_name} {self.last_name}>"
78+
79+
80+
async def create(num_employees: int = 1000) -> None:
81+
print("Creating a new employee index...")
82+
if await Employee._index.exists():
83+
await Employee._index.delete()
84+
await Employee.init()
85+
86+
for i in range(num_employees):
87+
address = Address(
88+
address=fake.address(), city=fake.city(), zip_code=fake.zipcode()
89+
)
90+
emp = Employee(
91+
emp_no=10000 + i,
92+
first_name=fake.first_name(),
93+
last_name=fake.last_name(),
94+
height=int((random.random() * 0.8 + 1.5) * 1000) / 1000,
95+
still_hired=random.random() >= 0.5,
96+
address=address,
97+
)
98+
await emp.save()
99+
await Employee._index.refresh()
100+
101+
102+
async def search(query: str, limit: int, sort: str) -> None:
103+
q: ESQLBase = (
104+
Employee.esql_from()
105+
.where(multi_match(query, Employee.first_name, Employee.last_name))
106+
.eval(full_name=concat(Employee.first_name, " ", Employee.last_name))
107+
)
108+
if sort == "height":
109+
q = q.sort(Employee.height.desc())
110+
else:
111+
q = q.sort(Employee.last_name.asc())
112+
q = q.limit(limit)
113+
async for result in Employee.esql_execute(q, return_additional=True):
114+
assert type(result) == tuple
115+
employee = result[0]
116+
full_name = result[1]["full_name"]
117+
print(
118+
f"{full_name:<20}",
119+
f"{'Hired' if employee.still_hired else 'Not hired':<10}",
120+
f"{employee.height:5.2f}m",
121+
f"{employee.address.city:<20}",
122+
)
123+
124+
125+
def parse_args() -> argparse.Namespace:
126+
parser = argparse.ArgumentParser(description="Employee ES|QL example")
127+
parser.add_argument(
128+
"--recreate-index",
129+
"-r",
130+
action="store_true",
131+
help="Recreate and populate the index",
132+
)
133+
parser.add_argument(
134+
"--limit",
135+
action="store",
136+
type=int,
137+
default=100,
138+
help="Maximum number or employees to return (default: 100)",
139+
)
140+
parser.add_argument(
141+
"--sort",
142+
action="store",
143+
type=str,
144+
default="name",
145+
help='Sort by "name" (ascending) or by "height" (descending) (default: name)',
146+
)
147+
parser.add_argument(
148+
"query", action="store", help="The name or partial name to search for"
149+
)
150+
return parser.parse_args()
151+
152+
153+
async def main() -> None:
154+
args = parse_args()
155+
156+
# initiate the default connection to elasticsearch
157+
async_connections.create_connection(hosts=[os.environ["ELASTICSEARCH_URL"]])
158+
159+
if args.recreate_index or not await Employee._index.exists():
160+
await create()
161+
await Employee.init()
162+
163+
await search(args.query, args.limit, args.sort)
164+
165+
# close the connection
166+
await async_connections.get_connection().close()
167+
168+
169+
if __name__ == "__main__":
170+
asyncio.run(main())

0 commit comments

Comments
 (0)