mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 17:52:19 +03:00
added processors for all type of es query
This commit is contained in:
parent
3698b4b370
commit
4e4387d674
|
@ -6,10 +6,6 @@ class QuerysetBridge(object):
|
||||||
"""Taking as search, the ES search resolved by DjangoESFilterConnectionField"""
|
"""Taking as search, the ES search resolved by DjangoESFilterConnectionField"""
|
||||||
self.search = search
|
self.search = search
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Returning self as Queryset to be the bridge"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def apply_query(self, method, *args, **kwargs):
|
def apply_query(self, method, *args, **kwargs):
|
||||||
"""Helper method to apply mutation to ES Query"""
|
"""Helper method to apply mutation to ES Query"""
|
||||||
if hasattr(self.search, method):
|
if hasattr(self.search, method):
|
||||||
|
@ -23,3 +19,15 @@ class QuerysetBridge(object):
|
||||||
"""Applying slice to ES and generating a QS from that"""
|
"""Applying slice to ES and generating a QS from that"""
|
||||||
_slice = self.search.__getitem__(k)
|
_slice = self.search.__getitem__(k)
|
||||||
return _slice.to_queryset()
|
return _slice.to_queryset()
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerBridge(object):
|
||||||
|
"""Bridge to Queryset through ES query"""
|
||||||
|
|
||||||
|
def __init__(self, search_manager):
|
||||||
|
"""Taking as search, the ES search resolved by DjangoESFilterConnectionField"""
|
||||||
|
self.search_manager = search_manager
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Returning self as Queryset to be the bridge"""
|
||||||
|
return QuerysetBridge(search=self.search_manager())
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from elasticsearch_dsl.query import Query
|
from elasticsearch_dsl.query import Query
|
||||||
|
|
||||||
from graphene_django.elasticsearch.filter.bridges import QuerysetBridge
|
from graphene_django.elasticsearch.filter.bridges import ManagerBridge
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,11 +19,14 @@ class DjangoESFilterConnectionField(DjangoFilterConnectionField):
|
||||||
filterset_class = kwargs.get('filterset_class', None)
|
filterset_class = kwargs.get('filterset_class', None)
|
||||||
if filterset_class is None:
|
if filterset_class is None:
|
||||||
raise ValueError('You should provide a FilterSetES as filterset_class argument.')
|
raise ValueError('You should provide a FilterSetES as filterset_class argument.')
|
||||||
|
|
||||||
super(DjangoESFilterConnectionField, self).__init__(object_type, *args, **kwargs)
|
super(DjangoESFilterConnectionField, self).__init__(object_type, *args, **kwargs)
|
||||||
|
|
||||||
|
self.manager = ManagerBridge(search_manager=self.filterset_class._meta.index.search)
|
||||||
|
|
||||||
def get_manager(self):
|
def get_manager(self):
|
||||||
"""Returning a QuerysetBridge to replace the direct use over the QS"""
|
"""Returning a ManagerBridge to replace the direct use over the Model manager"""
|
||||||
return QuerysetBridge(search=self.filterset_class._meta.index.search())
|
return self.manager
|
||||||
|
|
||||||
def merge_querysets(cls, default_queryset, queryset):
|
def merge_querysets(cls, default_queryset, queryset):
|
||||||
"""Merge ES queries"""
|
"""Merge ES queries"""
|
||||||
|
|
|
@ -1,58 +1,47 @@
|
||||||
"""Filters to ElasticSearch"""
|
"""Filters to ElasticSearch"""
|
||||||
from collections import OrderedDict
|
from graphene import String, Boolean, Int
|
||||||
|
from graphene_django.elasticsearch.filter.processors import PROCESSORS
|
||||||
import six
|
|
||||||
from elasticsearch_dsl import Q
|
|
||||||
from graphene import String
|
|
||||||
|
|
||||||
|
|
||||||
class StringFilterES(object): # pylint: disable=R0902
|
class FilterES(object):
|
||||||
"""String Fields specific to ElasticSearch."""
|
"""Fields specific to ElasticSearch."""
|
||||||
|
default_processor = 'term'
|
||||||
|
default_argument = String()
|
||||||
|
|
||||||
default_expr = 'contain'
|
def __init__(self, field_name, field_name_es=None, lookup_expressions=None,
|
||||||
variants = {
|
default_processor=None, argument=None):
|
||||||
"contain": lambda name, value: Q('match',
|
|
||||||
**{name: {
|
|
||||||
"query": value,
|
|
||||||
"fuzziness": "auto"
|
|
||||||
}}),
|
|
||||||
|
|
||||||
"term": lambda name, value: Q('term', **{name: value}),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, name=None, attr=None, lookup_expressions=None, default_expr=None):
|
|
||||||
"""
|
"""
|
||||||
:param name: Name of the field. This is the name that will be exported.
|
:param name: Name of the field. This is the name that will be exported.
|
||||||
:param attr: Path to the index attr that will be used as filter.
|
:param attr: Path to the index attr that will be used as filter.
|
||||||
"""
|
"""
|
||||||
assert name or attr, "At least the field name or the field attr should be passed"
|
self.field_name = field_name
|
||||||
self.field_name = name or attr.replace('.', '_')
|
|
||||||
self.default_expr = default_expr or self.default_expr
|
if isinstance(field_name_es, list):
|
||||||
|
self.field_name_es = field_name_es
|
||||||
|
else:
|
||||||
|
self.field_name_es = [field_name_es or field_name]
|
||||||
|
|
||||||
|
self.default_filter_processor = default_processor or self.default_processor
|
||||||
|
|
||||||
self.lookup_expressions = lookup_expressions
|
self.lookup_expressions = lookup_expressions
|
||||||
self.argument = String()
|
|
||||||
self.fields = self.generate_fields()
|
|
||||||
|
|
||||||
def generate_fields(self):
|
self.processor = None
|
||||||
"""
|
|
||||||
All FilterSet objects should specify its fields for the introspection.
|
|
||||||
|
|
||||||
:return: A mapping of field to Filter type of field with all the suffix
|
|
||||||
expressions combinations.
|
|
||||||
"""
|
|
||||||
fields = OrderedDict()
|
|
||||||
if self.lookup_expressions:
|
if self.lookup_expressions:
|
||||||
|
|
||||||
for variant in self.lookup_expressions:
|
for variant in self.lookup_expressions:
|
||||||
if variant in self.variants:
|
if variant in PROCESSORS:
|
||||||
variant_name = self.field_name if variant in ["default", self.default_expr] \
|
self.processor = self.build_processor(variant)
|
||||||
else "%s_%s" % (self.field_name, variant)
|
else:
|
||||||
fields[variant_name] = self
|
raise ValueError('We do not have processor: %s.' % variant)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
variant_name = self.field_name
|
self.processor = self.build_processor(self.default_processor)
|
||||||
fields[variant_name] = self
|
|
||||||
|
|
||||||
return fields
|
self.fields = self.processor.generate_field()
|
||||||
|
self.argument = argument or self.default_argument
|
||||||
|
|
||||||
|
def build_processor(self, variant):
|
||||||
|
processor_class = PROCESSORS[variant]
|
||||||
|
return processor_class(self, self.processor)
|
||||||
|
|
||||||
def generate_es_query(self, arguments):
|
def generate_es_query(self, arguments):
|
||||||
"""
|
"""
|
||||||
|
@ -60,24 +49,7 @@ class StringFilterES(object): # pylint: disable=R0902
|
||||||
:param arguments: parameters of the query.
|
:param arguments: parameters of the query.
|
||||||
:return: Returns a elasticsearch_dsl.Q query object.
|
:return: Returns a elasticsearch_dsl.Q query object.
|
||||||
"""
|
"""
|
||||||
queries = []
|
return self.processor.generate_es_query(arguments)
|
||||||
|
|
||||||
for argument, value in six.iteritems(arguments):
|
|
||||||
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}})
|
|
||||||
|
|
||||||
def Argument(self):
|
def Argument(self):
|
||||||
"""
|
"""
|
||||||
|
@ -85,3 +57,18 @@ class StringFilterES(object): # pylint: disable=R0902
|
||||||
:return: A Argument type
|
:return: A Argument type
|
||||||
"""
|
"""
|
||||||
return self.argument.Argument()
|
return self.argument.Argument()
|
||||||
|
|
||||||
|
|
||||||
|
class StringFilterES(FilterES):
|
||||||
|
"""String Fields specific to ElasticSearch."""
|
||||||
|
default_processor = 'contains'
|
||||||
|
|
||||||
|
|
||||||
|
class BoolFilterES(FilterES):
|
||||||
|
"""Boolean filter to ES"""
|
||||||
|
default_argument = Boolean()
|
||||||
|
|
||||||
|
|
||||||
|
class NumberFilterES(FilterES):
|
||||||
|
"""Filter to an numeric value to ES"""
|
||||||
|
default_argument = Int()
|
||||||
|
|
|
@ -2,19 +2,40 @@
|
||||||
import copy
|
import copy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from elasticsearch_dsl import Q
|
from elasticsearch_dsl import Q
|
||||||
from graphene import Enum, InputObjectType, Field
|
from graphene import Enum, InputObjectType, Field, Int, Float
|
||||||
from django_elasticsearch_dsl import StringField, TextField
|
from django_elasticsearch_dsl import StringField, TextField, BooleanField, IntegerField, FloatField, LongField, \
|
||||||
|
ShortField, DoubleField, DateField, KeywordField
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
from django_filters.utils import try_dbfield
|
from django_filters.utils import try_dbfield
|
||||||
from django_filters.filterset import BaseFilterSet
|
from django_filters.filterset import BaseFilterSet
|
||||||
|
|
||||||
from .filters import StringFilterES
|
from .filters import StringFilterES, FilterES, BoolFilterES, NumberFilterES
|
||||||
|
|
||||||
# Basic conversion from ES fields to FilterES fields
|
# Basic conversion from ES fields to FilterES fields
|
||||||
FILTER_FOR_ESFIELD_DEFAULTS = {
|
FILTER_FOR_ESFIELD_DEFAULTS = {
|
||||||
StringField: {'filter_class': StringFilterES},
|
StringField: {'filter_class': StringFilterES},
|
||||||
TextField: {'filter_class': StringFilterES},
|
TextField: {'filter_class': StringFilterES},
|
||||||
|
BooleanField: {'filter_class': BoolFilterES},
|
||||||
|
IntegerField: {'filter_class': NumberFilterES},
|
||||||
|
FloatField: {'filter_class': NumberFilterES,
|
||||||
|
'extra': {
|
||||||
|
'argument': Int()
|
||||||
|
}},
|
||||||
|
LongField: {'filter_class': NumberFilterES,
|
||||||
|
'extra': {
|
||||||
|
'argument': Int()
|
||||||
|
}},
|
||||||
|
ShortField: {'filter_class': NumberFilterES,
|
||||||
|
'extra': {
|
||||||
|
'argument': Int()
|
||||||
|
}},
|
||||||
|
DoubleField: {'filter_class': NumberFilterES,
|
||||||
|
'extra': {
|
||||||
|
'argument': Float()
|
||||||
|
}},
|
||||||
|
DateField: {'filter_class': StringFilterES},
|
||||||
|
KeywordField: {'filter_class': StringFilterES},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,9 +75,12 @@ class FilterSetESOptions(object):
|
||||||
class Meta:
|
class Meta:
|
||||||
index = UserIndex
|
index = UserIndex
|
||||||
includes = {
|
includes = {
|
||||||
'username': ['term']
|
'username': {
|
||||||
'last_login': ['lte', 'gte]
|
'field_name': 'graphene_field',
|
||||||
}
|
'field_name_es': 'elasticsearch_field',
|
||||||
|
'lookup_expressions': ['term', 'contains']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
The list syntax will create an filter with a behavior by default,
|
The list syntax will create an filter with a behavior by default,
|
||||||
for each field included in includes. The dictionary syntax will
|
for each field included in includes. The dictionary syntax will
|
||||||
|
@ -68,11 +92,12 @@ class FilterSetESOptions(object):
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
class UserFilter(FilterSetES):
|
class UserFilter(FilterSetES):
|
||||||
username = StringFieldES('username', core_type='text', expr=['partial'])
|
username = StringFieldES(field_name='username', lookup_expressions=['contains'])
|
||||||
class Meta:
|
class Meta:
|
||||||
index = UserIndex
|
index = UserIndex
|
||||||
includes = {
|
includes = {
|
||||||
'username': ['term', 'word']
|
'username': {
|
||||||
|
'lookup_expressions': ['term', 'contains']
|
||||||
}
|
}
|
||||||
|
|
||||||
A query with username as a parameter, will match those words with the
|
A query with username as a parameter, will match those words with the
|
||||||
|
@ -127,7 +152,7 @@ class FilterSetESMetaclass(type):
|
||||||
|
|
||||||
def __new__(mcs, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
"""Get filters declared explicitly in the class"""
|
"""Get filters declared explicitly in the class"""
|
||||||
|
# get declared as field
|
||||||
declared_filters = mcs.get_declared_filters(bases, attrs)
|
declared_filters = mcs.get_declared_filters(bases, attrs)
|
||||||
attrs['declared_filters'] = declared_filters
|
attrs['declared_filters'] = declared_filters
|
||||||
|
|
||||||
|
@ -135,16 +160,20 @@ class FilterSetESMetaclass(type):
|
||||||
|
|
||||||
if issubclass(new_class, BaseFilterSet):
|
if issubclass(new_class, BaseFilterSet):
|
||||||
new_class._meta = FilterSetESOptions(getattr(new_class, 'Meta', None))
|
new_class._meta = FilterSetESOptions(getattr(new_class, 'Meta', None))
|
||||||
base_filters = OrderedDict()
|
|
||||||
|
|
||||||
|
# get declared as meta
|
||||||
|
meta_filters = mcs.get_meta_filters(new_class._meta)
|
||||||
|
|
||||||
|
declared_filters.update(meta_filters)
|
||||||
|
new_class.filters_es = declared_filters
|
||||||
|
|
||||||
|
# recollecting registered graphene fields
|
||||||
|
base_filters = OrderedDict()
|
||||||
for name, filter_field in six.iteritems(declared_filters):
|
for name, filter_field in six.iteritems(declared_filters):
|
||||||
base_filters.update(filter_field.fields)
|
base_filters.update(filter_field.fields)
|
||||||
|
|
||||||
meta_filters = mcs.get_meta_filters(new_class._meta)
|
# adding sort field
|
||||||
base_filters.update(meta_filters)
|
|
||||||
|
|
||||||
sort_fields = {}
|
sort_fields = {}
|
||||||
|
|
||||||
if new_class._meta.order_by is not None:
|
if new_class._meta.order_by is not None:
|
||||||
sort_fields = mcs.generate_sort_field(new_class._meta.order_by)
|
sort_fields = mcs.generate_sort_field(new_class._meta.order_by)
|
||||||
sort_type = mcs.create_sort_enum(name, sort_fields)
|
sort_type = mcs.create_sort_enum(name, sort_fields)
|
||||||
|
@ -166,9 +195,9 @@ class FilterSetESMetaclass(type):
|
||||||
|
|
||||||
# List of filters declared in the class as static fields.
|
# List of filters declared in the class as static fields.
|
||||||
filters = [
|
filters = [
|
||||||
(filter_name, attrs.pop(filter_name))
|
(obj.field_name, attrs.pop(filter_name))
|
||||||
for filter_name, obj in list(attrs.items())
|
for filter_name, obj in list(attrs.items())
|
||||||
if isinstance(obj, StringFilterES)
|
if isinstance(obj, FilterES)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Merge declared filters from base classes
|
# Merge declared filters from base classes
|
||||||
|
@ -191,7 +220,7 @@ class FilterSetESMetaclass(type):
|
||||||
for name, index_field, data in index_fields:
|
for name, index_field, data in index_fields:
|
||||||
|
|
||||||
filter_class = mcs.get_filter_exp(name, index_field, data)
|
filter_class = mcs.get_filter_exp(name, index_field, data)
|
||||||
meta_filters.update(filter_class.fields)
|
meta_filters.update({name: filter_class})
|
||||||
|
|
||||||
return meta_filters
|
return meta_filters
|
||||||
|
|
||||||
|
@ -229,7 +258,6 @@ class FilterSetESMetaclass(type):
|
||||||
# This inner field is not filterable
|
# This inner field is not filterable
|
||||||
continue
|
continue
|
||||||
inner_data = data[inner_name] if data else None
|
inner_data = data[inner_name] if data else None
|
||||||
|
|
||||||
index_fields.append(mcs.get_filter_exp(inner_name, inner_field, inner_data, root=name))
|
index_fields.append(mcs.get_filter_exp(inner_name, inner_field, inner_data, root=name))
|
||||||
|
|
||||||
return index_fields
|
return index_fields
|
||||||
|
@ -245,27 +273,23 @@ class FilterSetESMetaclass(type):
|
||||||
|
|
||||||
# Get lookup_expr from configuration
|
# Get lookup_expr from configuration
|
||||||
if data and 'lookup_expressions' in data:
|
if data and 'lookup_expressions' in data:
|
||||||
if 'lookup_expressions' in kwargs:
|
kwargs['lookup_expressions'] = set(data['lookup_expressions'])
|
||||||
kwargs['lookup_expressions'] = set(kwargs['lookup_expressions'])\
|
|
||||||
.intersection(set(data['lookup_expressions']))
|
|
||||||
else:
|
|
||||||
kwargs['lookup_expressions'] = set(data['lookup_expressions'])
|
|
||||||
elif 'lookup_expressions' in kwargs:
|
elif 'lookup_expressions' in kwargs:
|
||||||
kwargs['lookup_expressions'] = set(kwargs['lookup_expressions'])
|
kwargs['lookup_expressions'] = set(kwargs['lookup_expressions'])
|
||||||
|
|
||||||
kwargs['name'], kwargs['attr'] = mcs.get_name(name, root, data)
|
kwargs['field_name'], kwargs['field_name_es'] = mcs.get_name(name, root, data)
|
||||||
return filter_class(**kwargs)
|
return filter_class(**kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_name(name, root, data):
|
def get_name(name, root, data):
|
||||||
"""Get names of the field and the path to resolve it"""
|
"""Get names of the field and the path to resolve it"""
|
||||||
field_name = data.get('name', None) if data else None
|
field_name = data.get('field_name', None) if data else None
|
||||||
attr = data.get('attr', None) if data else None
|
field_name_es = data.get('field_name_es', None) if data else None
|
||||||
if not field_name:
|
if not field_name:
|
||||||
field_name = '{root}_{name}'.format(root=root, name=name) if root else name
|
field_name = '{root}_{name}'.format(root=root, name=name) if root else name
|
||||||
if not attr:
|
if not field_name_es:
|
||||||
attr = '{root}.{name}'.format(root=root, name=name) if root else name
|
field_name_es = '{root}.{name}'.format(root=root, name=name) if root else name
|
||||||
return field_name, attr
|
return field_name, field_name_es
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_sort_enum(name, sort_fields):
|
def create_sort_enum(name, sort_fields):
|
||||||
|
@ -347,12 +371,11 @@ class FilterSetES(six.with_metaclass(FilterSetESMetaclass, object)):
|
||||||
# if the query have data
|
# if the query have data
|
||||||
if len(self.data):
|
if len(self.data):
|
||||||
# for each field passed to the query
|
# for each field passed to the query
|
||||||
for name in self.data:
|
for name, filter in six.iteritems(self.filters_es):
|
||||||
filter_es = self.base_filters.get(name)
|
|
||||||
# If a target filter is en FilterEs
|
# If a target filter is en FilterEs
|
||||||
if isinstance(filter_es, StringFilterES):
|
if isinstance(filter, FilterES):
|
||||||
# It is generated a query or response None if the filter don't have data
|
# It is generated a query or response None if the filter don't have data
|
||||||
query_filter = filter_es.generate_es_query(self.data)
|
query_filter = filter.generate_es_query(self.data)
|
||||||
|
|
||||||
if query_filter is not None:
|
if query_filter is not None:
|
||||||
query_base += query_filter
|
query_base += query_filter
|
||||||
|
|
167
graphene_django/elasticsearch/filter/processors.py
Normal file
167
graphene_django/elasticsearch/filter/processors.py
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from elasticsearch_dsl import Q
|
||||||
|
from graphene import List
|
||||||
|
|
||||||
|
|
||||||
|
class Processor(object):
|
||||||
|
suffix_expr = 'term'
|
||||||
|
|
||||||
|
def __init__(self, filter_es, parent_processor=None):
|
||||||
|
"""
|
||||||
|
Abstract processor to generate graphene field and ES query to lookups
|
||||||
|
:type filter_es: graphene_django.elasticsearch.filter.filterset.FilterES
|
||||||
|
:type parent_processor: graphene_django.elasticsearch.filter.filterset.Processor
|
||||||
|
"""
|
||||||
|
self.filter_es = filter_es
|
||||||
|
self.parent_processor = parent_processor
|
||||||
|
self.variant_name = self._get_variant_name()
|
||||||
|
|
||||||
|
def generate_field(self):
|
||||||
|
"""Field Decorator"""
|
||||||
|
self_field = self._build_field()
|
||||||
|
|
||||||
|
if self.parent_processor is not None:
|
||||||
|
parent_fields = self.parent_processor.generate_field()
|
||||||
|
parent_fields.update(self_field)
|
||||||
|
return parent_fields
|
||||||
|
|
||||||
|
else:
|
||||||
|
return self_field
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return self.filter_es.argument
|
||||||
|
|
||||||
|
def generate_es_query(self, data):
|
||||||
|
|
||||||
|
if self.variant_name in data:
|
||||||
|
value = data.get(self.variant_name)
|
||||||
|
self_query = self._build_query(value)
|
||||||
|
else:
|
||||||
|
self_query = Q("bool")
|
||||||
|
|
||||||
|
if self.parent_processor is not None:
|
||||||
|
parent_query = self.parent_processor.generate_es_query(data)
|
||||||
|
parent_query += self_query
|
||||||
|
return parent_query
|
||||||
|
|
||||||
|
else:
|
||||||
|
return self_query
|
||||||
|
|
||||||
|
def _build_field(self):
|
||||||
|
variant_name = self.variant_name
|
||||||
|
|
||||||
|
return OrderedDict({variant_name: self.filter_es})
|
||||||
|
|
||||||
|
def _get_variant_name(self):
|
||||||
|
if self.suffix_expr == self.filter_es.default_filter_processor:
|
||||||
|
variant_name = self.filter_es.field_name
|
||||||
|
|
||||||
|
else:
|
||||||
|
variant_name = "%s_%s" % (self.filter_es.field_name, self.suffix_expr)
|
||||||
|
|
||||||
|
return variant_name
|
||||||
|
|
||||||
|
def _build_query(self, value):
|
||||||
|
result = len(self.filter_es.field_name_es)
|
||||||
|
|
||||||
|
if result > 1:
|
||||||
|
queries = [self._get_query(name, value) for name in self.filter_es.field_name_es]
|
||||||
|
return Q("bool", must={"bool": {"should": queries}})
|
||||||
|
|
||||||
|
return Q("bool", must=self._get_query(self.filter_es.field_name_es[0], value))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('term', **{name: value})
|
||||||
|
|
||||||
|
|
||||||
|
class TermProcessor(Processor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ContainsProcessor(Processor):
|
||||||
|
suffix_expr = 'contains'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('match',
|
||||||
|
**{name: {
|
||||||
|
"query": value,
|
||||||
|
"fuzziness": "auto"
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
class RegexProcessor(Processor):
|
||||||
|
suffix_expr = 'regex'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('wildcard', **{name: value})
|
||||||
|
|
||||||
|
|
||||||
|
class PhraseProcessor(Processor):
|
||||||
|
suffix_expr = 'phrase'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('match_phrase',
|
||||||
|
**{name: {
|
||||||
|
"query": value
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
class PrefixProcessor(Processor):
|
||||||
|
suffix_expr = 'prefix'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('match_phrase_prefix',
|
||||||
|
**{name: {
|
||||||
|
"query": value
|
||||||
|
}})
|
||||||
|
|
||||||
|
|
||||||
|
class InProcessor(Processor):
|
||||||
|
suffix_expr = 'in'
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
return List(self.filter_es.argument.Argument().type)
|
||||||
|
|
||||||
|
|
||||||
|
class ExitsProcessor(Processor):
|
||||||
|
suffix_expr = 'exits'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q('bool', **{
|
||||||
|
'must' if value else 'must_not': {'exists': {'field': name}}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class LteProcessor(Processor):
|
||||||
|
suffix_expr = 'lte'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q("bool", must={'range': {name: {'lte': value}}})
|
||||||
|
|
||||||
|
|
||||||
|
class GteProcessor(Processor):
|
||||||
|
suffix_expr = 'gte'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_query(name, value):
|
||||||
|
return Q("bool", must={'range': {name: {'gte': value}}})
|
||||||
|
|
||||||
|
|
||||||
|
PROCESSORS = {
|
||||||
|
"contains": ContainsProcessor,
|
||||||
|
"term": TermProcessor,
|
||||||
|
"regex": RegexProcessor,
|
||||||
|
"phrase": PhraseProcessor,
|
||||||
|
"prefix": PrefixProcessor,
|
||||||
|
"in": InProcessor,
|
||||||
|
"lte": LteProcessor,
|
||||||
|
"gte": GteProcessor,
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ class ArticleFilterESAsField(FilterSetES):
|
||||||
includes = []
|
includes = []
|
||||||
order_by = ['id']
|
order_by = ['id']
|
||||||
|
|
||||||
headline = filters.StringFilterES(attr='headline', lookup_expressions=['term', 'contain'])
|
headline = filters.StringFilterES(field_name='headline', lookup_expressions=['term', 'contains'])
|
||||||
|
|
||||||
|
|
||||||
class ArticleFilterESInMeta(FilterSetES):
|
class ArticleFilterESInMeta(FilterSetES):
|
||||||
|
@ -47,11 +47,25 @@ class ArticleFilterESInMetaDict(FilterSetES):
|
||||||
index = ArticleDocument
|
index = ArticleDocument
|
||||||
includes = {
|
includes = {
|
||||||
'headline': {
|
'headline': {
|
||||||
'lookup_expressions': ['term', 'contain']
|
'lookup_expressions': ['term', 'contains']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleFilterMultiField(FilterSetES):
|
||||||
|
"""Article Filter for ES"""
|
||||||
|
class Meta(object):
|
||||||
|
"""Metaclass data"""
|
||||||
|
index = ArticleDocument
|
||||||
|
includes = []
|
||||||
|
|
||||||
|
headline = filters.StringFilterES(
|
||||||
|
field_name='contain',
|
||||||
|
field_name_es=['headline', 'lang'],
|
||||||
|
lookup_expressions=['contains']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ESFilterQuery(ObjectType):
|
class ESFilterQuery(ObjectType):
|
||||||
"""A query for ES fields"""
|
"""A query for ES fields"""
|
||||||
articles_as_field = DjangoESFilterConnectionField(
|
articles_as_field = DjangoESFilterConnectionField(
|
||||||
|
@ -63,3 +77,6 @@ class ESFilterQuery(ObjectType):
|
||||||
articles_in_meta_dict = DjangoESFilterConnectionField(
|
articles_in_meta_dict = DjangoESFilterConnectionField(
|
||||||
ArticleNode, filterset_class=ArticleFilterESInMetaDict
|
ArticleNode, filterset_class=ArticleFilterESInMetaDict
|
||||||
)
|
)
|
||||||
|
articles_in_multi_field = DjangoESFilterConnectionField(
|
||||||
|
ArticleNode, filterset_class=ArticleFilterMultiField
|
||||||
|
)
|
||||||
|
|
|
@ -78,11 +78,19 @@ def filter_generation(field, query_str, expected_arguments, method_to_mock="quer
|
||||||
assert result.data[field]["edges"][1]["node"]["headline"] == "a2"
|
assert result.data[field]["edges"][1]["node"]["headline"] == "a2"
|
||||||
|
|
||||||
|
|
||||||
def test_filter_as_field():
|
def test_filter_string():
|
||||||
filter_generation(
|
filter_generation(
|
||||||
"articlesAsField",
|
"articlesAsField",
|
||||||
"headline: \"A text\"",
|
"headline: \"A text\"",
|
||||||
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
|
filters.StringFilterES(field_name='headline').generate_es_query({"headline": "A text"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_string_date():
|
||||||
|
filter_generation(
|
||||||
|
"articlesAsField",
|
||||||
|
"headline: \"A text\"",
|
||||||
|
filters.StringFilterES(field_name='headline').generate_es_query({"headline": "A text"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +107,7 @@ def test_filter_in_meta():
|
||||||
filter_generation(
|
filter_generation(
|
||||||
"articlesInMeta",
|
"articlesInMeta",
|
||||||
"headline: \"A text\"",
|
"headline: \"A text\"",
|
||||||
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
|
filters.StringFilterES(field_name='headline').generate_es_query({"headline": "A text"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,5 +115,16 @@ def test_filter_in_meta_dict():
|
||||||
filter_generation(
|
filter_generation(
|
||||||
"articlesInMetaDict",
|
"articlesInMetaDict",
|
||||||
"headline: \"A text\"",
|
"headline: \"A text\"",
|
||||||
filters.StringFilterES(attr='headline').generate_es_query({"headline": "A text"}),
|
filters.StringFilterES(field_name='headline').generate_es_query({"headline": "A text"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_in_multi_field():
|
||||||
|
filter_generation(
|
||||||
|
"articlesInMultiField",
|
||||||
|
"contain: \"A text\"",
|
||||||
|
filters.StringFilterES(
|
||||||
|
field_name='contain',
|
||||||
|
field_name_es=['headline', 'lang'],
|
||||||
|
).generate_es_query({"contain": "A text"}),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user