elasticsearch 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.
Files changed (33) hide show
  1. elasticsearch/_async/client/__init__.py +19 -6
  2. elasticsearch/_async/client/cat.py +610 -26
  3. elasticsearch/_async/client/cluster.py +7 -2
  4. elasticsearch/_async/client/esql.py +20 -6
  5. elasticsearch/_async/client/indices.py +4 -4
  6. elasticsearch/_async/client/inference.py +5 -4
  7. elasticsearch/_async/client/sql.py +1 -1
  8. elasticsearch/_async/client/transform.py +60 -0
  9. elasticsearch/_sync/client/__init__.py +19 -6
  10. elasticsearch/_sync/client/cat.py +610 -26
  11. elasticsearch/_sync/client/cluster.py +7 -2
  12. elasticsearch/_sync/client/esql.py +20 -6
  13. elasticsearch/_sync/client/indices.py +4 -4
  14. elasticsearch/_sync/client/inference.py +5 -4
  15. elasticsearch/_sync/client/sql.py +1 -1
  16. elasticsearch/_sync/client/transform.py +60 -0
  17. elasticsearch/_version.py +1 -1
  18. elasticsearch/dsl/_async/document.py +84 -0
  19. elasticsearch/dsl/_sync/document.py +84 -0
  20. elasticsearch/dsl/document_base.py +42 -0
  21. elasticsearch/dsl/field.py +23 -10
  22. elasticsearch/dsl/response/aggs.py +1 -1
  23. elasticsearch/dsl/types.py +47 -10
  24. elasticsearch/dsl/utils.py +1 -1
  25. elasticsearch/esql/__init__.py +2 -1
  26. elasticsearch/esql/esql.py +85 -34
  27. elasticsearch/esql/functions.py +37 -25
  28. {elasticsearch-9.1.0.dist-info → elasticsearch-9.1.1.dist-info}/METADATA +1 -3
  29. {elasticsearch-9.1.0.dist-info → elasticsearch-9.1.1.dist-info}/RECORD +32 -33
  30. elasticsearch/esql/esql1.py1 +0 -307
  31. {elasticsearch-9.1.0.dist-info → elasticsearch-9.1.1.dist-info}/WHEEL +0 -0
  32. {elasticsearch-9.1.0.dist-info → elasticsearch-9.1.1.dist-info}/licenses/LICENSE +0 -0
  33. {elasticsearch-9.1.0.dist-info → elasticsearch-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 cluster settings from the
378
- local node.
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, for example `json` or `yaml`.
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/guide/en/elasticsearch/reference/9.1/index.html>`_
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/guide/en/elasticsearch/reference/9.1/index.html>`_
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/guide/en/elasticsearch/reference/9.1/index.html>`_
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//manage-data/data-store/mapping/update-mappings-examples">Update mapping API examples</a> guide.</p>
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>completion</code>, <code>chat_completion</code>)</li>
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>text_embedding</code>, <code>rerank</code>)</li>
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.Mapping[str, t.Any]] = None,
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
  )
elasticsearch/_version.py CHANGED
@@ -15,4 +15,4 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
 
18
- __versionstr__ = "9.1.0"
18
+ __versionstr__ = "9.1.1"
@@ -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
+ )
@@ -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(map(self._serialize, cast(Iterable[Any], data)))
139
- return self._serialize(data)
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]
@@ -63,7 +63,7 @@ class BucketData(AggResponse[_R]):
63
63
  )
64
64
 
65
65
  def __iter__(self) -> Iterator["Agg"]: # type: ignore[override]
66
- return iter(self.buckets) # type: ignore[arg-type]
66
+ return iter(self.buckets)
67
67
 
68
68
  def __len__(self) -> int:
69
69
  return len(self.buckets)