drf-haystack 1.9.3__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.
- drf_haystack/__init__.py +6 -0
- drf_haystack/constants.py +5 -0
- drf_haystack/fields.py +102 -0
- drf_haystack/filters.py +257 -0
- drf_haystack/generics.py +102 -0
- drf_haystack/mixins.py +116 -0
- drf_haystack/query.py +331 -0
- drf_haystack/serializers.py +489 -0
- drf_haystack/utils.py +26 -0
- drf_haystack/viewsets.py +11 -0
- drf_haystack-1.9.3.dist-info/METADATA +101 -0
- drf_haystack-1.9.3.dist-info/RECORD +13 -0
- drf_haystack-1.9.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from itertools import chain
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
except ImportError:
|
|
8
|
+
from django.utils.datastructures import SortedDict as OrderedDict
|
|
9
|
+
|
|
10
|
+
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
|
|
11
|
+
from haystack import fields as haystack_fields
|
|
12
|
+
from haystack.query import EmptySearchQuerySet
|
|
13
|
+
from haystack.utils.highlighting import Highlighter
|
|
14
|
+
from rest_framework import serializers
|
|
15
|
+
from rest_framework.fields import empty
|
|
16
|
+
from rest_framework.utils.field_mapping import ClassLookupDict, get_field_kwargs
|
|
17
|
+
|
|
18
|
+
from drf_haystack.fields import (
|
|
19
|
+
FacetDictField,
|
|
20
|
+
FacetListField,
|
|
21
|
+
HaystackBooleanField,
|
|
22
|
+
HaystackCharField,
|
|
23
|
+
HaystackDateField,
|
|
24
|
+
HaystackDateTimeField,
|
|
25
|
+
HaystackDecimalField,
|
|
26
|
+
HaystackFloatField,
|
|
27
|
+
HaystackIntegerField,
|
|
28
|
+
HaystackMultiValueField,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Meta(type):
|
|
33
|
+
"""
|
|
34
|
+
Template for the HaystackSerializerMeta.Meta class.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
fields = ()
|
|
38
|
+
exclude = ()
|
|
39
|
+
search_fields = ()
|
|
40
|
+
index_classes = ()
|
|
41
|
+
serializers = ()
|
|
42
|
+
ignore_fields = ()
|
|
43
|
+
field_aliases = {}
|
|
44
|
+
field_options = {}
|
|
45
|
+
index_aliases = {}
|
|
46
|
+
|
|
47
|
+
def __new__(mcs, name, bases, attrs):
|
|
48
|
+
cls = super().__new__(mcs, str(name), bases, attrs)
|
|
49
|
+
|
|
50
|
+
if cls.fields and cls.exclude:
|
|
51
|
+
raise ImproperlyConfigured(f"{name} cannot define both 'fields' and 'exclude'.")
|
|
52
|
+
|
|
53
|
+
return cls
|
|
54
|
+
|
|
55
|
+
def __setattr__(cls, key, value):
|
|
56
|
+
raise AttributeError("Meta class is immutable.")
|
|
57
|
+
|
|
58
|
+
def __delattr__(cls, key, value):
|
|
59
|
+
raise AttributeError("Meta class is immutable.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class HaystackSerializerMeta(serializers.SerializerMetaclass):
|
|
63
|
+
"""
|
|
64
|
+
Metaclass for the HaystackSerializer that ensures that all declared subclasses implemented a Meta.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __new__(mcs, name, bases, attrs):
|
|
68
|
+
attrs.setdefault("_abstract", False)
|
|
69
|
+
|
|
70
|
+
cls = super().__new__(mcs, str(name), bases, attrs)
|
|
71
|
+
|
|
72
|
+
if getattr(cls, "Meta", None):
|
|
73
|
+
cls.Meta = Meta("Meta", (Meta,), dict(cls.Meta.__dict__))
|
|
74
|
+
|
|
75
|
+
elif not cls._abstract:
|
|
76
|
+
raise ImproperlyConfigured(f"{name} must implement a Meta class or have the property _abstract")
|
|
77
|
+
|
|
78
|
+
return cls
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class HaystackSerializer(serializers.Serializer, metaclass=HaystackSerializerMeta):
|
|
82
|
+
"""
|
|
83
|
+
A `HaystackSerializer` which populates fields based on
|
|
84
|
+
which models that are available in the SearchQueryset.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_abstract = True
|
|
88
|
+
|
|
89
|
+
_field_mapping = ClassLookupDict({
|
|
90
|
+
haystack_fields.BooleanField: HaystackBooleanField,
|
|
91
|
+
haystack_fields.CharField: HaystackCharField,
|
|
92
|
+
haystack_fields.DateField: HaystackDateField,
|
|
93
|
+
haystack_fields.DateTimeField: HaystackDateTimeField,
|
|
94
|
+
haystack_fields.DecimalField: HaystackDecimalField,
|
|
95
|
+
haystack_fields.EdgeNgramField: HaystackCharField,
|
|
96
|
+
haystack_fields.FacetBooleanField: HaystackBooleanField,
|
|
97
|
+
haystack_fields.FacetCharField: HaystackCharField,
|
|
98
|
+
haystack_fields.FacetDateField: HaystackDateField,
|
|
99
|
+
haystack_fields.FacetDateTimeField: HaystackDateTimeField,
|
|
100
|
+
haystack_fields.FacetDecimalField: HaystackDecimalField,
|
|
101
|
+
haystack_fields.FacetFloatField: HaystackFloatField,
|
|
102
|
+
haystack_fields.FacetIntegerField: HaystackIntegerField,
|
|
103
|
+
haystack_fields.FacetMultiValueField: HaystackMultiValueField,
|
|
104
|
+
haystack_fields.FloatField: HaystackFloatField,
|
|
105
|
+
haystack_fields.IntegerField: HaystackIntegerField,
|
|
106
|
+
haystack_fields.LocationField: HaystackCharField,
|
|
107
|
+
haystack_fields.MultiValueField: HaystackMultiValueField,
|
|
108
|
+
haystack_fields.NgramField: HaystackCharField,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
def __init__(self, instance=None, data=empty, **kwargs):
|
|
112
|
+
super().__init__(instance, data, **kwargs)
|
|
113
|
+
|
|
114
|
+
if not self.Meta.index_classes and not self.Meta.serializers:
|
|
115
|
+
raise ImproperlyConfigured(
|
|
116
|
+
"You must set either the 'index_classes' or 'serializers' attribute on the serializer Meta class."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not self.instance:
|
|
120
|
+
self.instance = EmptySearchQuerySet()
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _get_default_field_kwargs(model, field):
|
|
124
|
+
"""
|
|
125
|
+
Get the required attributes from the model field in order
|
|
126
|
+
to instantiate a REST Framework serializer field.
|
|
127
|
+
"""
|
|
128
|
+
kwargs = {}
|
|
129
|
+
try:
|
|
130
|
+
field_name = field.model_attr or field.index_fieldname
|
|
131
|
+
model_field = model._meta.get_field(field_name)
|
|
132
|
+
kwargs.update(get_field_kwargs(field_name, model_field))
|
|
133
|
+
|
|
134
|
+
# Remove stuff we don't care about!
|
|
135
|
+
delete_attrs = [
|
|
136
|
+
"allow_blank",
|
|
137
|
+
"choices",
|
|
138
|
+
"model_field",
|
|
139
|
+
"allow_unicode",
|
|
140
|
+
]
|
|
141
|
+
for attr in delete_attrs:
|
|
142
|
+
kwargs.pop(attr, None)
|
|
143
|
+
except FieldDoesNotExist:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
return kwargs
|
|
147
|
+
|
|
148
|
+
def _get_index_field(self, field_name):
|
|
149
|
+
"""
|
|
150
|
+
Returns the correct index field.
|
|
151
|
+
"""
|
|
152
|
+
return field_name
|
|
153
|
+
|
|
154
|
+
def _get_index_class_name(self, index_cls):
|
|
155
|
+
"""
|
|
156
|
+
Converts in index model class to a name suitable for use as a field name prefix. A user
|
|
157
|
+
may optionally specify custom aliases via an 'index_aliases' attribute on the Meta class
|
|
158
|
+
"""
|
|
159
|
+
cls_name = index_cls.__name__
|
|
160
|
+
aliases = self.Meta.index_aliases
|
|
161
|
+
return aliases.get(cls_name, cls_name.split(".")[-1])
|
|
162
|
+
|
|
163
|
+
def get_fields(self):
|
|
164
|
+
"""
|
|
165
|
+
Get the required fields for serializing the result.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
fields = self.Meta.fields
|
|
169
|
+
exclude = self.Meta.exclude
|
|
170
|
+
ignore_fields = self.Meta.ignore_fields
|
|
171
|
+
indices = self.Meta.index_classes
|
|
172
|
+
|
|
173
|
+
declared_fields = copy.deepcopy(self._declared_fields)
|
|
174
|
+
prefix_field_names = len(indices) > 1
|
|
175
|
+
field_mapping = OrderedDict()
|
|
176
|
+
|
|
177
|
+
# overlapping fields on multiple indices is supported by internally prefixing the field
|
|
178
|
+
# names with the index class to which they belong or, optionally, a user-provided alias
|
|
179
|
+
# for the index.
|
|
180
|
+
for index_cls in self.Meta.index_classes:
|
|
181
|
+
prefix = ""
|
|
182
|
+
if prefix_field_names:
|
|
183
|
+
prefix = f"_{self._get_index_class_name(index_cls)}__"
|
|
184
|
+
for field_name, field_type in index_cls.fields.items():
|
|
185
|
+
orig_name = field_name
|
|
186
|
+
field_name = f"{prefix}{field_name}"
|
|
187
|
+
|
|
188
|
+
# Don't use this field if it is in `ignore_fields`
|
|
189
|
+
if orig_name in ignore_fields or field_name in ignore_fields:
|
|
190
|
+
continue
|
|
191
|
+
# When fields to include are decided by `exclude`
|
|
192
|
+
if exclude and (orig_name in exclude or field_name in exclude):
|
|
193
|
+
continue
|
|
194
|
+
# When fields to include are decided by `fields`
|
|
195
|
+
if fields and orig_name not in fields and field_name not in fields:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Look up the field attributes on the current index model,
|
|
199
|
+
# in order to correctly instantiate the serializer field.
|
|
200
|
+
model = index_cls().get_model()
|
|
201
|
+
kwargs = self._get_default_field_kwargs(model, field_type)
|
|
202
|
+
kwargs["prefix_field_names"] = prefix_field_names
|
|
203
|
+
field_mapping[field_name] = self._field_mapping[field_type](**kwargs)
|
|
204
|
+
|
|
205
|
+
# Add any explicitly declared fields. They *will* override any index fields
|
|
206
|
+
# in case of naming collision!.
|
|
207
|
+
if declared_fields:
|
|
208
|
+
for field_name in declared_fields:
|
|
209
|
+
field_mapping[field_name] = declared_fields[field_name]
|
|
210
|
+
return field_mapping
|
|
211
|
+
|
|
212
|
+
def to_representation(self, instance):
|
|
213
|
+
"""
|
|
214
|
+
If we have a serializer mapping, use that. Otherwise, use standard serializer behavior
|
|
215
|
+
Since we might be dealing with multiple indexes, some fields might
|
|
216
|
+
not be valid for all results. Do not render the fields which don't belong
|
|
217
|
+
to the search result.
|
|
218
|
+
"""
|
|
219
|
+
if self.Meta.serializers:
|
|
220
|
+
ret = self.multi_serializer_representation(instance)
|
|
221
|
+
else:
|
|
222
|
+
ret = super().to_representation(instance)
|
|
223
|
+
prefix_field_names = len(self.Meta.index_classes) > 1
|
|
224
|
+
current_index = self._get_index_class_name(type(instance.searchindex))
|
|
225
|
+
for field in self.fields:
|
|
226
|
+
orig_field = field
|
|
227
|
+
if prefix_field_names:
|
|
228
|
+
parts = field.split("__")
|
|
229
|
+
if len(parts) > 1:
|
|
230
|
+
index = parts[0][1:] # trim the preceding '_'
|
|
231
|
+
field = parts[1]
|
|
232
|
+
if index == current_index:
|
|
233
|
+
ret[field] = ret[orig_field]
|
|
234
|
+
del ret[orig_field]
|
|
235
|
+
elif field not in chain(instance.searchindex.fields.keys(), self._declared_fields.keys()):
|
|
236
|
+
del ret[orig_field]
|
|
237
|
+
|
|
238
|
+
# include the highlighted field in either case
|
|
239
|
+
if getattr(instance, "highlighted", None):
|
|
240
|
+
ret["highlighted"] = instance.highlighted[0]
|
|
241
|
+
return ret
|
|
242
|
+
|
|
243
|
+
def multi_serializer_representation(self, instance):
|
|
244
|
+
serializers = self.Meta.serializers
|
|
245
|
+
index = instance.searchindex
|
|
246
|
+
serializer_class = serializers.get(type(index), None)
|
|
247
|
+
if not serializer_class:
|
|
248
|
+
raise ImproperlyConfigured(f"Could not find serializer for {index} in mapping")
|
|
249
|
+
return serializer_class(context=self._context).to_representation(instance)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class FacetFieldSerializer(serializers.Serializer):
|
|
253
|
+
"""
|
|
254
|
+
Responsible for serializing a faceted result.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
text = serializers.SerializerMethodField()
|
|
258
|
+
count = serializers.SerializerMethodField()
|
|
259
|
+
narrow_url = serializers.SerializerMethodField()
|
|
260
|
+
|
|
261
|
+
def __init__(self, *args, **kwargs):
|
|
262
|
+
self._parent_field = None
|
|
263
|
+
super().__init__(*args, **kwargs)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def parent_field(self):
|
|
267
|
+
return self._parent_field
|
|
268
|
+
|
|
269
|
+
@parent_field.setter
|
|
270
|
+
def parent_field(self, value):
|
|
271
|
+
self._parent_field = value
|
|
272
|
+
|
|
273
|
+
def get_paginate_by_param(self):
|
|
274
|
+
"""
|
|
275
|
+
Returns the ``paginate_by_param`` for the (root) view paginator class.
|
|
276
|
+
This is needed in order to remove the query parameter from faceted
|
|
277
|
+
narrow urls.
|
|
278
|
+
|
|
279
|
+
If using a custom pagination class, this class attribute needs to
|
|
280
|
+
be set manually.
|
|
281
|
+
"""
|
|
282
|
+
if hasattr(self.root, "paginate_by_param") and self.root.paginate_by_param:
|
|
283
|
+
return self.root.paginate_by_param
|
|
284
|
+
|
|
285
|
+
pagination_class = self.context["view"].pagination_class
|
|
286
|
+
if not pagination_class:
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
# PageNumberPagination
|
|
290
|
+
if hasattr(pagination_class, "page_query_param"):
|
|
291
|
+
return pagination_class.page_query_param
|
|
292
|
+
|
|
293
|
+
# LimitOffsetPagination
|
|
294
|
+
elif hasattr(pagination_class, "offset_query_param"):
|
|
295
|
+
return pagination_class.offset_query_param
|
|
296
|
+
|
|
297
|
+
# CursorPagination
|
|
298
|
+
elif hasattr(pagination_class, "cursor_query_param"):
|
|
299
|
+
return pagination_class.cursor_query_param
|
|
300
|
+
|
|
301
|
+
else:
|
|
302
|
+
raise AttributeError(
|
|
303
|
+
f"{self.root.__class__.__name__} is missing a `paginate_by_param` attribute. "
|
|
304
|
+
f"Define a {self.root.__class__.__name__}.paginate_by_param or override "
|
|
305
|
+
f"{self.__class__.__name__}.get_paginate_by_param()."
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def get_text(self, instance):
|
|
309
|
+
"""
|
|
310
|
+
Haystack facets are returned as a two-tuple (value, count).
|
|
311
|
+
The text field should contain the faceted value.
|
|
312
|
+
"""
|
|
313
|
+
instance = instance[0]
|
|
314
|
+
if isinstance(instance, (str, (str,))):
|
|
315
|
+
return serializers.CharField(read_only=True).to_representation(instance)
|
|
316
|
+
elif isinstance(instance, datetime):
|
|
317
|
+
return serializers.DateTimeField(read_only=True).to_representation(instance)
|
|
318
|
+
return instance
|
|
319
|
+
|
|
320
|
+
def get_count(self, instance):
|
|
321
|
+
"""
|
|
322
|
+
Haystack facets are returned as a two-tuple (value, count).
|
|
323
|
+
The count field should contain the faceted count.
|
|
324
|
+
"""
|
|
325
|
+
instance = instance[1]
|
|
326
|
+
return serializers.IntegerField(read_only=True).to_representation(instance)
|
|
327
|
+
|
|
328
|
+
def get_narrow_url(self, instance):
|
|
329
|
+
"""
|
|
330
|
+
Return a link suitable for narrowing on the current item.
|
|
331
|
+
"""
|
|
332
|
+
text = instance[0]
|
|
333
|
+
request = self.context["request"]
|
|
334
|
+
query_params = request.GET.copy()
|
|
335
|
+
|
|
336
|
+
# Never keep the page query parameter in narrowing urls.
|
|
337
|
+
# It will raise a NotFound exception when trying to paginate a narrowed queryset.
|
|
338
|
+
page_query_param = self.get_paginate_by_param()
|
|
339
|
+
if page_query_param and page_query_param in query_params:
|
|
340
|
+
del query_params[page_query_param]
|
|
341
|
+
|
|
342
|
+
selected_facets = set(query_params.pop(self.root.facet_query_params_text, []))
|
|
343
|
+
selected_facets.add(f"{self.parent_field}_exact:{text}")
|
|
344
|
+
query_params.setlist(self.root.facet_query_params_text, sorted(selected_facets))
|
|
345
|
+
|
|
346
|
+
path = f"{request.path_info}?{query_params.urlencode()}"
|
|
347
|
+
url = request.build_absolute_uri(path)
|
|
348
|
+
return serializers.Hyperlink(url, "narrow-url")
|
|
349
|
+
|
|
350
|
+
def to_representation(self, field, instance):
|
|
351
|
+
"""
|
|
352
|
+
Set the ``parent_field`` property equal to the current field on the serializer class,
|
|
353
|
+
so that each field can query it to see what kind of attribute they are processing.
|
|
354
|
+
"""
|
|
355
|
+
self.parent_field = field
|
|
356
|
+
return super().to_representation(instance)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class HaystackFacetSerializer(serializers.Serializer, metaclass=HaystackSerializerMeta):
|
|
360
|
+
"""
|
|
361
|
+
The ``HaystackFacetSerializer`` is used to serialize the ``facet_counts()``
|
|
362
|
+
dictionary results on a ``SearchQuerySet`` instance.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
_abstract = True
|
|
366
|
+
serialize_objects = False
|
|
367
|
+
paginate_by_param = None
|
|
368
|
+
facet_dict_field_class = FacetDictField
|
|
369
|
+
facet_list_field_class = FacetListField
|
|
370
|
+
facet_field_serializer_class = FacetFieldSerializer
|
|
371
|
+
|
|
372
|
+
def get_fields(self):
|
|
373
|
+
"""
|
|
374
|
+
This returns a dictionary containing the top most fields,
|
|
375
|
+
``dates``, ``fields`` and ``queries``.
|
|
376
|
+
"""
|
|
377
|
+
field_mapping = OrderedDict()
|
|
378
|
+
for field, data in self.instance.items():
|
|
379
|
+
field_mapping.update({
|
|
380
|
+
field: self.facet_dict_field_class(
|
|
381
|
+
child=self.facet_list_field_class(child=self.facet_field_serializer_class(data)), required=False
|
|
382
|
+
)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
if self.serialize_objects is True:
|
|
386
|
+
field_mapping["objects"] = serializers.SerializerMethodField()
|
|
387
|
+
|
|
388
|
+
return field_mapping
|
|
389
|
+
|
|
390
|
+
def get_objects(self, instance):
|
|
391
|
+
"""
|
|
392
|
+
Return a list of objects matching the faceted result.
|
|
393
|
+
"""
|
|
394
|
+
view = self.context["view"]
|
|
395
|
+
queryset = self.context["objects"]
|
|
396
|
+
|
|
397
|
+
page = view.paginate_queryset(queryset)
|
|
398
|
+
if page is not None:
|
|
399
|
+
serializer = view.get_facet_objects_serializer(page, many=True)
|
|
400
|
+
return OrderedDict([
|
|
401
|
+
("count", self.get_count(queryset)),
|
|
402
|
+
("next", view.paginator.get_next_link()),
|
|
403
|
+
("previous", view.paginator.get_previous_link()),
|
|
404
|
+
("results", serializer.data),
|
|
405
|
+
])
|
|
406
|
+
|
|
407
|
+
serializer = view.get_serializer(queryset, many=True)
|
|
408
|
+
return serializer.data
|
|
409
|
+
|
|
410
|
+
def get_count(self, queryset):
|
|
411
|
+
"""
|
|
412
|
+
Determine an object count, supporting either querysets or regular lists.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
return queryset.count()
|
|
416
|
+
except (AttributeError, TypeError):
|
|
417
|
+
return len(queryset)
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def facet_query_params_text(self):
|
|
421
|
+
return self.context["facet_query_params_text"]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class HaystackSerializerMixin:
|
|
425
|
+
"""
|
|
426
|
+
This mixin can be added to a serializer to use the actual object as the data source for serialization rather
|
|
427
|
+
than the data stored in the search index fields. This makes it easy to return data from search results in
|
|
428
|
+
the same format as elsewhere in your API and reuse your existing serializers
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def to_representation(self, instance):
|
|
432
|
+
obj = instance.object
|
|
433
|
+
return super().to_representation(obj)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class HighlighterMixin:
|
|
437
|
+
"""
|
|
438
|
+
This mixin adds support for ``highlighting`` (the pure python, portable
|
|
439
|
+
version, not SearchQuerySet().highlight()). See Haystack docs
|
|
440
|
+
for more info).
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
highlighter_class = Highlighter
|
|
444
|
+
highlighter_css_class = "highlighted"
|
|
445
|
+
highlighter_html_tag = "span"
|
|
446
|
+
highlighter_max_length = 200
|
|
447
|
+
highlighter_field = None
|
|
448
|
+
|
|
449
|
+
def get_highlighter(self):
|
|
450
|
+
if not self.highlighter_class:
|
|
451
|
+
raise ImproperlyConfigured(
|
|
452
|
+
f"{self.__class__.__name__} is missing a highlighter_class. Define {self.__class__.__name__}.highlighter_class, "
|
|
453
|
+
f"or override {self.__class__.__name__}.get_highlighter()."
|
|
454
|
+
)
|
|
455
|
+
return self.highlighter_class
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def get_document_field(instance):
|
|
459
|
+
"""
|
|
460
|
+
Returns which field the search index has marked as it's
|
|
461
|
+
`document=True` field.
|
|
462
|
+
"""
|
|
463
|
+
for name, field in instance.searchindex.fields.items():
|
|
464
|
+
if field.document is True:
|
|
465
|
+
return name
|
|
466
|
+
|
|
467
|
+
def get_terms(self, data):
|
|
468
|
+
"""
|
|
469
|
+
Returns the terms to be highlighted
|
|
470
|
+
"""
|
|
471
|
+
terms = " ".join(self.context["request"].GET.values())
|
|
472
|
+
return terms
|
|
473
|
+
|
|
474
|
+
def to_representation(self, instance):
|
|
475
|
+
ret = super().to_representation(instance)
|
|
476
|
+
terms = self.get_terms(ret)
|
|
477
|
+
if terms:
|
|
478
|
+
highlighter = self.get_highlighter()(
|
|
479
|
+
terms,
|
|
480
|
+
html_tag=self.highlighter_html_tag,
|
|
481
|
+
css_class=self.highlighter_css_class,
|
|
482
|
+
max_length=self.highlighter_max_length,
|
|
483
|
+
)
|
|
484
|
+
document_field = self.get_document_field(instance)
|
|
485
|
+
if highlighter and document_field:
|
|
486
|
+
# Handle case where this data is None, but highlight expects it to be a string
|
|
487
|
+
data_to_highlight = getattr(instance, self.highlighter_field or document_field) or ""
|
|
488
|
+
ret["highlighted"] = highlighter.highlight(data_to_highlight)
|
|
489
|
+
return ret
|
drf_haystack/utils.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def merge_dict(a, b):
|
|
5
|
+
"""
|
|
6
|
+
Recursively merges and returns dict a with dict b.
|
|
7
|
+
Any list values will be combined and returned sorted.
|
|
8
|
+
|
|
9
|
+
:param a: dictionary object
|
|
10
|
+
:param b: dictionary object
|
|
11
|
+
:return: merged dictionary object
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
if not isinstance(b, dict):
|
|
15
|
+
return b
|
|
16
|
+
|
|
17
|
+
result = deepcopy(a)
|
|
18
|
+
for key, val in b.items():
|
|
19
|
+
if key in result and isinstance(result[key], dict):
|
|
20
|
+
result[key] = merge_dict(result[key], val)
|
|
21
|
+
elif key in result and isinstance(result[key], list):
|
|
22
|
+
result[key] = sorted(set(val) | set(result[key]))
|
|
23
|
+
else:
|
|
24
|
+
result[key] = deepcopy(val)
|
|
25
|
+
|
|
26
|
+
return result
|
drf_haystack/viewsets.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
|
|
2
|
+
from rest_framework.viewsets import ViewSetMixin
|
|
3
|
+
|
|
4
|
+
from drf_haystack.generics import HaystackGenericAPIView
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HaystackViewSet(RetrieveModelMixin, ListModelMixin, ViewSetMixin, HaystackGenericAPIView):
|
|
8
|
+
"""
|
|
9
|
+
The HaystackViewSet class provides the default ``list()`` and
|
|
10
|
+
``retrieve()`` actions with a haystack index as it's data source.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drf-haystack
|
|
3
|
+
Version: 1.9.3
|
|
4
|
+
Summary: Makes Haystack play nice with Django REST Framework
|
|
5
|
+
Author: Dhaval Gojiya, Rolf Håvard Blindheim, Ülgen Sarıkavak
|
|
6
|
+
Author-email: Dhaval Gojiya <dhavalgojiya10@gmail.com>, Rolf Håvard Blindheim <rhblind@gmail.com>, Ülgen Sarıkavak <foss@ulgenwanders.net>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 5.2
|
|
12
|
+
Classifier: Framework :: Django :: 6.0
|
|
13
|
+
Classifier: Framework :: Django :: 6.1
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Dist: django>=5.2,<6.2
|
|
24
|
+
Requires-Dist: django-haystack>=3.3.0,<4
|
|
25
|
+
Requires-Dist: djangorestframework>=3.16
|
|
26
|
+
Requires-Dist: python-dateutil
|
|
27
|
+
Requires-Python: >=3.11, <3.15
|
|
28
|
+
Project-URL: Documentation, https://drf-haystack.readthedocs.io
|
|
29
|
+
Project-URL: Homepage, https://github.com/django-commons/drf-haystack
|
|
30
|
+
Project-URL: Issues, https://github.com/django-commons/drf-haystack/issues
|
|
31
|
+
Project-URL: Repository, https://github.com/django-commons/drf-haystack.git
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
Haystack for Django REST Framework
|
|
35
|
+
==================================
|
|
36
|
+
|
|
37
|
+
Build status
|
|
38
|
+
------------
|
|
39
|
+
|
|
40
|
+
[](https://coveralls.io/github/django-commons/drf-haystack?branch=main)
|
|
41
|
+
[](https://badge.fury.io/py/drf-haystack)
|
|
42
|
+
[](http://drf-haystack.readthedocs.io/en/latest/?badge=latest)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
About
|
|
46
|
+
-----
|
|
47
|
+
|
|
48
|
+
Small library which tries to simplify integration of Haystack with Django REST Framework.
|
|
49
|
+
Fresh [documentation available](https://drf-haystack.readthedocs.io/en/latest/) on Read the docs!
|
|
50
|
+
|
|
51
|
+
Supported versions
|
|
52
|
+
------------------
|
|
53
|
+
|
|
54
|
+
- Python >=3.11, <3.15
|
|
55
|
+
- Django >=5.2, <6.2
|
|
56
|
+
- Haystack >=3.3.0, <4
|
|
57
|
+
- Django REST Framework >=3.16
|
|
58
|
+
- Elasticsearch >=7.0.0, <8
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Installation
|
|
62
|
+
------------
|
|
63
|
+
|
|
64
|
+
$ pip install drf-haystack
|
|
65
|
+
|
|
66
|
+
Supported features
|
|
67
|
+
------------------
|
|
68
|
+
We aim to support most features Haystack does (or at least those which can be used in a REST API).
|
|
69
|
+
Currently, we support:
|
|
70
|
+
|
|
71
|
+
- Autocomplete
|
|
72
|
+
- Boost (Experimental)
|
|
73
|
+
- Faceting
|
|
74
|
+
- Geo Spatial Search
|
|
75
|
+
- Highlighting
|
|
76
|
+
- More Like This
|
|
77
|
+
|
|
78
|
+
Show me more!
|
|
79
|
+
-------------
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from drf_haystack.serializers import HaystackSerializer
|
|
83
|
+
from drf_haystack.viewsets import HaystackViewSet
|
|
84
|
+
|
|
85
|
+
from myapp.search_indexes import PersonIndex # You would define this Index normally as per Haystack's documentation
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Serializer
|
|
89
|
+
class PersonSearchSerializer(HaystackSerializer):
|
|
90
|
+
class Meta:
|
|
91
|
+
index_classes = [PersonIndex]
|
|
92
|
+
fields = ["firstname", "lastname", "full_name"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ViewSet
|
|
96
|
+
class PersonSearchViewSet(HaystackViewSet):
|
|
97
|
+
index_models = [Person]
|
|
98
|
+
serializer_class = PersonSerializer
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That's it, you're good to go. Hook it up to a DRF router and happy searching!
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
drf_haystack/__init__.py,sha256=qwGKu2Z_M430O5rKtq-exGqqGTmG6bcj9sv464m2MXo,138
|
|
2
|
+
drf_haystack/constants.py,sha256=i-p9pMMH4fYCQhPYHQOv1sI2CEeBL95nl-9XDF5dbWw,268
|
|
3
|
+
drf_haystack/fields.py,sha256=KPLJUSeRXw_0D6gTjfC_foZ5c5Y3RZR6WtOX6mivJsg,3202
|
|
4
|
+
drf_haystack/filters.py,sha256=KqvPgWtCea2zUKbD0XwuX8dMY-rllq8da5tVdIJ2Q-A,9896
|
|
5
|
+
drf_haystack/generics.py,sha256=e1NyU4UudngLNUuPvdewkq2I3e7gv1X5n4NenuKQ670,4032
|
|
6
|
+
drf_haystack/mixins.py,sha256=I21eoCcEKM-9rVm4G1SAXciRbmi_KML-w3tU031aje4,4365
|
|
7
|
+
drf_haystack/query.py,sha256=B0v2RKrMome2cDnEFECp9St2ntrQusItuD3C3tn60YY,12902
|
|
8
|
+
drf_haystack/serializers.py,sha256=alanXaASJRVP50VutMTOQQaugebq4aVEwclsL_TLTt0,18295
|
|
9
|
+
drf_haystack/utils.py,sha256=THg6OLDIXCZEYWWZpU_UATmnBIC0pO4YKSBAdAiie5U,701
|
|
10
|
+
drf_haystack/viewsets.py,sha256=SumBygWENSLa85WlhkoOkl2x70uF6bMx20h1DSGmoDs,427
|
|
11
|
+
drf_haystack-1.9.3.dist-info/WHEEL,sha256=-i9oRNYVXXZJUIYl5zclLIg6onEb0NLibTX34uln84w,81
|
|
12
|
+
drf_haystack-1.9.3.dist-info/METADATA,sha256=CKh5O2LDJqKvo47K2vUT2OsPul6rJRh582zGCvqV9Ps,3438
|
|
13
|
+
drf_haystack-1.9.3.dist-info/RECORD,,
|