elasticsearch 8.17.1__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.
- elasticsearch/__init__.py +2 -2
- elasticsearch/_async/client/__init__.py +2125 -1053
- elasticsearch/_async/client/_base.py +1 -2
- elasticsearch/_async/client/async_search.py +46 -35
- elasticsearch/_async/client/autoscaling.py +32 -26
- elasticsearch/_async/client/cat.py +244 -176
- elasticsearch/_async/client/ccr.py +268 -128
- elasticsearch/_async/client/cluster.py +191 -164
- elasticsearch/_async/client/connector.py +226 -116
- elasticsearch/_async/client/dangling_indices.py +22 -16
- elasticsearch/_async/client/enrich.py +51 -11
- elasticsearch/_async/client/eql.py +54 -13
- elasticsearch/_async/client/esql.py +351 -7
- elasticsearch/_async/client/features.py +37 -27
- elasticsearch/_async/client/fleet.py +32 -22
- elasticsearch/_async/client/graph.py +10 -9
- elasticsearch/_async/client/ilm.py +115 -77
- elasticsearch/_async/client/indices.py +1119 -772
- elasticsearch/_async/client/inference.py +1933 -84
- elasticsearch/_async/client/ingest.py +83 -50
- elasticsearch/_async/client/license.py +90 -38
- elasticsearch/_async/client/logstash.py +20 -9
- elasticsearch/_async/client/migration.py +26 -17
- elasticsearch/_async/client/ml.py +646 -374
- elasticsearch/_async/client/monitoring.py +6 -3
- elasticsearch/_async/client/nodes.py +52 -54
- elasticsearch/_async/client/query_rules.py +59 -33
- elasticsearch/_async/client/rollup.py +124 -86
- elasticsearch/_async/client/search_application.py +60 -32
- elasticsearch/_async/client/searchable_snapshots.py +25 -12
- elasticsearch/_async/client/security.py +903 -562
- elasticsearch/_async/client/shutdown.py +34 -36
- elasticsearch/_async/client/simulate.py +22 -28
- elasticsearch/_async/client/slm.py +65 -40
- elasticsearch/_async/client/snapshot.py +454 -327
- elasticsearch/_async/client/sql.py +43 -22
- elasticsearch/_async/client/ssl.py +17 -18
- elasticsearch/_async/client/synonyms.py +58 -37
- elasticsearch/_async/client/tasks.py +77 -48
- elasticsearch/_async/client/text_structure.py +65 -56
- elasticsearch/_async/client/transform.py +124 -93
- elasticsearch/_async/client/watcher.py +117 -73
- elasticsearch/_async/client/xpack.py +18 -9
- elasticsearch/_async/helpers.py +1 -2
- elasticsearch/_sync/client/__init__.py +2125 -1053
- elasticsearch/_sync/client/_base.py +1 -2
- elasticsearch/_sync/client/async_search.py +46 -35
- elasticsearch/_sync/client/autoscaling.py +32 -26
- elasticsearch/_sync/client/cat.py +244 -176
- elasticsearch/_sync/client/ccr.py +268 -128
- elasticsearch/_sync/client/cluster.py +191 -164
- elasticsearch/_sync/client/connector.py +226 -116
- elasticsearch/_sync/client/dangling_indices.py +22 -16
- elasticsearch/_sync/client/enrich.py +51 -11
- elasticsearch/_sync/client/eql.py +54 -13
- elasticsearch/_sync/client/esql.py +351 -7
- elasticsearch/_sync/client/features.py +37 -27
- elasticsearch/_sync/client/fleet.py +32 -22
- elasticsearch/_sync/client/graph.py +10 -9
- elasticsearch/_sync/client/ilm.py +115 -77
- elasticsearch/_sync/client/indices.py +1119 -772
- elasticsearch/_sync/client/inference.py +1933 -84
- elasticsearch/_sync/client/ingest.py +83 -50
- elasticsearch/_sync/client/license.py +90 -38
- elasticsearch/_sync/client/logstash.py +20 -9
- elasticsearch/_sync/client/migration.py +26 -17
- elasticsearch/_sync/client/ml.py +646 -374
- elasticsearch/_sync/client/monitoring.py +6 -3
- elasticsearch/_sync/client/nodes.py +52 -54
- elasticsearch/_sync/client/query_rules.py +59 -33
- elasticsearch/_sync/client/rollup.py +124 -86
- elasticsearch/_sync/client/search_application.py +60 -32
- elasticsearch/_sync/client/searchable_snapshots.py +25 -12
- elasticsearch/_sync/client/security.py +903 -562
- elasticsearch/_sync/client/shutdown.py +34 -36
- elasticsearch/_sync/client/simulate.py +22 -28
- elasticsearch/_sync/client/slm.py +65 -40
- elasticsearch/_sync/client/snapshot.py +454 -327
- elasticsearch/_sync/client/sql.py +43 -22
- elasticsearch/_sync/client/ssl.py +17 -18
- elasticsearch/_sync/client/synonyms.py +58 -37
- elasticsearch/_sync/client/tasks.py +77 -48
- elasticsearch/_sync/client/text_structure.py +65 -56
- elasticsearch/_sync/client/transform.py +124 -93
- elasticsearch/_sync/client/utils.py +1 -41
- elasticsearch/_sync/client/watcher.py +117 -73
- elasticsearch/_sync/client/xpack.py +18 -9
- elasticsearch/_version.py +1 -1
- elasticsearch/client.py +2 -0
- elasticsearch/dsl/__init__.py +203 -0
- elasticsearch/dsl/_async/__init__.py +16 -0
- elasticsearch/dsl/_async/document.py +522 -0
- elasticsearch/dsl/_async/faceted_search.py +50 -0
- elasticsearch/dsl/_async/index.py +639 -0
- elasticsearch/dsl/_async/mapping.py +49 -0
- elasticsearch/dsl/_async/search.py +237 -0
- elasticsearch/dsl/_async/update_by_query.py +47 -0
- elasticsearch/dsl/_sync/__init__.py +16 -0
- elasticsearch/dsl/_sync/document.py +514 -0
- elasticsearch/dsl/_sync/faceted_search.py +50 -0
- elasticsearch/dsl/_sync/index.py +597 -0
- elasticsearch/dsl/_sync/mapping.py +49 -0
- elasticsearch/dsl/_sync/search.py +230 -0
- elasticsearch/dsl/_sync/update_by_query.py +45 -0
- elasticsearch/dsl/aggs.py +3734 -0
- elasticsearch/dsl/analysis.py +341 -0
- elasticsearch/dsl/async_connections.py +37 -0
- elasticsearch/dsl/connections.py +142 -0
- elasticsearch/dsl/document.py +20 -0
- elasticsearch/dsl/document_base.py +444 -0
- elasticsearch/dsl/exceptions.py +32 -0
- elasticsearch/dsl/faceted_search.py +28 -0
- elasticsearch/dsl/faceted_search_base.py +489 -0
- elasticsearch/dsl/field.py +4392 -0
- elasticsearch/dsl/function.py +180 -0
- elasticsearch/dsl/index.py +23 -0
- elasticsearch/dsl/index_base.py +178 -0
- elasticsearch/dsl/mapping.py +19 -0
- elasticsearch/dsl/mapping_base.py +219 -0
- elasticsearch/dsl/query.py +2822 -0
- elasticsearch/dsl/response/__init__.py +388 -0
- elasticsearch/dsl/response/aggs.py +100 -0
- elasticsearch/dsl/response/hit.py +53 -0
- elasticsearch/dsl/search.py +20 -0
- elasticsearch/dsl/search_base.py +1053 -0
- elasticsearch/dsl/serializer.py +34 -0
- elasticsearch/dsl/types.py +6453 -0
- elasticsearch/dsl/update_by_query.py +19 -0
- elasticsearch/dsl/update_by_query_base.py +149 -0
- elasticsearch/dsl/utils.py +687 -0
- elasticsearch/dsl/wrappers.py +144 -0
- elasticsearch/helpers/actions.py +1 -1
- elasticsearch/helpers/vectorstore/_async/strategies.py +12 -12
- elasticsearch/helpers/vectorstore/_sync/strategies.py +12 -12
- elasticsearch/helpers/vectorstore/_sync/vectorstore.py +4 -1
- {elasticsearch-8.17.1.dist-info → elasticsearch-9.0.0.dist-info}/METADATA +12 -15
- elasticsearch-9.0.0.dist-info/RECORD +160 -0
- elasticsearch/transport.py +0 -57
- elasticsearch-8.17.1.dist-info/RECORD +0 -119
- {elasticsearch-8.17.1.dist-info → elasticsearch-9.0.0.dist-info}/WHEEL +0 -0
- {elasticsearch-8.17.1.dist-info → elasticsearch-9.0.0.dist-info}/licenses/LICENSE +0 -0
- {elasticsearch-8.17.1.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
|