generating queries from filters to resolve the data first in ES.

This commit is contained in:
Alejandro Nunez Capote 2019-06-02 20:13:14 -04:00
parent 5b4d8144ee
commit 837d74f941
7 changed files with 116 additions and 7 deletions

View File

@ -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()

View File

@ -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)

View File

@ -39,6 +39,30 @@ class StringFilterES(object): # pylint: disable=R0902
for variant in self.variants: for variant in self.variants:
variant_name = self.field_name if variant in ["default", self.default_expr] \ variant_name = self.field_name if variant in ["default", self.default_expr] \
else "%s_%s" % (self.field_name, variant) else "%s_%s" % (self.field_name, variant)
fields[variant_name] = self.argument fields[variant_name] = self
return fields 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}})

View File

@ -1,11 +1,24 @@
"""Fields""" """Fields"""
from collections import OrderedDict from collections import OrderedDict
from elasticsearch_dsl import Q
from django.utils import six from django.utils import six
from django_filters.filterset import BaseFilterSet from django_filters.filterset import BaseFilterSet
from .filters import StringFilterES 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): class FilterSetESMetaclass(type):
"""Captures the meta class of the filterSet class.""" """Captures the meta class of the filterSet class."""
@ -23,6 +36,7 @@ class FilterSetESMetaclass(type):
base_filters.update(filter_field.fields) base_filters.update(filter_field.fields)
new_class.base_filters = base_filters new_class.base_filters = base_filters
new_class._meta = FilterSetESOptions(getattr(new_class, 'Meta', None))
return new_class return new_class
@classmethod @classmethod
@ -52,4 +66,33 @@ class FilterSetESMetaclass(type):
class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)): class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)):
"""FilterSet specific for ElasticSearch.""" """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

View File

@ -1,5 +1,5 @@
import six import six
from graphene import Argument from django_filters import Filter
from .filterset import custom_filterset_factory, setup_filterset from .filterset import custom_filterset_factory, setup_filterset
@ -14,11 +14,11 @@ def get_filtering_args_from_filterset(filterset_class, type):
args = {} args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters): 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 = convert_form_field(filter_field.field).Argument()
field_type.description = filter_field.label field_type.description = filter_field.label
else: else:
field_type = filter_field field_type = filter_field.argument
args[name] = field_type args[name] = field_type

View File

@ -13,7 +13,7 @@ except ImportError:
try: try:
import elasticsearch_dsl # noqa import django_elasticsearch_dsl # noqa
DJANGO_ELASTICSEARCH_DSL_INSTALLED = True DJANGO_ELASTICSEARCH_DSL_INSTALLED = True
except ImportError: except ImportError:

View File

@ -22,7 +22,7 @@ tests_require = [
"django-filter<2;python_version<'3'", "django-filter<2;python_version<'3'",
"django-filter>=2;python_version>='3'", "django-filter>=2;python_version>='3'",
"pytest-django>=3.3.2", "pytest-django>=3.3.2",
"elasticsearch-dsl<7.0", "django_elasticsearch_dsl",
] + rest_framework_require ] + rest_framework_require