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