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