elasticsearch 8.17.2__py3-none-any.whl → 8.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elasticsearch/_async/client/__init__.py +174 -79
- elasticsearch/_async/client/_base.py +0 -1
- elasticsearch/_async/client/async_search.py +12 -8
- elasticsearch/_async/client/autoscaling.py +4 -4
- elasticsearch/_async/client/cat.py +26 -26
- elasticsearch/_async/client/ccr.py +186 -72
- elasticsearch/_async/client/cluster.py +38 -19
- elasticsearch/_async/client/connector.py +30 -30
- elasticsearch/_async/client/dangling_indices.py +3 -3
- elasticsearch/_async/client/enrich.py +26 -5
- elasticsearch/_async/client/eql.py +32 -4
- elasticsearch/_async/client/esql.py +62 -6
- elasticsearch/_async/client/features.py +12 -2
- elasticsearch/_async/client/fleet.py +8 -2
- elasticsearch/_async/client/graph.py +1 -1
- elasticsearch/_async/client/ilm.py +23 -22
- elasticsearch/_async/client/indices.py +424 -132
- elasticsearch/_async/client/inference.py +1906 -61
- elasticsearch/_async/client/ingest.py +32 -38
- elasticsearch/_async/client/license.py +51 -16
- elasticsearch/_async/client/logstash.py +3 -3
- elasticsearch/_async/client/migration.py +3 -3
- elasticsearch/_async/client/ml.py +144 -115
- elasticsearch/_async/client/monitoring.py +1 -1
- elasticsearch/_async/client/nodes.py +9 -27
- elasticsearch/_async/client/query_rules.py +8 -8
- elasticsearch/_async/client/rollup.py +8 -8
- elasticsearch/_async/client/search_application.py +13 -13
- elasticsearch/_async/client/searchable_snapshots.py +4 -4
- elasticsearch/_async/client/security.py +71 -71
- elasticsearch/_async/client/shutdown.py +3 -10
- elasticsearch/_async/client/simulate.py +6 -6
- elasticsearch/_async/client/slm.py +9 -9
- elasticsearch/_async/client/snapshot.py +13 -17
- elasticsearch/_async/client/sql.py +6 -6
- elasticsearch/_async/client/ssl.py +1 -1
- elasticsearch/_async/client/synonyms.py +7 -7
- elasticsearch/_async/client/tasks.py +3 -9
- elasticsearch/_async/client/text_structure.py +4 -4
- elasticsearch/_async/client/transform.py +30 -28
- elasticsearch/_async/client/watcher.py +22 -14
- elasticsearch/_async/client/xpack.py +2 -2
- elasticsearch/_async/helpers.py +0 -1
- elasticsearch/_sync/client/__init__.py +174 -79
- elasticsearch/_sync/client/_base.py +0 -1
- elasticsearch/_sync/client/async_search.py +12 -8
- elasticsearch/_sync/client/autoscaling.py +4 -4
- elasticsearch/_sync/client/cat.py +26 -26
- elasticsearch/_sync/client/ccr.py +186 -72
- elasticsearch/_sync/client/cluster.py +38 -19
- elasticsearch/_sync/client/connector.py +30 -30
- elasticsearch/_sync/client/dangling_indices.py +3 -3
- elasticsearch/_sync/client/enrich.py +26 -5
- elasticsearch/_sync/client/eql.py +32 -4
- elasticsearch/_sync/client/esql.py +62 -6
- elasticsearch/_sync/client/features.py +12 -2
- elasticsearch/_sync/client/fleet.py +8 -2
- elasticsearch/_sync/client/graph.py +1 -1
- elasticsearch/_sync/client/ilm.py +23 -22
- elasticsearch/_sync/client/indices.py +424 -132
- elasticsearch/_sync/client/inference.py +1906 -61
- elasticsearch/_sync/client/ingest.py +32 -38
- elasticsearch/_sync/client/license.py +51 -16
- elasticsearch/_sync/client/logstash.py +3 -3
- elasticsearch/_sync/client/migration.py +3 -3
- elasticsearch/_sync/client/ml.py +144 -115
- elasticsearch/_sync/client/monitoring.py +1 -1
- elasticsearch/_sync/client/nodes.py +9 -27
- elasticsearch/_sync/client/query_rules.py +8 -8
- elasticsearch/_sync/client/rollup.py +8 -8
- elasticsearch/_sync/client/search_application.py +13 -13
- elasticsearch/_sync/client/searchable_snapshots.py +4 -4
- elasticsearch/_sync/client/security.py +71 -71
- elasticsearch/_sync/client/shutdown.py +3 -10
- elasticsearch/_sync/client/simulate.py +6 -6
- elasticsearch/_sync/client/slm.py +9 -9
- elasticsearch/_sync/client/snapshot.py +13 -17
- elasticsearch/_sync/client/sql.py +6 -6
- elasticsearch/_sync/client/ssl.py +1 -1
- elasticsearch/_sync/client/synonyms.py +7 -7
- elasticsearch/_sync/client/tasks.py +3 -9
- elasticsearch/_sync/client/text_structure.py +4 -4
- elasticsearch/_sync/client/transform.py +30 -28
- elasticsearch/_sync/client/utils.py +0 -3
- elasticsearch/_sync/client/watcher.py +22 -14
- elasticsearch/_sync/client/xpack.py +2 -2
- elasticsearch/_version.py +1 -1
- 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 +233 -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 +226 -0
- elasticsearch/dsl/_sync/update_by_query.py +45 -0
- elasticsearch/dsl/aggs.py +3730 -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 +1040 -0
- elasticsearch/dsl/serializer.py +34 -0
- elasticsearch/dsl/types.py +6509 -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 +119 -0
- {elasticsearch-8.17.2.dist-info → elasticsearch-8.18.1.dist-info}/METADATA +14 -2
- elasticsearch-8.18.1.dist-info/RECORD +163 -0
- elasticsearch-8.18.1.dist-info/licenses/LICENSE.txt +175 -0
- elasticsearch-8.18.1.dist-info/licenses/NOTICE.txt +559 -0
- elasticsearch-8.17.2.dist-info/RECORD +0 -119
- {elasticsearch-8.17.2.dist-info → elasticsearch-8.18.1.dist-info}/WHEEL +0 -0
- {elasticsearch-8.17.2.dist-info → elasticsearch-8.18.1.dist-info}/licenses/LICENSE +0 -0
- {elasticsearch-8.17.2.dist-info → elasticsearch-8.18.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
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
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from typing import (
|
|
20
|
+
TYPE_CHECKING,
|
|
21
|
+
Any,
|
|
22
|
+
Dict,
|
|
23
|
+
Generic,
|
|
24
|
+
List,
|
|
25
|
+
Optional,
|
|
26
|
+
Sequence,
|
|
27
|
+
Tuple,
|
|
28
|
+
Type,
|
|
29
|
+
Union,
|
|
30
|
+
cast,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from typing_extensions import Self
|
|
34
|
+
|
|
35
|
+
from .aggs import A, Agg
|
|
36
|
+
from .query import MatchAll, Nested, Query, Range, Terms
|
|
37
|
+
from .response import Response
|
|
38
|
+
from .utils import _R, AttrDict
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from .document_base import DocumentBase
|
|
42
|
+
from .response.aggs import BucketData
|
|
43
|
+
from .search_base import SearchBase
|
|
44
|
+
|
|
45
|
+
FilterValueType = Union[str, datetime, Sequence[str]]
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"FacetedSearchBase",
|
|
49
|
+
"HistogramFacet",
|
|
50
|
+
"TermsFacet",
|
|
51
|
+
"DateHistogramFacet",
|
|
52
|
+
"RangeFacet",
|
|
53
|
+
"NestedFacet",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Facet(Generic[_R]):
|
|
58
|
+
"""
|
|
59
|
+
A facet on faceted search. Wraps and aggregation and provides functionality
|
|
60
|
+
to create a filter for selected values and return a list of facet values
|
|
61
|
+
from the result of the aggregation.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
agg_type: str = ""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self, metric: Optional[Agg[_R]] = None, metric_sort: str = "desc", **kwargs: Any
|
|
68
|
+
):
|
|
69
|
+
self.filter_values = ()
|
|
70
|
+
self._params = kwargs
|
|
71
|
+
self._metric = metric
|
|
72
|
+
if metric and metric_sort:
|
|
73
|
+
self._params["order"] = {"metric": metric_sort}
|
|
74
|
+
|
|
75
|
+
def get_aggregation(self) -> Agg[_R]:
|
|
76
|
+
"""
|
|
77
|
+
Return the aggregation object.
|
|
78
|
+
"""
|
|
79
|
+
agg: Agg[_R] = A(self.agg_type, **self._params)
|
|
80
|
+
if self._metric:
|
|
81
|
+
agg.metric("metric", self._metric)
|
|
82
|
+
return agg
|
|
83
|
+
|
|
84
|
+
def add_filter(self, filter_values: List[FilterValueType]) -> Optional[Query]:
|
|
85
|
+
"""
|
|
86
|
+
Construct a filter.
|
|
87
|
+
"""
|
|
88
|
+
if not filter_values:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
f = self.get_value_filter(filter_values[0])
|
|
92
|
+
for v in filter_values[1:]:
|
|
93
|
+
f |= self.get_value_filter(v)
|
|
94
|
+
return f
|
|
95
|
+
|
|
96
|
+
def get_value_filter(self, filter_value: FilterValueType) -> Query: # type: ignore[empty-body]
|
|
97
|
+
"""
|
|
98
|
+
Construct a filter for an individual value
|
|
99
|
+
"""
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def is_filtered(self, key: str, filter_values: List[FilterValueType]) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Is a filter active on the given key.
|
|
105
|
+
"""
|
|
106
|
+
return key in filter_values
|
|
107
|
+
|
|
108
|
+
def get_value(self, bucket: "BucketData[_R]") -> Any:
|
|
109
|
+
"""
|
|
110
|
+
return a value representing a bucket. Its key as default.
|
|
111
|
+
"""
|
|
112
|
+
return bucket["key"]
|
|
113
|
+
|
|
114
|
+
def get_metric(self, bucket: "BucketData[_R]") -> int:
|
|
115
|
+
"""
|
|
116
|
+
Return a metric, by default doc_count for a bucket.
|
|
117
|
+
"""
|
|
118
|
+
if self._metric:
|
|
119
|
+
return cast(int, bucket["metric"]["value"])
|
|
120
|
+
return cast(int, bucket["doc_count"])
|
|
121
|
+
|
|
122
|
+
def get_values(
|
|
123
|
+
self, data: "BucketData[_R]", filter_values: List[FilterValueType]
|
|
124
|
+
) -> List[Tuple[Any, int, bool]]:
|
|
125
|
+
"""
|
|
126
|
+
Turn the raw bucket data into a list of tuples containing the key,
|
|
127
|
+
number of documents and a flag indicating whether this value has been
|
|
128
|
+
selected or not.
|
|
129
|
+
"""
|
|
130
|
+
out = []
|
|
131
|
+
for bucket in data.buckets:
|
|
132
|
+
b = cast("BucketData[_R]", bucket)
|
|
133
|
+
key = self.get_value(b)
|
|
134
|
+
out.append((key, self.get_metric(b), self.is_filtered(key, filter_values)))
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TermsFacet(Facet[_R]):
|
|
139
|
+
agg_type = "terms"
|
|
140
|
+
|
|
141
|
+
def add_filter(self, filter_values: List[FilterValueType]) -> Optional[Query]:
|
|
142
|
+
"""Create a terms filter instead of bool containing term filters."""
|
|
143
|
+
if filter_values:
|
|
144
|
+
return Terms(self._params["field"], filter_values, _expand__to_dot=False)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RangeFacet(Facet[_R]):
|
|
149
|
+
agg_type = "range"
|
|
150
|
+
|
|
151
|
+
def _range_to_dict(
|
|
152
|
+
self, range: Tuple[Any, Tuple[Optional[int], Optional[int]]]
|
|
153
|
+
) -> Dict[str, Any]:
|
|
154
|
+
key, _range = range
|
|
155
|
+
out: Dict[str, Any] = {"key": key}
|
|
156
|
+
if _range[0] is not None:
|
|
157
|
+
out["from"] = _range[0]
|
|
158
|
+
if _range[1] is not None:
|
|
159
|
+
out["to"] = _range[1]
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
ranges: Sequence[Tuple[Any, Tuple[Optional[int], Optional[int]]]],
|
|
165
|
+
**kwargs: Any,
|
|
166
|
+
):
|
|
167
|
+
super().__init__(**kwargs)
|
|
168
|
+
self._params["ranges"] = list(map(self._range_to_dict, ranges))
|
|
169
|
+
self._params["keyed"] = False
|
|
170
|
+
self._ranges = dict(ranges)
|
|
171
|
+
|
|
172
|
+
def get_value_filter(self, filter_value: FilterValueType) -> Query:
|
|
173
|
+
f, t = self._ranges[filter_value]
|
|
174
|
+
limits: Dict[str, Any] = {}
|
|
175
|
+
if f is not None:
|
|
176
|
+
limits["gte"] = f
|
|
177
|
+
if t is not None:
|
|
178
|
+
limits["lt"] = t
|
|
179
|
+
|
|
180
|
+
return Range(self._params["field"], limits, _expand__to_dot=False)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class HistogramFacet(Facet[_R]):
|
|
184
|
+
agg_type = "histogram"
|
|
185
|
+
|
|
186
|
+
def get_value_filter(self, filter_value: FilterValueType) -> Range:
|
|
187
|
+
return Range(
|
|
188
|
+
self._params["field"],
|
|
189
|
+
{
|
|
190
|
+
"gte": filter_value,
|
|
191
|
+
"lt": filter_value + self._params["interval"],
|
|
192
|
+
},
|
|
193
|
+
_expand__to_dot=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _date_interval_year(d: datetime) -> datetime:
|
|
198
|
+
return d.replace(
|
|
199
|
+
year=d.year + 1, day=(28 if d.month == 2 and d.day == 29 else d.day)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _date_interval_month(d: datetime) -> datetime:
|
|
204
|
+
return (d + timedelta(days=32)).replace(day=1)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _date_interval_week(d: datetime) -> datetime:
|
|
208
|
+
return d + timedelta(days=7)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _date_interval_day(d: datetime) -> datetime:
|
|
212
|
+
return d + timedelta(days=1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _date_interval_hour(d: datetime) -> datetime:
|
|
216
|
+
return d + timedelta(hours=1)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class DateHistogramFacet(Facet[_R]):
|
|
220
|
+
agg_type = "date_histogram"
|
|
221
|
+
|
|
222
|
+
DATE_INTERVALS = {
|
|
223
|
+
"year": _date_interval_year,
|
|
224
|
+
"1Y": _date_interval_year,
|
|
225
|
+
"month": _date_interval_month,
|
|
226
|
+
"1M": _date_interval_month,
|
|
227
|
+
"week": _date_interval_week,
|
|
228
|
+
"1w": _date_interval_week,
|
|
229
|
+
"day": _date_interval_day,
|
|
230
|
+
"1d": _date_interval_day,
|
|
231
|
+
"hour": _date_interval_hour,
|
|
232
|
+
"1h": _date_interval_hour,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
def __init__(self, **kwargs: Any):
|
|
236
|
+
kwargs.setdefault("min_doc_count", 0)
|
|
237
|
+
super().__init__(**kwargs)
|
|
238
|
+
|
|
239
|
+
def get_value(self, bucket: "BucketData[_R]") -> Any:
|
|
240
|
+
if not isinstance(bucket["key"], datetime):
|
|
241
|
+
# Elasticsearch returns key=None instead of 0 for date 1970-01-01,
|
|
242
|
+
# so we need to set key to 0 to avoid TypeError exception
|
|
243
|
+
if bucket["key"] is None:
|
|
244
|
+
bucket["key"] = 0
|
|
245
|
+
# Preserve milliseconds in the datetime
|
|
246
|
+
return datetime.utcfromtimestamp(int(cast(int, bucket["key"])) / 1000.0)
|
|
247
|
+
else:
|
|
248
|
+
return bucket["key"]
|
|
249
|
+
|
|
250
|
+
def get_value_filter(self, filter_value: Any) -> Range:
|
|
251
|
+
for interval_type in ("calendar_interval", "fixed_interval"):
|
|
252
|
+
if interval_type in self._params:
|
|
253
|
+
break
|
|
254
|
+
else:
|
|
255
|
+
interval_type = "interval"
|
|
256
|
+
|
|
257
|
+
return Range(
|
|
258
|
+
self._params["field"],
|
|
259
|
+
{
|
|
260
|
+
"gte": filter_value,
|
|
261
|
+
"lt": self.DATE_INTERVALS[self._params[interval_type]](filter_value),
|
|
262
|
+
},
|
|
263
|
+
_expand__to_dot=False,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class NestedFacet(Facet[_R]):
|
|
268
|
+
agg_type = "nested"
|
|
269
|
+
|
|
270
|
+
def __init__(self, path: str, nested_facet: Facet[_R]):
|
|
271
|
+
self._path = path
|
|
272
|
+
self._inner = nested_facet
|
|
273
|
+
super().__init__(path=path, aggs={"inner": nested_facet.get_aggregation()})
|
|
274
|
+
|
|
275
|
+
def get_values(
|
|
276
|
+
self, data: "BucketData[_R]", filter_values: List[FilterValueType]
|
|
277
|
+
) -> List[Tuple[Any, int, bool]]:
|
|
278
|
+
return self._inner.get_values(data.inner, filter_values)
|
|
279
|
+
|
|
280
|
+
def add_filter(self, filter_values: List[FilterValueType]) -> Optional[Query]:
|
|
281
|
+
inner_q = self._inner.add_filter(filter_values)
|
|
282
|
+
if inner_q:
|
|
283
|
+
return Nested(path=self._path, query=inner_q)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class FacetedResponse(Response[_R]):
|
|
288
|
+
if TYPE_CHECKING:
|
|
289
|
+
_faceted_search: "FacetedSearchBase[_R]"
|
|
290
|
+
_facets: Dict[str, List[Tuple[Any, int, bool]]]
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def query_string(self) -> Optional[Union[str, Query]]:
|
|
294
|
+
return self._faceted_search._query
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def facets(self) -> Dict[str, List[Tuple[Any, int, bool]]]:
|
|
298
|
+
if not hasattr(self, "_facets"):
|
|
299
|
+
super(AttrDict, self).__setattr__("_facets", AttrDict({}))
|
|
300
|
+
for name, facet in self._faceted_search.facets.items():
|
|
301
|
+
self._facets[name] = facet.get_values(
|
|
302
|
+
getattr(getattr(self.aggregations, "_filter_" + name), name),
|
|
303
|
+
self._faceted_search.filter_values.get(name, []),
|
|
304
|
+
)
|
|
305
|
+
return self._facets
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class FacetedSearchBase(Generic[_R]):
|
|
309
|
+
"""
|
|
310
|
+
Abstraction for creating faceted navigation searches that takes care of
|
|
311
|
+
composing the queries, aggregations and filters as needed as well as
|
|
312
|
+
presenting the results in an easy-to-consume fashion::
|
|
313
|
+
|
|
314
|
+
class BlogSearch(FacetedSearch):
|
|
315
|
+
index = 'blogs'
|
|
316
|
+
doc_types = [Blog, Post]
|
|
317
|
+
fields = ['title^5', 'category', 'description', 'body']
|
|
318
|
+
|
|
319
|
+
facets = {
|
|
320
|
+
'type': TermsFacet(field='_type'),
|
|
321
|
+
'category': TermsFacet(field='category'),
|
|
322
|
+
'weekly_posts': DateHistogramFacet(field='published_from', interval='week')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
def search(self):
|
|
326
|
+
' Override search to add your own filters '
|
|
327
|
+
s = super(BlogSearch, self).search()
|
|
328
|
+
return s.filter('term', published=True)
|
|
329
|
+
|
|
330
|
+
# when using:
|
|
331
|
+
blog_search = BlogSearch("web framework", filters={"category": "python"})
|
|
332
|
+
|
|
333
|
+
# supports pagination
|
|
334
|
+
blog_search[10:20]
|
|
335
|
+
|
|
336
|
+
response = blog_search.execute()
|
|
337
|
+
|
|
338
|
+
# easy access to aggregation results:
|
|
339
|
+
for category, hit_count, is_selected in response.facets.category:
|
|
340
|
+
print(
|
|
341
|
+
"Category %s has %d hits%s." % (
|
|
342
|
+
category,
|
|
343
|
+
hit_count,
|
|
344
|
+
' and is chosen' if is_selected else ''
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
index: Optional[str] = None
|
|
351
|
+
doc_types: Optional[List[Union[str, Type["DocumentBase"]]]] = None
|
|
352
|
+
fields: Sequence[str] = []
|
|
353
|
+
facets: Dict[str, Facet[_R]] = {}
|
|
354
|
+
using = "default"
|
|
355
|
+
|
|
356
|
+
if TYPE_CHECKING:
|
|
357
|
+
|
|
358
|
+
def search(self) -> "SearchBase[_R]": ...
|
|
359
|
+
|
|
360
|
+
def __init__(
|
|
361
|
+
self,
|
|
362
|
+
query: Optional[Union[str, Query]] = None,
|
|
363
|
+
filters: Dict[str, FilterValueType] = {},
|
|
364
|
+
sort: Sequence[str] = [],
|
|
365
|
+
):
|
|
366
|
+
"""
|
|
367
|
+
:arg query: the text to search for
|
|
368
|
+
:arg filters: facet values to filter
|
|
369
|
+
:arg sort: sort information to be passed to :class:`~elasticsearch.dsl.Search`
|
|
370
|
+
"""
|
|
371
|
+
self._query = query
|
|
372
|
+
self._filters: Dict[str, Query] = {}
|
|
373
|
+
self._sort = sort
|
|
374
|
+
self.filter_values: Dict[str, List[FilterValueType]] = {}
|
|
375
|
+
for name, value in filters.items():
|
|
376
|
+
self.add_filter(name, value)
|
|
377
|
+
|
|
378
|
+
self._s = self.build_search()
|
|
379
|
+
|
|
380
|
+
def __getitem__(self, k: Union[int, slice]) -> Self:
|
|
381
|
+
self._s = self._s[k]
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
def add_filter(
|
|
385
|
+
self, name: str, filter_values: Union[FilterValueType, List[FilterValueType]]
|
|
386
|
+
) -> None:
|
|
387
|
+
"""
|
|
388
|
+
Add a filter for a facet.
|
|
389
|
+
"""
|
|
390
|
+
# normalize the value into a list
|
|
391
|
+
if not isinstance(filter_values, (tuple, list)):
|
|
392
|
+
if filter_values is None:
|
|
393
|
+
return
|
|
394
|
+
filter_values = [
|
|
395
|
+
filter_values,
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
# remember the filter values for use in FacetedResponse
|
|
399
|
+
self.filter_values[name] = filter_values # type: ignore[assignment]
|
|
400
|
+
|
|
401
|
+
# get the filter from the facet
|
|
402
|
+
f = self.facets[name].add_filter(filter_values) # type: ignore[arg-type]
|
|
403
|
+
if f is None:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
self._filters[name] = f
|
|
407
|
+
|
|
408
|
+
def query(
|
|
409
|
+
self, search: "SearchBase[_R]", query: Union[str, Query]
|
|
410
|
+
) -> "SearchBase[_R]":
|
|
411
|
+
"""
|
|
412
|
+
Add query part to ``search``.
|
|
413
|
+
|
|
414
|
+
Override this if you wish to customize the query used.
|
|
415
|
+
"""
|
|
416
|
+
if query:
|
|
417
|
+
if self.fields:
|
|
418
|
+
return search.query("multi_match", fields=self.fields, query=query)
|
|
419
|
+
else:
|
|
420
|
+
return search.query("multi_match", query=query)
|
|
421
|
+
return search
|
|
422
|
+
|
|
423
|
+
def aggregate(self, search: "SearchBase[_R]") -> None:
|
|
424
|
+
"""
|
|
425
|
+
Add aggregations representing the facets selected, including potential
|
|
426
|
+
filters.
|
|
427
|
+
"""
|
|
428
|
+
for f, facet in self.facets.items():
|
|
429
|
+
agg = facet.get_aggregation()
|
|
430
|
+
agg_filter: Query = MatchAll()
|
|
431
|
+
for field, filter in self._filters.items():
|
|
432
|
+
if f == field:
|
|
433
|
+
continue
|
|
434
|
+
agg_filter &= filter
|
|
435
|
+
search.aggs.bucket("_filter_" + f, "filter", filter=agg_filter).bucket(
|
|
436
|
+
f, agg
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def filter(self, search: "SearchBase[_R]") -> "SearchBase[_R]":
|
|
440
|
+
"""
|
|
441
|
+
Add a ``post_filter`` to the search request narrowing the results based
|
|
442
|
+
on the facet filters.
|
|
443
|
+
"""
|
|
444
|
+
if not self._filters:
|
|
445
|
+
return search
|
|
446
|
+
|
|
447
|
+
post_filter: Query = MatchAll()
|
|
448
|
+
for f in self._filters.values():
|
|
449
|
+
post_filter &= f
|
|
450
|
+
return search.post_filter(post_filter)
|
|
451
|
+
|
|
452
|
+
def highlight(self, search: "SearchBase[_R]") -> "SearchBase[_R]":
|
|
453
|
+
"""
|
|
454
|
+
Add highlighting for all the fields
|
|
455
|
+
"""
|
|
456
|
+
return search.highlight(
|
|
457
|
+
*(f if "^" not in f else f.split("^", 1)[0] for f in self.fields)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def sort(self, search: "SearchBase[_R]") -> "SearchBase[_R]":
|
|
461
|
+
"""
|
|
462
|
+
Add sorting information to the request.
|
|
463
|
+
"""
|
|
464
|
+
if self._sort:
|
|
465
|
+
search = search.sort(*self._sort)
|
|
466
|
+
return search
|
|
467
|
+
|
|
468
|
+
def params(self, **kwargs: Any) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Specify query params to be used when executing the search. All the
|
|
471
|
+
keyword arguments will override the current values. See
|
|
472
|
+
https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.Elasticsearch.search
|
|
473
|
+
for all available parameters.
|
|
474
|
+
"""
|
|
475
|
+
self._s = self._s.params(**kwargs)
|
|
476
|
+
|
|
477
|
+
def build_search(self) -> "SearchBase[_R]":
|
|
478
|
+
"""
|
|
479
|
+
Construct the ``Search`` object.
|
|
480
|
+
"""
|
|
481
|
+
s = self.search()
|
|
482
|
+
if self._query is not None:
|
|
483
|
+
s = self.query(s, self._query)
|
|
484
|
+
s = self.filter(s)
|
|
485
|
+
if self.fields:
|
|
486
|
+
s = self.highlight(s)
|
|
487
|
+
s = self.sort(s)
|
|
488
|
+
self.aggregate(s)
|
|
489
|
+
return s
|