mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 17:52:19 +03:00
generating queries from filters to resolve the data first in ES.
This commit is contained in:
parent
5b4d8144ee
commit
837d74f941
25
graphene_django/elasticsearch/filter/bridges.py
Normal file
25
graphene_django/elasticsearch/filter/bridges.py
Normal 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()
|
17
graphene_django/elasticsearch/filter/fields.py
Normal file
17
graphene_django/elasticsearch/filter/fields.py
Normal 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)
|
|
@ -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}})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user