elasticsearch 8.17.1__py3-none-any.whl → 8.18.0__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 (139) hide show
  1. elasticsearch/__init__.py +2 -2
  2. elasticsearch/_async/client/__init__.py +2146 -859
  3. elasticsearch/_async/client/_base.py +0 -1
  4. elasticsearch/_async/client/async_search.py +44 -29
  5. elasticsearch/_async/client/autoscaling.py +32 -26
  6. elasticsearch/_async/client/cat.py +244 -169
  7. elasticsearch/_async/client/ccr.py +268 -128
  8. elasticsearch/_async/client/cluster.py +191 -164
  9. elasticsearch/_async/client/connector.py +212 -116
  10. elasticsearch/_async/client/dangling_indices.py +22 -16
  11. elasticsearch/_async/client/enrich.py +51 -11
  12. elasticsearch/_async/client/eql.py +54 -13
  13. elasticsearch/_async/client/esql.py +352 -4
  14. elasticsearch/_async/client/features.py +37 -27
  15. elasticsearch/_async/client/fleet.py +22 -10
  16. elasticsearch/_async/client/graph.py +10 -9
  17. elasticsearch/_async/client/ilm.py +108 -77
  18. elasticsearch/_async/client/indices.py +1112 -677
  19. elasticsearch/_async/client/inference.py +1875 -133
  20. elasticsearch/_async/client/ingest.py +83 -50
  21. elasticsearch/_async/client/license.py +90 -38
  22. elasticsearch/_async/client/logstash.py +20 -9
  23. elasticsearch/_async/client/migration.py +26 -17
  24. elasticsearch/_async/client/ml.py +642 -365
  25. elasticsearch/_async/client/monitoring.py +6 -3
  26. elasticsearch/_async/client/nodes.py +51 -53
  27. elasticsearch/_async/client/query_rules.py +59 -33
  28. elasticsearch/_async/client/rollup.py +124 -86
  29. elasticsearch/_async/client/search_application.py +60 -32
  30. elasticsearch/_async/client/searchable_snapshots.py +25 -12
  31. elasticsearch/_async/client/security.py +896 -558
  32. elasticsearch/_async/client/shutdown.py +34 -36
  33. elasticsearch/_async/client/simulate.py +22 -28
  34. elasticsearch/_async/client/slm.py +65 -40
  35. elasticsearch/_async/client/snapshot.py +190 -213
  36. elasticsearch/_async/client/sql.py +43 -22
  37. elasticsearch/_async/client/ssl.py +17 -18
  38. elasticsearch/_async/client/synonyms.py +58 -37
  39. elasticsearch/_async/client/tasks.py +77 -48
  40. elasticsearch/_async/client/text_structure.py +65 -56
  41. elasticsearch/_async/client/transform.py +124 -93
  42. elasticsearch/_async/client/watcher.py +116 -72
  43. elasticsearch/_async/client/xpack.py +18 -9
  44. elasticsearch/_async/helpers.py +1 -2
  45. elasticsearch/_sync/client/__init__.py +2146 -859
  46. elasticsearch/_sync/client/_base.py +0 -1
  47. elasticsearch/_sync/client/async_search.py +44 -29
  48. elasticsearch/_sync/client/autoscaling.py +32 -26
  49. elasticsearch/_sync/client/cat.py +244 -169
  50. elasticsearch/_sync/client/ccr.py +268 -128
  51. elasticsearch/_sync/client/cluster.py +191 -164
  52. elasticsearch/_sync/client/connector.py +212 -116
  53. elasticsearch/_sync/client/dangling_indices.py +22 -16
  54. elasticsearch/_sync/client/enrich.py +51 -11
  55. elasticsearch/_sync/client/eql.py +54 -13
  56. elasticsearch/_sync/client/esql.py +352 -4
  57. elasticsearch/_sync/client/features.py +37 -27
  58. elasticsearch/_sync/client/fleet.py +22 -10
  59. elasticsearch/_sync/client/graph.py +10 -9
  60. elasticsearch/_sync/client/ilm.py +108 -77
  61. elasticsearch/_sync/client/indices.py +1112 -677
  62. elasticsearch/_sync/client/inference.py +1875 -133
  63. elasticsearch/_sync/client/ingest.py +83 -50
  64. elasticsearch/_sync/client/license.py +90 -38
  65. elasticsearch/_sync/client/logstash.py +20 -9
  66. elasticsearch/_sync/client/migration.py +26 -17
  67. elasticsearch/_sync/client/ml.py +642 -365
  68. elasticsearch/_sync/client/monitoring.py +6 -3
  69. elasticsearch/_sync/client/nodes.py +51 -53
  70. elasticsearch/_sync/client/query_rules.py +59 -33
  71. elasticsearch/_sync/client/rollup.py +124 -86
  72. elasticsearch/_sync/client/search_application.py +60 -32
  73. elasticsearch/_sync/client/searchable_snapshots.py +25 -12
  74. elasticsearch/_sync/client/security.py +896 -558
  75. elasticsearch/_sync/client/shutdown.py +34 -36
  76. elasticsearch/_sync/client/simulate.py +22 -28
  77. elasticsearch/_sync/client/slm.py +65 -40
  78. elasticsearch/_sync/client/snapshot.py +190 -213
  79. elasticsearch/_sync/client/sql.py +43 -22
  80. elasticsearch/_sync/client/ssl.py +17 -18
  81. elasticsearch/_sync/client/synonyms.py +58 -37
  82. elasticsearch/_sync/client/tasks.py +77 -48
  83. elasticsearch/_sync/client/text_structure.py +65 -56
  84. elasticsearch/_sync/client/transform.py +124 -93
  85. elasticsearch/_sync/client/utils.py +1 -4
  86. elasticsearch/_sync/client/watcher.py +116 -72
  87. elasticsearch/_sync/client/xpack.py +18 -9
  88. elasticsearch/_version.py +1 -1
  89. elasticsearch/client.py +2 -0
  90. elasticsearch/dsl/__init__.py +203 -0
  91. elasticsearch/dsl/_async/__init__.py +16 -0
  92. elasticsearch/dsl/_async/document.py +522 -0
  93. elasticsearch/dsl/_async/faceted_search.py +50 -0
  94. elasticsearch/dsl/_async/index.py +639 -0
  95. elasticsearch/dsl/_async/mapping.py +49 -0
  96. elasticsearch/dsl/_async/search.py +233 -0
  97. elasticsearch/dsl/_async/update_by_query.py +47 -0
  98. elasticsearch/dsl/_sync/__init__.py +16 -0
  99. elasticsearch/dsl/_sync/document.py +514 -0
  100. elasticsearch/dsl/_sync/faceted_search.py +50 -0
  101. elasticsearch/dsl/_sync/index.py +597 -0
  102. elasticsearch/dsl/_sync/mapping.py +49 -0
  103. elasticsearch/dsl/_sync/search.py +226 -0
  104. elasticsearch/dsl/_sync/update_by_query.py +45 -0
  105. elasticsearch/dsl/aggs.py +3730 -0
  106. elasticsearch/dsl/analysis.py +341 -0
  107. elasticsearch/dsl/async_connections.py +37 -0
  108. elasticsearch/dsl/connections.py +142 -0
  109. elasticsearch/dsl/document.py +20 -0
  110. elasticsearch/dsl/document_base.py +444 -0
  111. elasticsearch/dsl/exceptions.py +32 -0
  112. elasticsearch/dsl/faceted_search.py +28 -0
  113. elasticsearch/dsl/faceted_search_base.py +489 -0
  114. elasticsearch/dsl/field.py +4254 -0
  115. elasticsearch/dsl/function.py +180 -0
  116. elasticsearch/dsl/index.py +23 -0
  117. elasticsearch/dsl/index_base.py +178 -0
  118. elasticsearch/dsl/mapping.py +19 -0
  119. elasticsearch/dsl/mapping_base.py +219 -0
  120. elasticsearch/dsl/query.py +2816 -0
  121. elasticsearch/dsl/response/__init__.py +388 -0
  122. elasticsearch/dsl/response/aggs.py +100 -0
  123. elasticsearch/dsl/response/hit.py +53 -0
  124. elasticsearch/dsl/search.py +20 -0
  125. elasticsearch/dsl/search_base.py +1040 -0
  126. elasticsearch/dsl/serializer.py +34 -0
  127. elasticsearch/dsl/types.py +6471 -0
  128. elasticsearch/dsl/update_by_query.py +19 -0
  129. elasticsearch/dsl/update_by_query_base.py +149 -0
  130. elasticsearch/dsl/utils.py +687 -0
  131. elasticsearch/dsl/wrappers.py +119 -0
  132. elasticsearch/helpers/actions.py +1 -1
  133. elasticsearch/helpers/vectorstore/_sync/vectorstore.py +4 -1
  134. {elasticsearch-8.17.1.dist-info → elasticsearch-8.18.0.dist-info}/METADATA +12 -2
  135. elasticsearch-8.18.0.dist-info/RECORD +161 -0
  136. elasticsearch-8.17.1.dist-info/RECORD +0 -119
  137. {elasticsearch-8.17.1.dist-info → elasticsearch-8.18.0.dist-info}/WHEEL +0 -0
  138. {elasticsearch-8.17.1.dist-info → elasticsearch-8.18.0.dist-info}/licenses/LICENSE +0 -0
  139. {elasticsearch-8.17.1.dist-info → elasticsearch-8.18.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,1040 @@
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
+ import collections.abc
19
+ import copy
20
+ from typing import (
21
+ TYPE_CHECKING,
22
+ Any,
23
+ Callable,
24
+ Dict,
25
+ Generic,
26
+ Iterator,
27
+ List,
28
+ Optional,
29
+ Protocol,
30
+ Tuple,
31
+ Type,
32
+ Union,
33
+ cast,
34
+ overload,
35
+ )
36
+
37
+ from typing_extensions import Self, TypeVar
38
+
39
+ from .aggs import A, Agg, AggBase
40
+ from .document_base import InstrumentedField
41
+ from .exceptions import IllegalOperation
42
+ from .query import Bool, Q, Query
43
+ from .response import Hit, Response
44
+ from .utils import _R, AnyUsingType, AttrDict, DslBase, recursive_to_dict
45
+
46
+ if TYPE_CHECKING:
47
+ from .field import Field, Object
48
+
49
+
50
+ class SupportsClone(Protocol):
51
+ def _clone(self) -> Self: ...
52
+
53
+
54
+ _S = TypeVar("_S", bound=SupportsClone)
55
+
56
+
57
+ class QueryProxy(Generic[_S]):
58
+ """
59
+ Simple proxy around DSL objects (queries) that can be called
60
+ (to add query/post_filter) and also allows attribute access which is proxied to
61
+ the wrapped query.
62
+ """
63
+
64
+ def __init__(self, search: _S, attr_name: str):
65
+ self._search = search
66
+ self._proxied: Optional[Query] = None
67
+ self._attr_name = attr_name
68
+
69
+ def __nonzero__(self) -> bool:
70
+ return self._proxied is not None
71
+
72
+ __bool__ = __nonzero__
73
+
74
+ def __call__(self, *args: Any, **kwargs: Any) -> _S:
75
+ s = self._search._clone()
76
+
77
+ # we cannot use self._proxied since we just cloned self._search and
78
+ # need to access the new self on the clone
79
+ proxied = getattr(s, self._attr_name)
80
+ if proxied._proxied is None:
81
+ proxied._proxied = Q(*args, **kwargs)
82
+ else:
83
+ proxied._proxied &= Q(*args, **kwargs)
84
+
85
+ # always return search to be chainable
86
+ return s
87
+
88
+ def __getattr__(self, attr_name: str) -> Any:
89
+ return getattr(self._proxied, attr_name)
90
+
91
+ def __setattr__(self, attr_name: str, value: Any) -> None:
92
+ if not attr_name.startswith("_"):
93
+ if self._proxied is not None:
94
+ self._proxied = Q(self._proxied.to_dict())
95
+ setattr(self._proxied, attr_name, value)
96
+ super().__setattr__(attr_name, value)
97
+
98
+ def __getstate__(self) -> Tuple[_S, Optional[Query], str]:
99
+ return self._search, self._proxied, self._attr_name
100
+
101
+ def __setstate__(self, state: Tuple[_S, Optional[Query], str]) -> None:
102
+ self._search, self._proxied, self._attr_name = state
103
+
104
+
105
+ class ProxyDescriptor(Generic[_S]):
106
+ """
107
+ Simple descriptor to enable setting of queries and filters as:
108
+
109
+ s = Search()
110
+ s.query = Q(...)
111
+
112
+ """
113
+
114
+ def __init__(self, name: str):
115
+ self._attr_name = f"_{name}_proxy"
116
+
117
+ def __get__(self, instance: Any, owner: object) -> QueryProxy[_S]:
118
+ return cast(QueryProxy[_S], getattr(instance, self._attr_name))
119
+
120
+ def __set__(self, instance: _S, value: Dict[str, Any]) -> None:
121
+ proxy: QueryProxy[_S] = getattr(instance, self._attr_name)
122
+ proxy._proxied = Q(value)
123
+
124
+
125
+ class AggsProxy(AggBase[_R], DslBase):
126
+ name = "aggs"
127
+
128
+ def __init__(self, search: "SearchBase[_R]"):
129
+ self._base = cast("Agg[_R]", self)
130
+ self._search = search
131
+ self._params = {"aggs": {}}
132
+
133
+ def to_dict(self) -> Dict[str, Any]:
134
+ return cast(Dict[str, Any], super().to_dict().get("aggs", {}))
135
+
136
+
137
+ class Request(Generic[_R]):
138
+ def __init__(
139
+ self,
140
+ using: AnyUsingType = "default",
141
+ index: Optional[Union[str, List[str]]] = None,
142
+ doc_type: Optional[
143
+ Union[type, str, List[Union[type, str]], Dict[str, Union[type, str]]]
144
+ ] = None,
145
+ extra: Optional[Dict[str, Any]] = None,
146
+ ):
147
+ self._using = using
148
+
149
+ self._index = None
150
+ if isinstance(index, (tuple, list)):
151
+ self._index = list(index)
152
+ elif index:
153
+ self._index = [index]
154
+
155
+ self._doc_type: List[Union[type, str]] = []
156
+ self._doc_type_map: Dict[str, Any] = {}
157
+ if isinstance(doc_type, (tuple, list)):
158
+ self._doc_type.extend(doc_type)
159
+ elif isinstance(doc_type, collections.abc.Mapping):
160
+ self._doc_type.extend(doc_type.keys())
161
+ self._doc_type_map.update(doc_type)
162
+ elif doc_type:
163
+ self._doc_type.append(doc_type)
164
+
165
+ self._params: Dict[str, Any] = {}
166
+ self._extra: Dict[str, Any] = extra or {}
167
+
168
+ def __eq__(self, other: Any) -> bool:
169
+ return (
170
+ isinstance(other, Request)
171
+ and other._params == self._params
172
+ and other._index == self._index
173
+ and other._doc_type == self._doc_type
174
+ and other.to_dict() == self.to_dict()
175
+ )
176
+
177
+ def __copy__(self) -> Self:
178
+ return self._clone()
179
+
180
+ def params(self, **kwargs: Any) -> Self:
181
+ """
182
+ Specify query params to be used when executing the search. All the
183
+ keyword arguments will override the current values. See
184
+ https://elasticsearch-py.readthedocs.io/en/latest/api/elasticsearch.html#elasticsearch.Elasticsearch.search
185
+ for all available parameters.
186
+
187
+ Example::
188
+
189
+ s = Search()
190
+ s = s.params(routing='user-1', preference='local')
191
+ """
192
+ s = self._clone()
193
+ s._params.update(kwargs)
194
+ return s
195
+
196
+ def index(self, *index: Union[str, List[str], Tuple[str, ...]]) -> Self:
197
+ """
198
+ Set the index for the search. If called empty it will remove all information.
199
+
200
+ Example::
201
+
202
+ s = Search()
203
+ s = s.index('twitter-2015.01.01', 'twitter-2015.01.02')
204
+ s = s.index(['twitter-2015.01.01', 'twitter-2015.01.02'])
205
+ """
206
+ # .index() resets
207
+ s = self._clone()
208
+ if not index:
209
+ s._index = None
210
+ else:
211
+ indexes = []
212
+ for i in index:
213
+ if isinstance(i, str):
214
+ indexes.append(i)
215
+ elif isinstance(i, list):
216
+ indexes += i
217
+ elif isinstance(i, tuple):
218
+ indexes += list(i)
219
+
220
+ s._index = (self._index or []) + indexes
221
+
222
+ return s
223
+
224
+ def _resolve_field(self, path: str) -> Optional["Field"]:
225
+ for dt in self._doc_type:
226
+ if not hasattr(dt, "_index"):
227
+ continue
228
+ field = dt._index.resolve_field(path)
229
+ if field is not None:
230
+ return cast("Field", field)
231
+ return None
232
+
233
+ def _resolve_nested(
234
+ self, hit: AttrDict[Any], parent_class: Optional[type] = None
235
+ ) -> Type[_R]:
236
+ doc_class = Hit
237
+
238
+ nested_path = []
239
+ nesting = hit["_nested"]
240
+ while nesting and "field" in nesting:
241
+ nested_path.append(nesting["field"])
242
+ nesting = nesting.get("_nested")
243
+ nested_path_str = ".".join(nested_path)
244
+
245
+ nested_field: Optional["Object"]
246
+ if parent_class is not None and hasattr(parent_class, "_index"):
247
+ nested_field = cast(
248
+ Optional["Object"], parent_class._index.resolve_field(nested_path_str)
249
+ )
250
+ else:
251
+ nested_field = cast(
252
+ Optional["Object"], self._resolve_field(nested_path_str)
253
+ )
254
+
255
+ if nested_field is not None:
256
+ return cast(Type[_R], nested_field._doc_class)
257
+
258
+ return cast(Type[_R], doc_class)
259
+
260
+ def _get_result(
261
+ self, hit: AttrDict[Any], parent_class: Optional[type] = None
262
+ ) -> _R:
263
+ doc_class: Any = Hit
264
+ dt = hit.get("_type")
265
+
266
+ if "_nested" in hit:
267
+ doc_class = self._resolve_nested(hit, parent_class)
268
+
269
+ elif dt in self._doc_type_map:
270
+ doc_class = self._doc_type_map[dt]
271
+
272
+ else:
273
+ for doc_type in self._doc_type:
274
+ if hasattr(doc_type, "_matches") and doc_type._matches(hit):
275
+ doc_class = doc_type
276
+ break
277
+
278
+ for t in hit.get("inner_hits", ()):
279
+ hit["inner_hits"][t] = Response[_R](
280
+ self, hit["inner_hits"][t], doc_class=doc_class
281
+ )
282
+
283
+ callback = getattr(doc_class, "from_es", doc_class)
284
+ return cast(_R, callback(hit))
285
+
286
+ def doc_type(
287
+ self, *doc_type: Union[type, str], **kwargs: Callable[[AttrDict[Any]], Any]
288
+ ) -> Self:
289
+ """
290
+ Set the type to search through. You can supply a single value or
291
+ multiple. Values can be strings or subclasses of ``Document``.
292
+
293
+ You can also pass in any keyword arguments, mapping a doc_type to a
294
+ callback that should be used instead of the Hit class.
295
+
296
+ If no doc_type is supplied any information stored on the instance will
297
+ be erased.
298
+
299
+ Example:
300
+
301
+ s = Search().doc_type('product', 'store', User, custom=my_callback)
302
+ """
303
+ # .doc_type() resets
304
+ s = self._clone()
305
+ if not doc_type and not kwargs:
306
+ s._doc_type = []
307
+ s._doc_type_map = {}
308
+ else:
309
+ s._doc_type.extend(doc_type)
310
+ s._doc_type.extend(kwargs.keys())
311
+ s._doc_type_map.update(kwargs)
312
+ return s
313
+
314
+ def using(self, client: AnyUsingType) -> Self:
315
+ """
316
+ Associate the search request with an elasticsearch client. A fresh copy
317
+ will be returned with current instance remaining unchanged.
318
+
319
+ :arg client: an instance of ``elasticsearch.Elasticsearch`` to use or
320
+ an alias to look up in ``elasticsearch.dsl.connections``
321
+
322
+ """
323
+ s = self._clone()
324
+ s._using = client
325
+ return s
326
+
327
+ def extra(self, **kwargs: Any) -> Self:
328
+ """
329
+ Add extra keys to the request body. Mostly here for backwards
330
+ compatibility.
331
+ """
332
+ s = self._clone()
333
+ if "from_" in kwargs:
334
+ kwargs["from"] = kwargs.pop("from_")
335
+ s._extra.update(kwargs)
336
+ return s
337
+
338
+ def _clone(self) -> Self:
339
+ s = self.__class__(
340
+ using=self._using, index=self._index, doc_type=self._doc_type
341
+ )
342
+ s._doc_type_map = self._doc_type_map.copy()
343
+ s._extra = self._extra.copy()
344
+ s._params = self._params.copy()
345
+ return s
346
+
347
+ if TYPE_CHECKING:
348
+
349
+ def to_dict(self) -> Dict[str, Any]: ...
350
+
351
+
352
+ class SearchBase(Request[_R]):
353
+ query = ProxyDescriptor[Self]("query")
354
+ post_filter = ProxyDescriptor[Self]("post_filter")
355
+ _response: Response[_R]
356
+
357
+ def __init__(self, **kwargs: Any):
358
+ """
359
+ Search request to elasticsearch.
360
+
361
+ :arg using: `Elasticsearch` instance to use
362
+ :arg index: limit the search to index
363
+ :arg doc_type: only query this type.
364
+
365
+ All the parameters supplied (or omitted) at creation type can be later
366
+ overridden by methods (`using`, `index` and `doc_type` respectively).
367
+ """
368
+ super().__init__(**kwargs)
369
+
370
+ self.aggs = AggsProxy[_R](self)
371
+ self._sort: List[Union[str, Dict[str, Dict[str, str]]]] = []
372
+ self._knn: List[Dict[str, Any]] = []
373
+ self._rank: Dict[str, Any] = {}
374
+ self._collapse: Dict[str, Any] = {}
375
+ self._source: Optional[Union[bool, List[str], Dict[str, List[str]]]] = None
376
+ self._highlight: Dict[str, Any] = {}
377
+ self._highlight_opts: Dict[str, Any] = {}
378
+ self._suggest: Dict[str, Any] = {}
379
+ self._script_fields: Dict[str, Any] = {}
380
+ self._response_class = Response[_R]
381
+
382
+ self._query_proxy = QueryProxy(self, "query")
383
+ self._post_filter_proxy = QueryProxy(self, "post_filter")
384
+
385
+ def filter(self, *args: Any, **kwargs: Any) -> Self:
386
+ return self.query(Bool(filter=[Q(*args, **kwargs)]))
387
+
388
+ def exclude(self, *args: Any, **kwargs: Any) -> Self:
389
+ return self.query(Bool(filter=[~Q(*args, **kwargs)]))
390
+
391
+ def __getitem__(self, n: Union[int, slice]) -> Self:
392
+ """
393
+ Support slicing the `Search` instance for pagination.
394
+
395
+ Slicing equates to the from/size parameters. E.g.::
396
+
397
+ s = Search().query(...)[0:25]
398
+
399
+ is equivalent to::
400
+
401
+ s = Search().query(...).extra(from_=0, size=25)
402
+
403
+ """
404
+ s = self._clone()
405
+
406
+ if isinstance(n, slice):
407
+ # If negative slicing, abort.
408
+ if n.start and n.start < 0 or n.stop and n.stop < 0:
409
+ raise ValueError("Search does not support negative slicing.")
410
+ slice_start = n.start
411
+ slice_stop = n.stop
412
+ else: # This is an index lookup, equivalent to slicing by [n:n+1].
413
+ # If negative index, abort.
414
+ if n < 0:
415
+ raise ValueError("Search does not support negative indexing.")
416
+ slice_start = n
417
+ slice_stop = n + 1
418
+
419
+ old_from = s._extra.get("from")
420
+ old_to = None
421
+ if "size" in s._extra:
422
+ old_to = (old_from or 0) + s._extra["size"]
423
+
424
+ new_from = old_from
425
+ if slice_start is not None:
426
+ new_from = (old_from or 0) + slice_start
427
+ new_to = old_to
428
+ if slice_stop is not None:
429
+ new_to = (old_from or 0) + slice_stop
430
+ if old_to is not None and old_to < new_to:
431
+ new_to = old_to
432
+
433
+ if new_from is not None:
434
+ s._extra["from"] = new_from
435
+ if new_to is not None:
436
+ s._extra["size"] = max(0, new_to - (new_from or 0))
437
+ return s
438
+
439
+ @classmethod
440
+ def from_dict(cls, d: Dict[str, Any]) -> Self:
441
+ """
442
+ Construct a new `Search` instance from a raw dict containing the search
443
+ body. Useful when migrating from raw dictionaries.
444
+
445
+ Example::
446
+
447
+ s = Search.from_dict({
448
+ "query": {
449
+ "bool": {
450
+ "must": [...]
451
+ }
452
+ },
453
+ "aggs": {...}
454
+ })
455
+ s = s.filter('term', published=True)
456
+ """
457
+ s = cls()
458
+ s.update_from_dict(d)
459
+ return s
460
+
461
+ def _clone(self) -> Self:
462
+ """
463
+ Return a clone of the current search request. Performs a shallow copy
464
+ of all the underlying objects. Used internally by most state modifying
465
+ APIs.
466
+ """
467
+ s = super()._clone()
468
+
469
+ s._response_class = self._response_class
470
+ s._knn = [knn.copy() for knn in self._knn]
471
+ s._rank = self._rank.copy()
472
+ s._collapse = self._collapse.copy()
473
+ s._sort = self._sort[:]
474
+ s._source = copy.copy(self._source) if self._source is not None else None
475
+ s._highlight = self._highlight.copy()
476
+ s._highlight_opts = self._highlight_opts.copy()
477
+ s._suggest = self._suggest.copy()
478
+ s._script_fields = self._script_fields.copy()
479
+ for x in ("query", "post_filter"):
480
+ getattr(s, x)._proxied = getattr(self, x)._proxied
481
+
482
+ # copy top-level bucket definitions
483
+ if self.aggs._params.get("aggs"):
484
+ s.aggs._params = {"aggs": self.aggs._params["aggs"].copy()}
485
+ return s
486
+
487
+ def response_class(self, cls: Type[Response[_R]]) -> Self:
488
+ """
489
+ Override the default wrapper used for the response.
490
+ """
491
+ s = self._clone()
492
+ s._response_class = cls
493
+ return s
494
+
495
+ def update_from_dict(self, d: Dict[str, Any]) -> Self:
496
+ """
497
+ Apply options from a serialized body to the current instance. Modifies
498
+ the object in-place. Used mostly by ``from_dict``.
499
+ """
500
+ d = d.copy()
501
+ if "query" in d:
502
+ self.query._proxied = Q(d.pop("query"))
503
+ if "post_filter" in d:
504
+ self.post_filter._proxied = Q(d.pop("post_filter"))
505
+
506
+ aggs = d.pop("aggs", d.pop("aggregations", {}))
507
+ if aggs:
508
+ self.aggs._params = {
509
+ "aggs": {name: A(value) for (name, value) in aggs.items()}
510
+ }
511
+ if "knn" in d:
512
+ self._knn = d.pop("knn")
513
+ if isinstance(self._knn, dict):
514
+ self._knn = [self._knn]
515
+ if "rank" in d:
516
+ self._rank = d.pop("rank")
517
+ if "collapse" in d:
518
+ self._collapse = d.pop("collapse")
519
+ if "sort" in d:
520
+ self._sort = d.pop("sort")
521
+ if "_source" in d:
522
+ self._source = d.pop("_source")
523
+ if "highlight" in d:
524
+ high = d.pop("highlight").copy()
525
+ self._highlight = high.pop("fields")
526
+ self._highlight_opts = high
527
+ if "suggest" in d:
528
+ self._suggest = d.pop("suggest")
529
+ if "text" in self._suggest:
530
+ text = self._suggest.pop("text")
531
+ for s in self._suggest.values():
532
+ s.setdefault("text", text)
533
+ if "script_fields" in d:
534
+ self._script_fields = d.pop("script_fields")
535
+ self._extra.update(d)
536
+ return self
537
+
538
+ def script_fields(self, **kwargs: Any) -> Self:
539
+ """
540
+ Define script fields to be calculated on hits. See
541
+ https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
542
+ for more details.
543
+
544
+ Example::
545
+
546
+ s = Search()
547
+ s = s.script_fields(times_two="doc['field'].value * 2")
548
+ s = s.script_fields(
549
+ times_three={
550
+ 'script': {
551
+ 'lang': 'painless',
552
+ 'source': "doc['field'].value * params.n",
553
+ 'params': {'n': 3}
554
+ }
555
+ }
556
+ )
557
+
558
+ """
559
+ s = self._clone()
560
+ for name in kwargs:
561
+ if isinstance(kwargs[name], str):
562
+ kwargs[name] = {"script": kwargs[name]}
563
+ s._script_fields.update(kwargs)
564
+ return s
565
+
566
+ def knn(
567
+ self,
568
+ field: Union[str, "InstrumentedField"],
569
+ k: int,
570
+ num_candidates: int,
571
+ query_vector: Optional[List[float]] = None,
572
+ query_vector_builder: Optional[Dict[str, Any]] = None,
573
+ boost: Optional[float] = None,
574
+ filter: Optional[Query] = None,
575
+ similarity: Optional[float] = None,
576
+ inner_hits: Optional[Dict[str, Any]] = None,
577
+ ) -> Self:
578
+ """
579
+ Add a k-nearest neighbor (kNN) search.
580
+
581
+ :arg field: the vector field to search against as a string or document class attribute
582
+ :arg k: number of nearest neighbors to return as top hits
583
+ :arg num_candidates: number of nearest neighbor candidates to consider per shard
584
+ :arg query_vector: the vector to search for
585
+ :arg query_vector_builder: A dictionary indicating how to build a query vector
586
+ :arg boost: A floating-point boost factor for kNN scores
587
+ :arg filter: query to filter the documents that can match
588
+ :arg similarity: the minimum similarity required for a document to be considered a match, as a float value
589
+ :arg inner_hits: retrieve hits from nested field
590
+
591
+ Example::
592
+
593
+ s = Search()
594
+ s = s.knn(field='embedding', k=5, num_candidates=10, query_vector=vector,
595
+ filter=Q('term', category='blog')))
596
+ """
597
+ s = self._clone()
598
+ s._knn.append(
599
+ {
600
+ "field": str(field), # str() is for InstrumentedField instances
601
+ "k": k,
602
+ "num_candidates": num_candidates,
603
+ }
604
+ )
605
+ if query_vector is None and query_vector_builder is None:
606
+ raise ValueError("one of query_vector and query_vector_builder is required")
607
+ if query_vector is not None and query_vector_builder is not None:
608
+ raise ValueError(
609
+ "only one of query_vector and query_vector_builder must be given"
610
+ )
611
+ if query_vector is not None:
612
+ s._knn[-1]["query_vector"] = cast(Any, query_vector)
613
+ if query_vector_builder is not None:
614
+ s._knn[-1]["query_vector_builder"] = query_vector_builder
615
+ if boost is not None:
616
+ s._knn[-1]["boost"] = boost
617
+ if filter is not None:
618
+ if isinstance(filter, Query):
619
+ s._knn[-1]["filter"] = filter.to_dict()
620
+ else:
621
+ s._knn[-1]["filter"] = filter
622
+ if similarity is not None:
623
+ s._knn[-1]["similarity"] = similarity
624
+ if inner_hits is not None:
625
+ s._knn[-1]["inner_hits"] = inner_hits
626
+ return s
627
+
628
+ def rank(self, rrf: Optional[Union[bool, Dict[str, Any]]] = None) -> Self:
629
+ """
630
+ Defines a method for combining and ranking results sets from a combination
631
+ of searches. Requires a minimum of 2 results sets.
632
+
633
+ :arg rrf: Set to ``True`` or an options dictionary to set the rank method to reciprocal rank fusion (RRF).
634
+
635
+ Example::
636
+
637
+ s = Search()
638
+ s = s.query('match', content='search text')
639
+ s = s.knn(field='embedding', k=5, num_candidates=10, query_vector=vector)
640
+ s = s.rank(rrf=True)
641
+
642
+ Note: This option is in technical preview and may change in the future. The syntax will likely change before GA.
643
+ """
644
+ s = self._clone()
645
+ s._rank = {}
646
+ if rrf is not None and rrf is not False:
647
+ s._rank["rrf"] = {} if rrf is True else rrf
648
+ return s
649
+
650
+ def source(
651
+ self,
652
+ fields: Optional[
653
+ Union[
654
+ bool,
655
+ str,
656
+ "InstrumentedField",
657
+ List[Union[str, "InstrumentedField"]],
658
+ Dict[str, List[Union[str, "InstrumentedField"]]],
659
+ ]
660
+ ] = None,
661
+ **kwargs: Any,
662
+ ) -> Self:
663
+ """
664
+ Selectively control how the _source field is returned.
665
+
666
+ :arg fields: field name, wildcard string, list of field names or wildcards,
667
+ or dictionary of includes and excludes
668
+ :arg kwargs: ``includes`` or ``excludes`` arguments, when ``fields`` is ``None``.
669
+
670
+ When no arguments are given, the entire document will be returned for
671
+ each hit. If ``fields`` is a string or list of strings, the field names or field
672
+ wildcards given will be included. If ``fields`` is a dictionary with keys of
673
+ 'includes' and/or 'excludes' the fields will be either included or excluded
674
+ appropriately.
675
+
676
+ Calling this multiple times with the same named parameter will override the
677
+ previous values with the new ones.
678
+
679
+ Example::
680
+
681
+ s = Search()
682
+ s = s.source(includes=['obj1.*'], excludes=["*.description"])
683
+
684
+ s = Search()
685
+ s = s.source(includes=['obj1.*']).source(excludes=["*.description"])
686
+
687
+ """
688
+ s = self._clone()
689
+
690
+ if fields and kwargs:
691
+ raise ValueError("You cannot specify fields and kwargs at the same time.")
692
+
693
+ @overload
694
+ def ensure_strings(fields: str) -> str: ...
695
+
696
+ @overload
697
+ def ensure_strings(fields: "InstrumentedField") -> str: ...
698
+
699
+ @overload
700
+ def ensure_strings(
701
+ fields: List[Union[str, "InstrumentedField"]],
702
+ ) -> List[str]: ...
703
+
704
+ @overload
705
+ def ensure_strings(
706
+ fields: Dict[str, List[Union[str, "InstrumentedField"]]],
707
+ ) -> Dict[str, List[str]]: ...
708
+
709
+ def ensure_strings(
710
+ fields: Union[
711
+ str,
712
+ "InstrumentedField",
713
+ List[Union[str, "InstrumentedField"]],
714
+ Dict[str, List[Union[str, "InstrumentedField"]]],
715
+ ],
716
+ ) -> Union[str, List[str], Dict[str, List[str]]]:
717
+ if isinstance(fields, dict):
718
+ return {k: ensure_strings(v) for k, v in fields.items()}
719
+ elif not isinstance(fields, (str, InstrumentedField)):
720
+ # we assume that if `fields` is not a any of [dict, str,
721
+ # InstrumentedField] then it is an iterable of strings or
722
+ # InstrumentedFields, so we convert them to a plain list of
723
+ # strings
724
+ return [str(f) for f in fields]
725
+ else:
726
+ return str(fields)
727
+
728
+ if fields is not None:
729
+ s._source = fields if isinstance(fields, bool) else ensure_strings(fields) # type: ignore[assignment]
730
+ return s
731
+
732
+ if kwargs and not isinstance(s._source, dict):
733
+ s._source = {}
734
+
735
+ if isinstance(s._source, dict):
736
+ for key, value in kwargs.items():
737
+ if value is None:
738
+ try:
739
+ del s._source[key]
740
+ except KeyError:
741
+ pass
742
+ else:
743
+ s._source[key] = ensure_strings(value)
744
+
745
+ return s
746
+
747
+ def sort(
748
+ self, *keys: Union[str, "InstrumentedField", Dict[str, Dict[str, str]]]
749
+ ) -> Self:
750
+ """
751
+ Add sorting information to the search request. If called without
752
+ arguments it will remove all sort requirements. Otherwise it will
753
+ replace them. Acceptable arguments are::
754
+
755
+ 'some.field'
756
+ '-some.other.field'
757
+ {'different.field': {'any': 'dict'}}
758
+
759
+ so for example::
760
+
761
+ s = Search().sort(
762
+ 'category',
763
+ '-title',
764
+ {"price" : {"order" : "asc", "mode" : "avg"}}
765
+ )
766
+
767
+ will sort by ``category``, ``title`` (in descending order) and
768
+ ``price`` in ascending order using the ``avg`` mode.
769
+
770
+ The API returns a copy of the Search object and can thus be chained.
771
+ """
772
+ s = self._clone()
773
+ s._sort = []
774
+ for k in keys:
775
+ if not isinstance(k, dict):
776
+ sort_field = str(k)
777
+ if sort_field.startswith("-"):
778
+ if sort_field[1:] == "_score":
779
+ raise IllegalOperation("Sorting by `-_score` is not allowed.")
780
+ s._sort.append({sort_field[1:]: {"order": "desc"}})
781
+ else:
782
+ s._sort.append(sort_field)
783
+ else:
784
+ s._sort.append(k)
785
+ return s
786
+
787
+ def collapse(
788
+ self,
789
+ field: Optional[Union[str, "InstrumentedField"]] = None,
790
+ inner_hits: Optional[Dict[str, Any]] = None,
791
+ max_concurrent_group_searches: Optional[int] = None,
792
+ ) -> Self:
793
+ """
794
+ Add collapsing information to the search request.
795
+ If called without providing ``field``, it will remove all collapse
796
+ requirements, otherwise it will replace them with the provided
797
+ arguments.
798
+ The API returns a copy of the Search object and can thus be chained.
799
+ """
800
+ s = self._clone()
801
+ s._collapse = {}
802
+
803
+ if field is None:
804
+ return s
805
+
806
+ s._collapse["field"] = str(field)
807
+ if inner_hits:
808
+ s._collapse["inner_hits"] = inner_hits
809
+ if max_concurrent_group_searches:
810
+ s._collapse["max_concurrent_group_searches"] = max_concurrent_group_searches
811
+ return s
812
+
813
+ def highlight_options(self, **kwargs: Any) -> Self:
814
+ """
815
+ Update the global highlighting options used for this request. For
816
+ example::
817
+
818
+ s = Search()
819
+ s = s.highlight_options(order='score')
820
+ """
821
+ s = self._clone()
822
+ s._highlight_opts.update(kwargs)
823
+ return s
824
+
825
+ def highlight(
826
+ self, *fields: Union[str, "InstrumentedField"], **kwargs: Any
827
+ ) -> Self:
828
+ """
829
+ Request highlighting of some fields. All keyword arguments passed in will be
830
+ used as parameters for all the fields in the ``fields`` parameter. Example::
831
+
832
+ Search().highlight('title', 'body', fragment_size=50)
833
+
834
+ will produce the equivalent of::
835
+
836
+ {
837
+ "highlight": {
838
+ "fields": {
839
+ "body": {"fragment_size": 50},
840
+ "title": {"fragment_size": 50}
841
+ }
842
+ }
843
+ }
844
+
845
+ If you want to have different options for different fields
846
+ you can call ``highlight`` twice::
847
+
848
+ Search().highlight('title', fragment_size=50).highlight('body', fragment_size=100)
849
+
850
+ which will produce::
851
+
852
+ {
853
+ "highlight": {
854
+ "fields": {
855
+ "body": {"fragment_size": 100},
856
+ "title": {"fragment_size": 50}
857
+ }
858
+ }
859
+ }
860
+
861
+ """
862
+ s = self._clone()
863
+ for f in fields:
864
+ s._highlight[str(f)] = kwargs
865
+ return s
866
+
867
+ def suggest(
868
+ self,
869
+ name: str,
870
+ text: Optional[str] = None,
871
+ regex: Optional[str] = None,
872
+ **kwargs: Any,
873
+ ) -> Self:
874
+ """
875
+ Add a suggestions request to the search.
876
+
877
+ :arg name: name of the suggestion
878
+ :arg text: text to suggest on
879
+
880
+ All keyword arguments will be added to the suggestions body. For example::
881
+
882
+ s = Search()
883
+ s = s.suggest('suggestion-1', 'Elasticsearch', term={'field': 'body'})
884
+
885
+ # regex query for Completion Suggester
886
+ s = Search()
887
+ s = s.suggest('suggestion-1', regex='py[thon|py]', completion={'field': 'body'})
888
+ """
889
+ if text is None and regex is None:
890
+ raise ValueError('You have to pass "text" or "regex" argument.')
891
+ if text and regex:
892
+ raise ValueError('You can only pass either "text" or "regex" argument.')
893
+ if regex and "completion" not in kwargs:
894
+ raise ValueError(
895
+ '"regex" argument must be passed with "completion" keyword argument.'
896
+ )
897
+
898
+ s = self._clone()
899
+ if regex:
900
+ s._suggest[name] = {"regex": regex}
901
+ elif text:
902
+ if "completion" in kwargs:
903
+ s._suggest[name] = {"prefix": text}
904
+ else:
905
+ s._suggest[name] = {"text": text}
906
+ s._suggest[name].update(kwargs)
907
+ return s
908
+
909
+ def search_after(self) -> Self:
910
+ """
911
+ Return a ``Search`` instance that retrieves the next page of results.
912
+
913
+ This method provides an easy way to paginate a long list of results using
914
+ the ``search_after`` option. For example::
915
+
916
+ page_size = 20
917
+ s = Search()[:page_size].sort("date")
918
+
919
+ while True:
920
+ # get a page of results
921
+ r = await s.execute()
922
+
923
+ # do something with this page of results
924
+
925
+ # exit the loop if we reached the end
926
+ if len(r.hits) < page_size:
927
+ break
928
+
929
+ # get a search object with the next page of results
930
+ s = s.search_after()
931
+
932
+ Note that the ``search_after`` option requires the search to have an
933
+ explicit ``sort`` order.
934
+ """
935
+ if not hasattr(self, "_response"):
936
+ raise ValueError("A search must be executed before using search_after")
937
+ return cast(Self, self._response.search_after())
938
+
939
+ def to_dict(self, count: bool = False, **kwargs: Any) -> Dict[str, Any]:
940
+ """
941
+ Serialize the search into the dictionary that will be sent over as the
942
+ request's body.
943
+
944
+ :arg count: a flag to specify if we are interested in a body for count -
945
+ no aggregations, no pagination bounds etc.
946
+
947
+ All additional keyword arguments will be included into the dictionary.
948
+ """
949
+ d = {}
950
+
951
+ if self.query:
952
+ d["query"] = recursive_to_dict(self.query)
953
+
954
+ if self._knn:
955
+ if len(self._knn) == 1:
956
+ d["knn"] = self._knn[0]
957
+ else:
958
+ d["knn"] = self._knn
959
+
960
+ if self._rank:
961
+ d["rank"] = self._rank
962
+
963
+ # count request doesn't care for sorting and other things
964
+ if not count:
965
+ if self.post_filter:
966
+ d["post_filter"] = recursive_to_dict(self.post_filter.to_dict())
967
+
968
+ if self.aggs.aggs:
969
+ d.update(recursive_to_dict(self.aggs.to_dict()))
970
+
971
+ if self._sort:
972
+ d["sort"] = self._sort
973
+
974
+ if self._collapse:
975
+ d["collapse"] = self._collapse
976
+
977
+ d.update(recursive_to_dict(self._extra))
978
+
979
+ if self._source not in (None, {}):
980
+ d["_source"] = self._source
981
+
982
+ if self._highlight:
983
+ d["highlight"] = {"fields": self._highlight}
984
+ d["highlight"].update(self._highlight_opts)
985
+
986
+ if self._suggest:
987
+ d["suggest"] = self._suggest
988
+
989
+ if self._script_fields:
990
+ d["script_fields"] = self._script_fields
991
+
992
+ d.update(recursive_to_dict(kwargs))
993
+ return d
994
+
995
+
996
+ class MultiSearchBase(Request[_R]):
997
+ """
998
+ Combine multiple :class:`~elasticsearch.dsl.Search` objects into a single
999
+ request.
1000
+ """
1001
+
1002
+ def __init__(self, **kwargs: Any):
1003
+ super().__init__(**kwargs)
1004
+ self._searches: List[SearchBase[_R]] = []
1005
+
1006
+ def __getitem__(self, key: Union[int, slice]) -> Any:
1007
+ return self._searches[key]
1008
+
1009
+ def __iter__(self) -> Iterator[SearchBase[_R]]:
1010
+ return iter(self._searches)
1011
+
1012
+ def _clone(self) -> Self:
1013
+ ms = super()._clone()
1014
+ ms._searches = self._searches[:]
1015
+ return ms
1016
+
1017
+ def add(self, search: SearchBase[_R]) -> Self:
1018
+ """
1019
+ Adds a new :class:`~elasticsearch.dsl.Search` object to the request::
1020
+
1021
+ ms = MultiSearch(index='my-index')
1022
+ ms = ms.add(Search(doc_type=Category).filter('term', category='python'))
1023
+ ms = ms.add(Search(doc_type=Blog))
1024
+ """
1025
+ ms = self._clone()
1026
+ ms._searches.append(search)
1027
+ return ms
1028
+
1029
+ def to_dict(self) -> List[Dict[str, Any]]: # type: ignore[override]
1030
+ out: List[Dict[str, Any]] = []
1031
+ for s in self._searches:
1032
+ meta: Dict[str, Any] = {}
1033
+ if s._index:
1034
+ meta["index"] = cast(Any, s._index)
1035
+ meta.update(s._params)
1036
+
1037
+ out.append(meta)
1038
+ out.append(s.to_dict())
1039
+
1040
+ return out