devind-team / graphene-django-filter

Advanced filters for Graphene
MIT License
19 stars 7 forks source link

Cross search #5

Open Luferov opened 2 years ago

Luferov commented 2 years ago

Sometimes, it is required to search for one variable in several fields. For example:

import graphene
from graphene_django_filter import AdvancedFilterSet

class UserFilter(AdvancedFilterSet):
    class Meta:
        model = User
        fields= {
            'last_name': ('icontains',),
            'first_name': ('icontains',),
            'sir_name': ('icontains',),
        }

Graphql query might look like this:

query SearchUser($search: String!) {
  users(
    filter: {
      cross: {
        fields: ['last_name', 'first_name', 'sir_name'],
        delimiter: ' ' # space, for example
      }
    }
  )  {
    edges {
      node {
        id
        last_name
        first_name
        sir_name
      }
    }
  }
}

Graphql query should generate sql query via django orm like this:

from django.db.models import Q

delimiter = ' '
search = 'name'
filter = Q(last_name__icontains=search) | Q(first_name__icontains=search) | Q(sir_name__icontains=search)

search = 'name1 name2'
s1, s2 = search.split(delimiter)
filter = Q(
    Q(last_name__icontains=s1) | Q(first_name__icontains=s2),
    Q(last_name__icontains=s2) | Q(first_name__icontains=s1),
    Q(last_name__icontains=s1) | Q(sir_name__icontains=s1),
    Q(last_name__icontains=s2) | Q(sir_name__icontains=s2),
    # ...
)
SquakR commented 2 years ago

I think there is no way to implement a simple yet flexible API for this feature. The proposed API will work fine only for simple cases. But, for example this approach does not cover search by initials - 'Иванов И. И.' instead of 'Иванов Иван Иванович'. Possible solutions:

  1. Implement the proposed API and resolve more complex cases manually
  2. Client side template
    query SearchUser($search: String!) {
    users(
    filter: {
      cross: [
        {
          template: '{last_name} {first_name[0].} {sir_name[0].}'
          icontains: $search
        }
      ]
    }
    )  {
    edges {
      node {
        id
        last_name
        first_name
        sir_name
      }
    }
    }
    }
  3. Server side temlate
    
    from graphene_django_filter import AdavancedDjangoObjectType

class UserType(AdavancedDjangoObjectType): class Meta: model = User fields = 'all' cross_search = { 'full_name: { 'template': '{last_name} {first_name[0].} {sir_name[0].}' 'lookups: ('exact', 'icontains',) } }

```graphql
query SearchUser($search: String!) {
  users(
    filter: {
      full_name: { icontains: $search }
    }
  )  {
    edges {
      node {
        id
        last_name
        first_name
        sir_name
      }
    }
  }
}
  1. Use manual approach with annotation
    
    import django_filters
    from graphene_django_filter import AdavancedFilterSet

class UserFilterSet(AdvancedFilterSet): full_name = django_filters.CharFilter(field_name='full_name', lookup_expr='exact') full_name__icontains = django_filters.CharFilter(field_name='full_name', lookup_expr='icontains')

def filter_queryset(self, queryset): return super().filter_queryset( queryset.annotate(full_name=...) )


I think the third approach looks fine and will cover most cases. In the most complex cases, you can always use the fourth one.