From 837d74f941b2fc614f9dd12ed7152f8c004098a9 Mon Sep 17 00:00:00 2001 From: Alejandro Nunez Capote Date: Sun, 2 Jun 2019 20:13:14 -0400 Subject: [PATCH] generating queries from filters to resolve the data first in ES. --- .../elasticsearch/filter/bridges.py | 25 +++++++++++ .../elasticsearch/filter/fields.py | 17 +++++++ .../elasticsearch/filter/filters.py | 26 ++++++++++- .../elasticsearch/filter/filterset.py | 45 ++++++++++++++++++- graphene_django/filter/utils.py | 6 +-- graphene_django/utils/utils.py | 2 +- setup.py | 2 +- 7 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 graphene_django/elasticsearch/filter/bridges.py create mode 100644 graphene_django/elasticsearch/filter/fields.py diff --git a/graphene_django/elasticsearch/filter/bridges.py b/graphene_django/elasticsearch/filter/bridges.py new file mode 100644 index 0000000..0de147c --- /dev/null +++ b/graphene_django/elasticsearch/filter/bridges.py @@ -0,0 +1,25 @@ + +class QuerysetBridge(object): + """Bridge to Queryset through ES query""" + + def __init__(self, search): + """Taking as search, the ES search resolved by DjangoESFilterConnectionField""" + self.search = search + + def get_queryset(self): + """Returning self as Queryset to be the bridge""" + return self + + def apply_query(self, method, *args, **kwargs): + """Helper method to apply mutation to ES Query""" + if hasattr(self.search, method): + self.search = getattr(self.search, method)(*args, **kwargs) + + def __len__(self): + """Bridget method to response the ES count as QS len""" + return self.search.count() + + def __getitem__(self, k): + """Applying slice to ES and generating a QS from that""" + _slice = self.search.__getitem__(k) + return _slice.to_queryset() diff --git a/graphene_django/elasticsearch/filter/fields.py b/graphene_django/elasticsearch/filter/fields.py new file mode 100644 index 0000000..bbacadb --- /dev/null +++ b/graphene_django/elasticsearch/filter/fields.py @@ -0,0 +1,17 @@ +from graphene_django.elasticsearch.filter.bridges import QuerysetBridge +from graphene_django.filter import DjangoFilterConnectionField +from elasticsearch_dsl.query import Query + + +class DjangoESFilterConnectionField(DjangoFilterConnectionField): + """A Field to replace DjangoFilterConnectionField manager by QuerysetBridge""" + + def get_manager(self): + """Retuning a QuerysetBridge to replace the direct use over the QS""" + return QuerysetBridge(search=self.filterset_class._meta.index.search()) + + def merge_querysets(cls, default_queryset, queryset): + """Merge ES queries""" + if isinstance(default_queryset, Query): + return default_queryset & queryset + return default_queryset.query(queryset) diff --git a/graphene_django/elasticsearch/filter/filters.py b/graphene_django/elasticsearch/filter/filters.py index 8c2faec..358616f 100644 --- a/graphene_django/elasticsearch/filter/filters.py +++ b/graphene_django/elasticsearch/filter/filters.py @@ -39,6 +39,30 @@ class StringFilterES(object): # pylint: disable=R0902 for variant in self.variants: variant_name = self.field_name if variant in ["default", self.default_expr] \ else "%s_%s" % (self.field_name, variant) - fields[variant_name] = self.argument + fields[variant_name] = self return fields + + def get_q(self, arguments): + """ + :param arguments: parameters of the query. + :return: Returns a elasticsearch_dsl.Q query object. + """ + queries = [] + + for argument, value in arguments.iteritems(): + if argument in self.fields: + + if argument == self.field_name: + suffix_expr = self.default_expr or 'default' + else: + argument_split = argument.split("_") + suffix_expr = argument_split[len(argument_split) - 1] + + if suffix_expr in self.variants: + query = self.variants.get(suffix_expr, None) + + if query: + queries.extend([query(self.field_name, value)]) + + return Q("bool", must=queries[0]) if len(queries) == 1 else Q("bool", must={"bool": {"should": queries}}) diff --git a/graphene_django/elasticsearch/filter/filterset.py b/graphene_django/elasticsearch/filter/filterset.py index 4bf15e9..556e2aa 100644 --- a/graphene_django/elasticsearch/filter/filterset.py +++ b/graphene_django/elasticsearch/filter/filterset.py @@ -1,11 +1,24 @@ """Fields""" from collections import OrderedDict + +from elasticsearch_dsl import Q from django.utils import six from django_filters.filterset import BaseFilterSet from .filters import StringFilterES +class FilterSetESOptions(object): + """Basic FilterSetES options to Metadata""" + def __init__(self, options=None): + """ + The field option is combined with the index to automatically generate + filters. + """ + self.index = getattr(options, 'index', None) + self.model = self.index._doc_type.model if self.index else None + + class FilterSetESMetaclass(type): """Captures the meta class of the filterSet class.""" @@ -23,6 +36,7 @@ class FilterSetESMetaclass(type): base_filters.update(filter_field.fields) new_class.base_filters = base_filters + new_class._meta = FilterSetESOptions(getattr(new_class, 'Meta', None)) return new_class @classmethod @@ -52,4 +66,33 @@ class FilterSetESMetaclass(type): class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)): """FilterSet specific for ElasticSearch.""" - pass + def __init__(self, data, queryset, request): + """ + Receiving params necessaries to resolved the data + :param data: argument passed to query + :param queryset: a ES queryset + :param request: the context of request + """ + self.data = data + self.es_query = queryset + self.request = request + + @property + def qs(self): + """Returning ES queryset as QS""" + query_base = self.generate_q() + self.es_query.apply_query("query", query_base) + self.es_query.apply_query("source", ["id"]) + return self.es_query + + def generate_q(self): + """ + Generate a query for each filter. + :return: Generates a super query with bool as root, and combines all sub-queries from each argument. + """ + query_base = Q("bool") + for name, filter_es in six.iteritems(self.declared_filters): + query_filter = filter_es.get_q(self.data) + if query_filter is not None: + query_base += query_filter + return query_base diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index 1487793..3c09619 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -1,5 +1,5 @@ import six -from graphene import Argument +from django_filters import Filter from .filterset import custom_filterset_factory, setup_filterset @@ -14,11 +14,11 @@ def get_filtering_args_from_filterset(filterset_class, type): args = {} for name, filter_field in six.iteritems(filterset_class.base_filters): - if not isinstance(filter_field, Argument): + if isinstance(filter_field, Filter): field_type = convert_form_field(filter_field.field).Argument() field_type.description = filter_field.label else: - field_type = filter_field + field_type = filter_field.argument args[name] = field_type diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index c15ff63..5195e25 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -13,7 +13,7 @@ except ImportError: try: - import elasticsearch_dsl # noqa + import django_elasticsearch_dsl # noqa DJANGO_ELASTICSEARCH_DSL_INSTALLED = True except ImportError: diff --git a/setup.py b/setup.py index 473b041..c5a2123 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ tests_require = [ "django-filter<2;python_version<'3'", "django-filter>=2;python_version>='3'", "pytest-django>=3.3.2", - "elasticsearch-dsl<7.0", + "django_elasticsearch_dsl", ] + rest_framework_require