Django Rest Framework (DRF) Search Filter¶
Stretch provides a DRF FilterBackend so that you can use Elasticsearch for your API endpoints. After you have a StretchIndex
set up for your related model, you can add it to a DRF Viewset.
from stretch.drf import StretchSearchFilter
class FooViewSet(ModelViewSet):
queryset = Foo.objects.all()
serializer_class = FooSerializer
# Use dot syntax to point to your StretchIndex class
stretch_index = 'example.stretch_indices.FooIndex'
filter_backends = (
...
# Include the search filter in your ``filter_backends``
StretchSearchFilter,
...
)
Making a Request¶
When you call your API endpoint, use your DRF SEARCH_PARAM to pass your query. By default the value is search
. If you want to use a different query parameter, subclass StretchSearchFilter
and set search_param = [your custom value]
.
curl http://localhost:8000/api/foo?search=[your query]
How it Works¶
DRF Filters implement a single method filter_queryset
just like a normal DRF filter. Stretch takes the incoming queryset and uses it to filter an Elasticsearch query using object IDs. The filter has a
Custom Searches¶
The DRF filter has a default search that includes fuzzy matching and phrase matching. You can set the default_search_fields on the index to specify which fields to use. By default is searches all top level fields on the index.
class FooIndex(StretchIndex):
...
class Meta:
...
default_search_fields = [
'field_1',
'field_3',
'field_4.autocomplete', # You can use Elasticsearch subfields that use different analyzers!
]
There are two ways to customize the search query. The simplest way is to add a stretch_modify_search
method on your DRF view. The filter will automatically pass the Elasticsearch DSL object to that method before executing the search. You can modify the search object or create a new one and return it.
class FooViewSet(ModelViewSet):
...
stretch_index = 'example.stretch_indices.FooIndex'
filter_backends = (
...
StretchSearchFilter,
...
)
def stretch_modify_search(self, s, view, index, request, queryset):
s = s.filter('terms', tags=['red', 'fish', 'blue'])
return s
For more control you can subclass StretchSearchFilter
. The only real rule here is to make sure the filter_queryset
method returns a queryset.
from stretch.drf import StretchSearchFilter
class CustomSearchFilter(StretchSearchFilter):
def filter_queryset(self, request, queryset, view):
"""
Return a deduplicated list of Foo objects by name
"""
index = self._get_index(view)
s = self.build_search(view, index, request, queryset)
search_results = s.execute()
# Get unique list of Foo object names
ordered_names = []
for bucket in search_results.aggregations.names.buckets:
ordered_names.append(bucket.key)
# Filter and order queryset by names
queryset = queryset.filter(name__in=ordered_names)
distinct_name_pks = queryset.order_by('name').distinct('name').values_list('pk', flat=True)
preserved = Case(
*[When(name=name, then=pos) for pos, name in enumerate(ordered_names)]
)
queryset = Foo.objects.filter(pk__in=distinct_name_pks).order_by(preserved)
return queryset
def build_search(self, view, index, request, queryset):
"""
Use aggregations to get to matching unique Foo names
"""
s = super().build_search(view, index, request, queryset)
# Retrieve a list of Foo object names
s.aggs.bucket(
'names',
'terms',
field='name',
order={'name_score': 'desc'}
).bucket(
'name_score',
'max',
script='_score'
)
return s