elasticsearch9 9.1.0__py3-none-any.whl → 9.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elasticsearch9/_async/client/__init__.py +19 -6
- elasticsearch9/_async/client/cat.py +610 -26
- elasticsearch9/_async/client/cluster.py +7 -2
- elasticsearch9/_async/client/esql.py +20 -6
- elasticsearch9/_async/client/indices.py +4 -4
- elasticsearch9/_async/client/inference.py +5 -4
- elasticsearch9/_async/client/sql.py +1 -1
- elasticsearch9/_async/client/transform.py +60 -0
- elasticsearch9/_sync/client/__init__.py +19 -6
- elasticsearch9/_sync/client/cat.py +610 -26
- elasticsearch9/_sync/client/cluster.py +7 -2
- elasticsearch9/_sync/client/esql.py +20 -6
- elasticsearch9/_sync/client/indices.py +4 -4
- elasticsearch9/_sync/client/inference.py +5 -4
- elasticsearch9/_sync/client/sql.py +1 -1
- elasticsearch9/_sync/client/transform.py +60 -0
- elasticsearch9/_version.py +1 -1
- elasticsearch9/dsl/_async/document.py +84 -0
- elasticsearch9/dsl/_sync/document.py +84 -0
- elasticsearch9/dsl/document_base.py +42 -0
- elasticsearch9/dsl/field.py +23 -10
- elasticsearch9/dsl/response/aggs.py +1 -1
- elasticsearch9/dsl/types.py +47 -10
- elasticsearch9/dsl/utils.py +1 -1
- elasticsearch9/esql/__init__.py +2 -1
- elasticsearch9/esql/esql.py +85 -34
- elasticsearch9/esql/functions.py +37 -25
- {elasticsearch9-9.1.0.dist-info → elasticsearch9-9.1.1.dist-info}/METADATA +1 -3
- {elasticsearch9-9.1.0.dist-info → elasticsearch9-9.1.1.dist-info}/RECORD +32 -33
- elasticsearch9/esql/esql1.py1 +0 -307
- {elasticsearch9-9.1.0.dist-info → elasticsearch9-9.1.1.dist-info}/WHEEL +0 -0
- {elasticsearch9-9.1.0.dist-info → elasticsearch9-9.1.1.dist-info}/licenses/LICENSE +0 -0
- {elasticsearch9-9.1.0.dist-info → elasticsearch9-9.1.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -374,8 +374,13 @@ class ClusterClient(NamespacedClient):
|
|
|
374
374
|
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-cluster-get-settings>`_
|
|
375
375
|
|
|
376
376
|
:param flat_settings: If `true`, returns settings in flat format.
|
|
377
|
-
:param include_defaults: If `true`, returns default
|
|
378
|
-
|
|
377
|
+
:param include_defaults: If `true`, also returns default values for all other
|
|
378
|
+
cluster settings, reflecting the values in the `elasticsearch.yml` file of
|
|
379
|
+
one of the nodes in the cluster. If the nodes in your cluster do not all
|
|
380
|
+
have the same values in their `elasticsearch.yml` config files then the values
|
|
381
|
+
returned by this API may vary from invocation to invocation and may not reflect
|
|
382
|
+
the values that Elasticsearch uses in all situations. Use the `GET _nodes/settings`
|
|
383
|
+
API to fetch the settings for each individual node in your cluster.
|
|
379
384
|
:param master_timeout: Period to wait for a connection to the master node. If
|
|
380
385
|
no response is received before the timeout expires, the request fails and
|
|
381
386
|
returns an error.
|
|
@@ -28,6 +28,9 @@ from .utils import (
|
|
|
28
28
|
_stability_warning,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
if t.TYPE_CHECKING:
|
|
32
|
+
from elasticsearch.esql import ESQLBase
|
|
33
|
+
|
|
31
34
|
|
|
32
35
|
class EsqlClient(NamespacedClient):
|
|
33
36
|
|
|
@@ -50,7 +53,7 @@ class EsqlClient(NamespacedClient):
|
|
|
50
53
|
def async_query(
|
|
51
54
|
self,
|
|
52
55
|
*,
|
|
53
|
-
query: t.Optional[str] = None,
|
|
56
|
+
query: t.Optional[t.Union[str, "ESQLBase"]] = None,
|
|
54
57
|
allow_partial_results: t.Optional[bool] = None,
|
|
55
58
|
columnar: t.Optional[bool] = None,
|
|
56
59
|
delimiter: t.Optional[str] = None,
|
|
@@ -111,7 +114,12 @@ class EsqlClient(NamespacedClient):
|
|
|
111
114
|
which has the name of all the columns.
|
|
112
115
|
:param filter: Specify a Query DSL query in the filter parameter to filter the
|
|
113
116
|
set of documents that an ES|QL query runs on.
|
|
114
|
-
:param format: A short version of the Accept header,
|
|
117
|
+
:param format: A short version of the Accept header, e.g. json, yaml. `csv`,
|
|
118
|
+
`tsv`, and `txt` formats will return results in a tabular format, excluding
|
|
119
|
+
other metadata fields from the response. For async requests, nothing will
|
|
120
|
+
be returned if the async query doesn't finish within the timeout. The query
|
|
121
|
+
ID and running status are available in the `X-Elasticsearch-Async-Id` and
|
|
122
|
+
`X-Elasticsearch-Async-Is-Running` HTTP headers of the response, respectively.
|
|
115
123
|
:param include_ccs_metadata: When set to `true` and performing a cross-cluster
|
|
116
124
|
query, the response will include an extra `_clusters` object with information
|
|
117
125
|
about the clusters that participated in the search along with info such as
|
|
@@ -165,7 +173,7 @@ class EsqlClient(NamespacedClient):
|
|
|
165
173
|
__query["pretty"] = pretty
|
|
166
174
|
if not __body:
|
|
167
175
|
if query is not None:
|
|
168
|
-
__body["query"] = query
|
|
176
|
+
__body["query"] = str(query)
|
|
169
177
|
if columnar is not None:
|
|
170
178
|
__body["columnar"] = columnar
|
|
171
179
|
if filter is not None:
|
|
@@ -405,6 +413,8 @@ class EsqlClient(NamespacedClient):
|
|
|
405
413
|
Returns an object extended information about a running ES|QL query.</p>
|
|
406
414
|
|
|
407
415
|
|
|
416
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-get-query>`_
|
|
417
|
+
|
|
408
418
|
:param id: The query ID
|
|
409
419
|
"""
|
|
410
420
|
if id in SKIP_IN_PATH:
|
|
@@ -446,6 +456,8 @@ class EsqlClient(NamespacedClient):
|
|
|
446
456
|
<p>Get running ES|QL queries information.
|
|
447
457
|
Returns an object containing IDs and other information about the running ES|QL queries.</p>
|
|
448
458
|
|
|
459
|
+
|
|
460
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-list-queries>`_
|
|
449
461
|
"""
|
|
450
462
|
__path_parts: t.Dict[str, str] = {}
|
|
451
463
|
__path = "/_query/queries"
|
|
@@ -484,7 +496,7 @@ class EsqlClient(NamespacedClient):
|
|
|
484
496
|
def query(
|
|
485
497
|
self,
|
|
486
498
|
*,
|
|
487
|
-
query: t.Optional[str] = None,
|
|
499
|
+
query: t.Optional[t.Union[str, "ESQLBase"]] = None,
|
|
488
500
|
allow_partial_results: t.Optional[bool] = None,
|
|
489
501
|
columnar: t.Optional[bool] = None,
|
|
490
502
|
delimiter: t.Optional[str] = None,
|
|
@@ -539,7 +551,9 @@ class EsqlClient(NamespacedClient):
|
|
|
539
551
|
`all_columns` which has the name of all columns.
|
|
540
552
|
:param filter: Specify a Query DSL query in the filter parameter to filter the
|
|
541
553
|
set of documents that an ES|QL query runs on.
|
|
542
|
-
:param format: A short version of the Accept header, e.g. json, yaml.
|
|
554
|
+
:param format: A short version of the Accept header, e.g. json, yaml. `csv`,
|
|
555
|
+
`tsv`, and `txt` formats will return results in a tabular format, excluding
|
|
556
|
+
other metadata fields from the response.
|
|
543
557
|
:param include_ccs_metadata: When set to `true` and performing a cross-cluster
|
|
544
558
|
query, the response will include an extra `_clusters` object with information
|
|
545
559
|
about the clusters that participated in the search along with info such as
|
|
@@ -579,7 +593,7 @@ class EsqlClient(NamespacedClient):
|
|
|
579
593
|
__query["pretty"] = pretty
|
|
580
594
|
if not __body:
|
|
581
595
|
if query is not None:
|
|
582
|
-
__body["query"] = query
|
|
596
|
+
__body["query"] = str(query)
|
|
583
597
|
if columnar is not None:
|
|
584
598
|
__body["columnar"] = columnar
|
|
585
599
|
if filter is not None:
|
|
@@ -1208,7 +1208,7 @@ class IndicesClient(NamespacedClient):
|
|
|
1208
1208
|
Removes the data stream options from a data stream.</p>
|
|
1209
1209
|
|
|
1210
1210
|
|
|
1211
|
-
`<https://www.elastic.co/
|
|
1211
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-delete-data-stream-options>`_
|
|
1212
1212
|
|
|
1213
1213
|
:param name: A comma-separated list of data streams of which the data stream
|
|
1214
1214
|
options will be deleted; use `*` to get all data streams
|
|
@@ -2568,7 +2568,7 @@ class IndicesClient(NamespacedClient):
|
|
|
2568
2568
|
<p>Get the data stream options configuration of one or more data streams.</p>
|
|
2569
2569
|
|
|
2570
2570
|
|
|
2571
|
-
`<https://www.elastic.co/
|
|
2571
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-get-data-stream-options>`_
|
|
2572
2572
|
|
|
2573
2573
|
:param name: Comma-separated list of data streams to limit the request. Supports
|
|
2574
2574
|
wildcards (`*`). To target all data streams, omit this parameter or use `*`
|
|
@@ -3684,7 +3684,7 @@ class IndicesClient(NamespacedClient):
|
|
|
3684
3684
|
Update the data stream options of the specified data streams.</p>
|
|
3685
3685
|
|
|
3686
3686
|
|
|
3687
|
-
`<https://www.elastic.co/
|
|
3687
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-data-stream-options>`_
|
|
3688
3688
|
|
|
3689
3689
|
:param name: Comma-separated list of data streams used to limit the request.
|
|
3690
3690
|
Supports wildcards (`*`). To target all data streams use `*` or `_all`.
|
|
@@ -4051,7 +4051,7 @@ class IndicesClient(NamespacedClient):
|
|
|
4051
4051
|
<li>Change a field's mapping using reindexing</li>
|
|
4052
4052
|
<li>Rename a field using a field alias</li>
|
|
4053
4053
|
</ul>
|
|
4054
|
-
<p>Learn how to use the update mapping API with practical examples in the <a href="https://www.elastic.co/docs
|
|
4054
|
+
<p>Learn how to use the update mapping API with practical examples in the <a href="https://www.elastic.co/docs/manage-data/data-store/mapping/update-mappings-examples">Update mapping API examples</a> guide.</p>
|
|
4055
4055
|
|
|
4056
4056
|
|
|
4057
4057
|
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-put-mapping>`_
|
|
@@ -396,17 +396,18 @@ class InferenceClient(NamespacedClient):
|
|
|
396
396
|
<li>Azure AI Studio (<code>completion</code>, <code>text_embedding</code>)</li>
|
|
397
397
|
<li>Azure OpenAI (<code>completion</code>, <code>text_embedding</code>)</li>
|
|
398
398
|
<li>Cohere (<code>completion</code>, <code>rerank</code>, <code>text_embedding</code>)</li>
|
|
399
|
-
<li>DeepSeek (<code>
|
|
399
|
+
<li>DeepSeek (<code>chat_completion</code>, <code>completion</code>)</li>
|
|
400
400
|
<li>Elasticsearch (<code>rerank</code>, <code>sparse_embedding</code>, <code>text_embedding</code> - this service is for built-in models and models uploaded through Eland)</li>
|
|
401
401
|
<li>ELSER (<code>sparse_embedding</code>)</li>
|
|
402
402
|
<li>Google AI Studio (<code>completion</code>, <code>text_embedding</code>)</li>
|
|
403
|
-
<li>Google Vertex AI (<code>rerank</code>, <code>text_embedding</code>)</li>
|
|
403
|
+
<li>Google Vertex AI (<code>chat_completion</code>, <code>completion</code>, <code>rerank</code>, <code>text_embedding</code>)</li>
|
|
404
404
|
<li>Hugging Face (<code>chat_completion</code>, <code>completion</code>, <code>rerank</code>, <code>text_embedding</code>)</li>
|
|
405
|
+
<li>JinaAI (<code>rerank</code>, <code>text_embedding</code>)</li>
|
|
406
|
+
<li>Llama (<code>chat_completion</code>, <code>completion</code>, <code>text_embedding</code>)</li>
|
|
405
407
|
<li>Mistral (<code>chat_completion</code>, <code>completion</code>, <code>text_embedding</code>)</li>
|
|
406
408
|
<li>OpenAI (<code>chat_completion</code>, <code>completion</code>, <code>text_embedding</code>)</li>
|
|
407
|
-
<li>VoyageAI (<code>
|
|
409
|
+
<li>VoyageAI (<code>rerank</code>, <code>text_embedding</code>)</li>
|
|
408
410
|
<li>Watsonx inference integration (<code>text_embedding</code>)</li>
|
|
409
|
-
<li>JinaAI (<code>text_embedding</code>, <code>rerank</code>)</li>
|
|
410
411
|
</ul>
|
|
411
412
|
|
|
412
413
|
|
|
@@ -283,7 +283,7 @@ class SqlClient(NamespacedClient):
|
|
|
283
283
|
keep_alive: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
|
|
284
284
|
keep_on_completion: t.Optional[bool] = None,
|
|
285
285
|
page_timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
|
|
286
|
-
params: t.Optional[t.
|
|
286
|
+
params: t.Optional[t.Sequence[t.Any]] = None,
|
|
287
287
|
pretty: t.Optional[bool] = None,
|
|
288
288
|
query: t.Optional[str] = None,
|
|
289
289
|
request_timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
|
|
@@ -602,6 +602,66 @@ class TransformClient(NamespacedClient):
|
|
|
602
602
|
path_parts=__path_parts,
|
|
603
603
|
)
|
|
604
604
|
|
|
605
|
+
@_rewrite_parameters()
|
|
606
|
+
def set_upgrade_mode(
|
|
607
|
+
self,
|
|
608
|
+
*,
|
|
609
|
+
enabled: t.Optional[bool] = None,
|
|
610
|
+
error_trace: t.Optional[bool] = None,
|
|
611
|
+
filter_path: t.Optional[t.Union[str, t.Sequence[str]]] = None,
|
|
612
|
+
human: t.Optional[bool] = None,
|
|
613
|
+
pretty: t.Optional[bool] = None,
|
|
614
|
+
timeout: t.Optional[t.Union[str, t.Literal[-1], t.Literal[0]]] = None,
|
|
615
|
+
) -> ObjectApiResponse[t.Any]:
|
|
616
|
+
"""
|
|
617
|
+
.. raw:: html
|
|
618
|
+
|
|
619
|
+
<p>Set upgrade_mode for transform indices.
|
|
620
|
+
Sets a cluster wide upgrade_mode setting that prepares transform
|
|
621
|
+
indices for an upgrade.
|
|
622
|
+
When upgrading your cluster, in some circumstances you must restart your
|
|
623
|
+
nodes and reindex your transform indices. In those circumstances,
|
|
624
|
+
there must be no transforms running. You can close the transforms,
|
|
625
|
+
do the upgrade, then open all the transforms again. Alternatively,
|
|
626
|
+
you can use this API to temporarily halt tasks associated with the transforms
|
|
627
|
+
and prevent new transforms from opening. You can also use this API
|
|
628
|
+
during upgrades that do not require you to reindex your transform
|
|
629
|
+
indices, though stopping transforms is not a requirement in that case.
|
|
630
|
+
You can see the current value for the upgrade_mode setting by using the get
|
|
631
|
+
transform info API.</p>
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
`<https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-transform-set-upgrade-mode>`_
|
|
635
|
+
|
|
636
|
+
:param enabled: When `true`, it enables `upgrade_mode` which temporarily halts
|
|
637
|
+
all transform tasks and prohibits new transform tasks from starting.
|
|
638
|
+
:param timeout: The time to wait for the request to be completed.
|
|
639
|
+
"""
|
|
640
|
+
__path_parts: t.Dict[str, str] = {}
|
|
641
|
+
__path = "/_transform/set_upgrade_mode"
|
|
642
|
+
__query: t.Dict[str, t.Any] = {}
|
|
643
|
+
if enabled is not None:
|
|
644
|
+
__query["enabled"] = enabled
|
|
645
|
+
if error_trace is not None:
|
|
646
|
+
__query["error_trace"] = error_trace
|
|
647
|
+
if filter_path is not None:
|
|
648
|
+
__query["filter_path"] = filter_path
|
|
649
|
+
if human is not None:
|
|
650
|
+
__query["human"] = human
|
|
651
|
+
if pretty is not None:
|
|
652
|
+
__query["pretty"] = pretty
|
|
653
|
+
if timeout is not None:
|
|
654
|
+
__query["timeout"] = timeout
|
|
655
|
+
__headers = {"accept": "application/json"}
|
|
656
|
+
return self.perform_request( # type: ignore[return-value]
|
|
657
|
+
"POST",
|
|
658
|
+
__path,
|
|
659
|
+
params=__query,
|
|
660
|
+
headers=__headers,
|
|
661
|
+
endpoint_id="transform.set_upgrade_mode",
|
|
662
|
+
path_parts=__path_parts,
|
|
663
|
+
)
|
|
664
|
+
|
|
605
665
|
@_rewrite_parameters(
|
|
606
666
|
parameter_aliases={"from": "from_"},
|
|
607
667
|
)
|
elasticsearch9/_version.py
CHANGED
|
@@ -20,6 +20,7 @@ from typing import (
|
|
|
20
20
|
TYPE_CHECKING,
|
|
21
21
|
Any,
|
|
22
22
|
AsyncIterable,
|
|
23
|
+
AsyncIterator,
|
|
23
24
|
Dict,
|
|
24
25
|
List,
|
|
25
26
|
Optional,
|
|
@@ -42,6 +43,7 @@ from .search import AsyncSearch
|
|
|
42
43
|
|
|
43
44
|
if TYPE_CHECKING:
|
|
44
45
|
from elasticsearch import AsyncElasticsearch
|
|
46
|
+
from elasticsearch.esql.esql import ESQLBase
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
class AsyncIndexMeta(DocumentMeta):
|
|
@@ -520,3 +522,85 @@ class AsyncDocument(DocumentBase, metaclass=AsyncIndexMeta):
|
|
|
520
522
|
return action
|
|
521
523
|
|
|
522
524
|
return await async_bulk(es, Generate(actions), **kwargs)
|
|
525
|
+
|
|
526
|
+
@classmethod
|
|
527
|
+
async def esql_execute(
|
|
528
|
+
cls,
|
|
529
|
+
query: "ESQLBase",
|
|
530
|
+
return_additional: bool = False,
|
|
531
|
+
ignore_missing_fields: bool = False,
|
|
532
|
+
using: Optional[AsyncUsingType] = None,
|
|
533
|
+
**kwargs: Any,
|
|
534
|
+
) -> AsyncIterator[Union[Self, Tuple[Self, Dict[str, Any]]]]:
|
|
535
|
+
"""
|
|
536
|
+
Execute the given ES|QL query and return an iterator of 2-element tuples,
|
|
537
|
+
where the first element is an instance of this ``Document`` and the
|
|
538
|
+
second a dictionary with any remaining columns requested in the query.
|
|
539
|
+
|
|
540
|
+
:arg query: an ES|QL query object created with the ``esql_from()`` method.
|
|
541
|
+
:arg return_additional: if ``False`` (the default), this method returns
|
|
542
|
+
document objects. If set to ``True``, the method returns tuples with
|
|
543
|
+
a document in the first element and a dictionary with any additional
|
|
544
|
+
columns returned by the query in the second element.
|
|
545
|
+
:arg ignore_missing_fields: if ``False`` (the default), all the fields of
|
|
546
|
+
the document must be present in the query, or else an exception is
|
|
547
|
+
raised. Set to ``True`` to allow missing fields, which will result in
|
|
548
|
+
partially initialized document objects.
|
|
549
|
+
:arg using: connection alias to use, defaults to ``'default'``
|
|
550
|
+
:arg kwargs: additional options for the ``client.esql.query()`` function.
|
|
551
|
+
"""
|
|
552
|
+
es = cls._get_connection(using)
|
|
553
|
+
response = await es.esql.query(query=str(query), **kwargs)
|
|
554
|
+
query_columns = [col["name"] for col in response.body.get("columns", [])]
|
|
555
|
+
|
|
556
|
+
# Here we get the list of columns defined in the document, which are the
|
|
557
|
+
# columns that we will take from each result to assemble the document
|
|
558
|
+
# object.
|
|
559
|
+
# When `for_esql=False` is passed below by default, the list will include
|
|
560
|
+
# nested fields, which ES|QL does not return, causing an error. When passing
|
|
561
|
+
# `ignore_missing_fields=True` the list will be generated with
|
|
562
|
+
# `for_esql=True`, so the error will not occur, but the documents will
|
|
563
|
+
# not have any Nested objects in them.
|
|
564
|
+
doc_fields = set(cls._get_field_names(for_esql=ignore_missing_fields))
|
|
565
|
+
if not ignore_missing_fields and not doc_fields.issubset(set(query_columns)):
|
|
566
|
+
raise ValueError(
|
|
567
|
+
f"Not all fields of {cls.__name__} were returned by the query. "
|
|
568
|
+
"Make sure your document does not use Nested fields, which are "
|
|
569
|
+
"currently not supported in ES|QL. To force the query to be "
|
|
570
|
+
"evaluated in spite of the missing fields, pass set the "
|
|
571
|
+
"ignore_missing_fields=True option in the esql_execute() call."
|
|
572
|
+
)
|
|
573
|
+
non_doc_fields: set[str] = set(query_columns) - doc_fields - {"_id"}
|
|
574
|
+
index_id = query_columns.index("_id")
|
|
575
|
+
|
|
576
|
+
results = response.body.get("values", [])
|
|
577
|
+
for column_values in results:
|
|
578
|
+
# create a dictionary with all the document fields, expanding the
|
|
579
|
+
# dot notation returned by ES|QL into the recursive dictionaries
|
|
580
|
+
# used by Document.from_dict()
|
|
581
|
+
doc_dict: Dict[str, Any] = {}
|
|
582
|
+
for col, val in zip(query_columns, column_values):
|
|
583
|
+
if col in doc_fields:
|
|
584
|
+
cols = col.split(".")
|
|
585
|
+
d = doc_dict
|
|
586
|
+
for c in cols[:-1]:
|
|
587
|
+
if c not in d:
|
|
588
|
+
d[c] = {}
|
|
589
|
+
d = d[c]
|
|
590
|
+
d[cols[-1]] = val
|
|
591
|
+
|
|
592
|
+
# create the document instance
|
|
593
|
+
obj = cls(meta={"_id": column_values[index_id]})
|
|
594
|
+
obj._from_dict(doc_dict)
|
|
595
|
+
|
|
596
|
+
if return_additional:
|
|
597
|
+
# build a dict with any other values included in the response
|
|
598
|
+
other = {
|
|
599
|
+
col: val
|
|
600
|
+
for col, val in zip(query_columns, column_values)
|
|
601
|
+
if col in non_doc_fields
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
yield obj, other
|
|
605
|
+
else:
|
|
606
|
+
yield obj
|
|
@@ -21,6 +21,7 @@ from typing import (
|
|
|
21
21
|
Any,
|
|
22
22
|
Dict,
|
|
23
23
|
Iterable,
|
|
24
|
+
Iterator,
|
|
24
25
|
List,
|
|
25
26
|
Optional,
|
|
26
27
|
Tuple,
|
|
@@ -42,6 +43,7 @@ from .search import Search
|
|
|
42
43
|
|
|
43
44
|
if TYPE_CHECKING:
|
|
44
45
|
from elasticsearch import Elasticsearch
|
|
46
|
+
from elasticsearch.esql.esql import ESQLBase
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
class IndexMeta(DocumentMeta):
|
|
@@ -512,3 +514,85 @@ class Document(DocumentBase, metaclass=IndexMeta):
|
|
|
512
514
|
return action
|
|
513
515
|
|
|
514
516
|
return bulk(es, Generate(actions), **kwargs)
|
|
517
|
+
|
|
518
|
+
@classmethod
|
|
519
|
+
def esql_execute(
|
|
520
|
+
cls,
|
|
521
|
+
query: "ESQLBase",
|
|
522
|
+
return_additional: bool = False,
|
|
523
|
+
ignore_missing_fields: bool = False,
|
|
524
|
+
using: Optional[UsingType] = None,
|
|
525
|
+
**kwargs: Any,
|
|
526
|
+
) -> Iterator[Union[Self, Tuple[Self, Dict[str, Any]]]]:
|
|
527
|
+
"""
|
|
528
|
+
Execute the given ES|QL query and return an iterator of 2-element tuples,
|
|
529
|
+
where the first element is an instance of this ``Document`` and the
|
|
530
|
+
second a dictionary with any remaining columns requested in the query.
|
|
531
|
+
|
|
532
|
+
:arg query: an ES|QL query object created with the ``esql_from()`` method.
|
|
533
|
+
:arg return_additional: if ``False`` (the default), this method returns
|
|
534
|
+
document objects. If set to ``True``, the method returns tuples with
|
|
535
|
+
a document in the first element and a dictionary with any additional
|
|
536
|
+
columns returned by the query in the second element.
|
|
537
|
+
:arg ignore_missing_fields: if ``False`` (the default), all the fields of
|
|
538
|
+
the document must be present in the query, or else an exception is
|
|
539
|
+
raised. Set to ``True`` to allow missing fields, which will result in
|
|
540
|
+
partially initialized document objects.
|
|
541
|
+
:arg using: connection alias to use, defaults to ``'default'``
|
|
542
|
+
:arg kwargs: additional options for the ``client.esql.query()`` function.
|
|
543
|
+
"""
|
|
544
|
+
es = cls._get_connection(using)
|
|
545
|
+
response = es.esql.query(query=str(query), **kwargs)
|
|
546
|
+
query_columns = [col["name"] for col in response.body.get("columns", [])]
|
|
547
|
+
|
|
548
|
+
# Here we get the list of columns defined in the document, which are the
|
|
549
|
+
# columns that we will take from each result to assemble the document
|
|
550
|
+
# object.
|
|
551
|
+
# When `for_esql=False` is passed below by default, the list will include
|
|
552
|
+
# nested fields, which ES|QL does not return, causing an error. When passing
|
|
553
|
+
# `ignore_missing_fields=True` the list will be generated with
|
|
554
|
+
# `for_esql=True`, so the error will not occur, but the documents will
|
|
555
|
+
# not have any Nested objects in them.
|
|
556
|
+
doc_fields = set(cls._get_field_names(for_esql=ignore_missing_fields))
|
|
557
|
+
if not ignore_missing_fields and not doc_fields.issubset(set(query_columns)):
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"Not all fields of {cls.__name__} were returned by the query. "
|
|
560
|
+
"Make sure your document does not use Nested fields, which are "
|
|
561
|
+
"currently not supported in ES|QL. To force the query to be "
|
|
562
|
+
"evaluated in spite of the missing fields, pass set the "
|
|
563
|
+
"ignore_missing_fields=True option in the esql_execute() call."
|
|
564
|
+
)
|
|
565
|
+
non_doc_fields: set[str] = set(query_columns) - doc_fields - {"_id"}
|
|
566
|
+
index_id = query_columns.index("_id")
|
|
567
|
+
|
|
568
|
+
results = response.body.get("values", [])
|
|
569
|
+
for column_values in results:
|
|
570
|
+
# create a dictionary with all the document fields, expanding the
|
|
571
|
+
# dot notation returned by ES|QL into the recursive dictionaries
|
|
572
|
+
# used by Document.from_dict()
|
|
573
|
+
doc_dict: Dict[str, Any] = {}
|
|
574
|
+
for col, val in zip(query_columns, column_values):
|
|
575
|
+
if col in doc_fields:
|
|
576
|
+
cols = col.split(".")
|
|
577
|
+
d = doc_dict
|
|
578
|
+
for c in cols[:-1]:
|
|
579
|
+
if c not in d:
|
|
580
|
+
d[c] = {}
|
|
581
|
+
d = d[c]
|
|
582
|
+
d[cols[-1]] = val
|
|
583
|
+
|
|
584
|
+
# create the document instance
|
|
585
|
+
obj = cls(meta={"_id": column_values[index_id]})
|
|
586
|
+
obj._from_dict(doc_dict)
|
|
587
|
+
|
|
588
|
+
if return_additional:
|
|
589
|
+
# build a dict with any other values included in the response
|
|
590
|
+
other = {
|
|
591
|
+
col: val
|
|
592
|
+
for col, val in zip(query_columns, column_values)
|
|
593
|
+
if col in non_doc_fields
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
yield obj, other
|
|
597
|
+
else:
|
|
598
|
+
yield obj
|
|
@@ -49,6 +49,7 @@ from .utils import DOC_META_FIELDS, ObjectBase
|
|
|
49
49
|
if TYPE_CHECKING:
|
|
50
50
|
from elastic_transport import ObjectApiResponse
|
|
51
51
|
|
|
52
|
+
from ..esql.esql import ESQLBase
|
|
52
53
|
from .index_base import IndexBase
|
|
53
54
|
|
|
54
55
|
|
|
@@ -602,3 +603,44 @@ class DocumentBase(ObjectBase):
|
|
|
602
603
|
|
|
603
604
|
meta["_source"] = d
|
|
604
605
|
return meta
|
|
606
|
+
|
|
607
|
+
@classmethod
|
|
608
|
+
def _get_field_names(
|
|
609
|
+
cls, for_esql: bool = False, nested_class: Optional[type[InnerDoc]] = None
|
|
610
|
+
) -> List[str]:
|
|
611
|
+
"""Return the list of field names used by this document.
|
|
612
|
+
If the document has nested objects, their fields are reported using dot
|
|
613
|
+
notation. If the ``for_esql`` argument is set to ``True``, the list omits
|
|
614
|
+
nested fields, which are currently unsupported in ES|QL.
|
|
615
|
+
"""
|
|
616
|
+
fields = []
|
|
617
|
+
class_ = nested_class or cls
|
|
618
|
+
for field_name in class_._doc_type.mapping:
|
|
619
|
+
field = class_._doc_type.mapping[field_name]
|
|
620
|
+
if isinstance(field, Object):
|
|
621
|
+
if for_esql and isinstance(field, Nested):
|
|
622
|
+
# ES|QL does not recognize Nested fields at this time
|
|
623
|
+
continue
|
|
624
|
+
sub_fields = cls._get_field_names(
|
|
625
|
+
for_esql=for_esql, nested_class=field._doc_class
|
|
626
|
+
)
|
|
627
|
+
for sub_field in sub_fields:
|
|
628
|
+
fields.append(f"{field_name}.{sub_field}")
|
|
629
|
+
else:
|
|
630
|
+
fields.append(field_name)
|
|
631
|
+
return fields
|
|
632
|
+
|
|
633
|
+
@classmethod
|
|
634
|
+
def esql_from(cls) -> "ESQLBase":
|
|
635
|
+
"""Return a base ES|QL query for instances of this document class.
|
|
636
|
+
|
|
637
|
+
The returned query is initialized with ``FROM`` and ``KEEP`` statements,
|
|
638
|
+
and can be completed as desired.
|
|
639
|
+
"""
|
|
640
|
+
from ..esql import ESQL # here to avoid circular imports
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
ESQL.from_(cls)
|
|
644
|
+
.metadata("_id")
|
|
645
|
+
.keep("_id", *tuple(cls._get_field_names(for_esql=True)))
|
|
646
|
+
)
|
elasticsearch9/dsl/field.py
CHANGED
|
@@ -119,9 +119,16 @@ class Field(DslBase):
|
|
|
119
119
|
def __getitem__(self, subfield: str) -> "Field":
|
|
120
120
|
return cast(Field, self._params.get("fields", {})[subfield])
|
|
121
121
|
|
|
122
|
-
def _serialize(self, data: Any) -> Any:
|
|
122
|
+
def _serialize(self, data: Any, skip_empty: bool) -> Any:
|
|
123
123
|
return data
|
|
124
124
|
|
|
125
|
+
def _safe_serialize(self, data: Any, skip_empty: bool) -> Any:
|
|
126
|
+
try:
|
|
127
|
+
return self._serialize(data, skip_empty)
|
|
128
|
+
except TypeError:
|
|
129
|
+
# older method signature, without skip_empty
|
|
130
|
+
return self._serialize(data) # type: ignore[call-arg]
|
|
131
|
+
|
|
125
132
|
def _deserialize(self, data: Any) -> Any:
|
|
126
133
|
return data
|
|
127
134
|
|
|
@@ -133,10 +140,16 @@ class Field(DslBase):
|
|
|
133
140
|
return AttrList([])
|
|
134
141
|
return self._empty()
|
|
135
142
|
|
|
136
|
-
def serialize(self, data: Any) -> Any:
|
|
143
|
+
def serialize(self, data: Any, skip_empty: bool = True) -> Any:
|
|
137
144
|
if isinstance(data, (list, AttrList, tuple)):
|
|
138
|
-
return list(
|
|
139
|
-
|
|
145
|
+
return list(
|
|
146
|
+
map(
|
|
147
|
+
self._safe_serialize,
|
|
148
|
+
cast(Iterable[Any], data),
|
|
149
|
+
[skip_empty] * len(data),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
return self._safe_serialize(data, skip_empty)
|
|
140
153
|
|
|
141
154
|
def deserialize(self, data: Any) -> Any:
|
|
142
155
|
if isinstance(data, (list, AttrList, tuple)):
|
|
@@ -186,7 +199,7 @@ class RangeField(Field):
|
|
|
186
199
|
data = {k: self._core_field.deserialize(v) for k, v in data.items()} # type: ignore[union-attr]
|
|
187
200
|
return Range(data)
|
|
188
201
|
|
|
189
|
-
def _serialize(self, data: Any) -> Optional[Dict[str, Any]]:
|
|
202
|
+
def _serialize(self, data: Any, skip_empty: bool) -> Optional[Dict[str, Any]]:
|
|
190
203
|
if data is None:
|
|
191
204
|
return None
|
|
192
205
|
if not isinstance(data, collections.abc.Mapping):
|
|
@@ -550,7 +563,7 @@ class Object(Field):
|
|
|
550
563
|
return self._wrap(data)
|
|
551
564
|
|
|
552
565
|
def _serialize(
|
|
553
|
-
self, data: Optional[Union[Dict[str, Any], "InnerDoc"]]
|
|
566
|
+
self, data: Optional[Union[Dict[str, Any], "InnerDoc"]], skip_empty: bool
|
|
554
567
|
) -> Optional[Dict[str, Any]]:
|
|
555
568
|
if data is None:
|
|
556
569
|
return None
|
|
@@ -559,7 +572,7 @@ class Object(Field):
|
|
|
559
572
|
if isinstance(data, collections.abc.Mapping):
|
|
560
573
|
return data
|
|
561
574
|
|
|
562
|
-
return data.to_dict()
|
|
575
|
+
return data.to_dict(skip_empty=skip_empty)
|
|
563
576
|
|
|
564
577
|
def clean(self, data: Any) -> Any:
|
|
565
578
|
data = super().clean(data)
|
|
@@ -768,7 +781,7 @@ class Binary(Field):
|
|
|
768
781
|
def _deserialize(self, data: Any) -> bytes:
|
|
769
782
|
return base64.b64decode(data)
|
|
770
783
|
|
|
771
|
-
def _serialize(self, data: Any) -> Optional[str]:
|
|
784
|
+
def _serialize(self, data: Any, skip_empty: bool) -> Optional[str]:
|
|
772
785
|
if data is None:
|
|
773
786
|
return None
|
|
774
787
|
return base64.b64encode(data).decode()
|
|
@@ -2619,7 +2632,7 @@ class Ip(Field):
|
|
|
2619
2632
|
# the ipaddress library for pypy only accepts unicode.
|
|
2620
2633
|
return ipaddress.ip_address(unicode(data))
|
|
2621
2634
|
|
|
2622
|
-
def _serialize(self, data: Any) -> Optional[str]:
|
|
2635
|
+
def _serialize(self, data: Any, skip_empty: bool) -> Optional[str]:
|
|
2623
2636
|
if data is None:
|
|
2624
2637
|
return None
|
|
2625
2638
|
return str(data)
|
|
@@ -3367,7 +3380,7 @@ class Percolator(Field):
|
|
|
3367
3380
|
def _deserialize(self, data: Any) -> "Query":
|
|
3368
3381
|
return Q(data) # type: ignore[no-any-return]
|
|
3369
3382
|
|
|
3370
|
-
def _serialize(self, data: Any) -> Optional[Dict[str, Any]]:
|
|
3383
|
+
def _serialize(self, data: Any, skip_empty: bool) -> Optional[Dict[str, Any]]:
|
|
3371
3384
|
if data is None:
|
|
3372
3385
|
return None
|
|
3373
3386
|
return data.to_dict() # type: ignore[no-any-return]
|