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.
@@ -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
@@ -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
+ [![Coverage Status](https://coveralls.io/repos/github/django-commons/drf-haystack/badge.svg?branch=main)](https://coveralls.io/github/django-commons/drf-haystack?branch=main)
41
+ [![PyPI version](https://badge.fury.io/py/drf-haystack.svg)](https://badge.fury.io/py/drf-haystack)
42
+ [![Documentation Status](https://readthedocs.org/projects/drf-haystack/badge/?version=latest)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.13
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any