Merge branch 'feature/products_search' into 'develop'

Feature/products search

See merge request gm/gm-backend!126
This commit is contained in:
e.stoyushko 2019-11-14 09:16:15 +00:00
commit cf04f55bd3
12 changed files with 338 additions and 56 deletions

View File

@ -160,7 +160,7 @@ class WineRegionQuerySet(models.QuerySet):
"""Wine region queryset."""
class WineRegion(models.Model):
class WineRegion(models.Model, TranslatedFieldsMixin):
"""Wine region model."""
name = models.CharField(_('name'), max_length=255)
country = models.ForeignKey(Country, on_delete=models.PROTECT,

View File

@ -78,6 +78,12 @@ class ProductQuerySet(models.QuerySet):
return self.select_related('product_type', 'establishment') \
.prefetch_related('product_type__subtypes')
def with_es_related(self):
"""Returns qs with related for ES indexing."""
return self.with_base_related().prefetch_related('tags', 'classifications', 'classifications__tags',
'classifications__type', 'classifications_standard',
'standards').select_related('wine_region', 'wine_sub_region')
def common(self):
return self.filter(category=self.model.COMMON)

View File

@ -1,9 +1,11 @@
from search_indexes.documents.establishment import EstablishmentDocument
from search_indexes.documents.news import NewsDocument
from search_indexes.documents.product import ProductDocument
# todo: make signal to update documents on related fields
__all__ = [
'EstablishmentDocument',
'NewsDocument',
'ProductDocument',
]

View File

@ -0,0 +1,116 @@
"""Product app documents."""
from django.conf import settings
from django_elasticsearch_dsl import Document, Index, fields
from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from product import models
ProductIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'product'))
ProductIndex.settings(number_of_shards=1, number_of_replicas=1, mapping={'total_fields':{'limit': 3000}})
@ProductIndex.doc_type
class ProductDocument(Document):
"""Product document."""
description = fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES)
product_type = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
'index_name': fields.KeywordField(),
'use_subtypes': fields.BooleanField(),
})
subtypes = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES),
'index_name': fields.KeywordField(),
},
multi=True
)
establishment = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'slug': fields.KeywordField(),
# 'city' TODO: city indexing
}
)
wine_colors = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True,
)
wine_region = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
})
wine_sub_region = fields.ObjectField(properties={'name': fields.KeywordField()})
classifications = fields.ObjectField( # TODO
properties={
'classification_type': fields.ObjectField(properties={}),
'standard': fields.ObjectField(properties={
'name': fields.KeywordField(),
'standard_type': fields.IntegerField(),
# 'coordinates': fields.GeoPointField(),
}),
'tags': fields.ObjectField(
properties={
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True
),
},
multi=True
)
standards = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'standard_type': fields.IntegerField(),
# 'coordinates': fields.GeoPointField(),
},
multi=True
)
wine_village = fields.ObjectField(properties={
'name': fields.KeywordField(),
})
tags = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True
)
class Django:
model = models.Product
fields = (
'id',
'category',
'name',
'available',
'public_mark',
'slug',
'old_id',
'state',
'old_unique_key',
'vintage',
)
related_models = [models.ProductType]
def get_queryset(self):
return super().get_queryset().published().with_es_related()

View File

@ -4,6 +4,7 @@ from elasticsearch_dsl import AttrDict
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from news.serializers import NewsTypeSerializer
from search_indexes.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument
from search_indexes.utils import get_translated_value
@ -19,6 +20,62 @@ class TagsDocumentSerializer(serializers.Serializer):
return get_translated_value(obj.label)
class ProductSubtypeDocumentSerializer(serializers.Serializer):
"""Product subtype serializer for ES Document."""
id = serializers.IntegerField()
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translted(obj):
return get_translated_value(obj.name)
class WineRegionCountryDocumentSerialzer(serializers.Serializer):
"""Wine region country ES document serializer."""
id = serializers.IntegerField()
code = serializers.CharField()
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translated(obj):
return get_translated_value(obj.name)
def get_attribute(self, instance):
return instance.country
class WineRegionDocumentSerializer(serializers.Serializer):
"""Wine region ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
country = WineRegionCountryDocumentSerialzer(allow_null=True)
class WineColorDocumentSerializer(serializers.Serializer):
"""Wine color ES document serializer,"""
id = serializers.IntegerField()
label_translated = serializers.SerializerMethodField()
index_name = serializers.CharField(source='value')
@staticmethod
def get_label_translated(obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('label'))
return get_translated_value(obj.label)
class ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
class CityDocumentShortSerializer(serializers.Serializer):
"""City serializer for ES Document,"""
@ -123,3 +180,41 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
# 'establishment_type',
# 'establishment_subtypes',
)
class ProductDocumentSerializer(DocumentSerializer):
"""Product document serializer"""
tags = TagsDocumentSerializer(many=True)
subtypes = ProductSubtypeDocumentSerializer(many=True)
wine_region = WineRegionDocumentSerializer(allow_null=True)
wine_colors = WineColorDocumentSerializer(many=True)
product_type = serializers.SerializerMethodField()
establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True)
@staticmethod
def get_product_type(obj):
return get_translated_value(obj.product_type.name if obj.product_type else {})
class Meta:
"""Meta class."""
document = ProductDocument
fields = (
'id',
'category',
'name',
'available',
'public_mark',
'slug',
'old_id',
'state',
'old_unique_key',
'vintage',
'tags',
'product_type',
'subtypes',
'wine_region',
'wine_colors',
'establishment_detail',
)

View File

@ -11,44 +11,20 @@ def update_document(sender, **kwargs):
model_name = sender._meta.model_name
instance = kwargs['instance']
if app_label == 'location':
if model_name == 'country':
establishments = Establishment.objects.filter(
address__city__country=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'city':
establishments = Establishment.objects.filter(
address__city=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'address':
establishments = Establishment.objects.filter(
address=instance)
for establishment in establishments:
registry.update(establishment)
if app_label == 'establishment':
app_label_model_name_to_filter = {
('location','country'): 'address__city__country',
('location','city'): 'address__city',
('location', 'address'): 'address',
# todo: remove after migration
from establishment import models as establishment_models
if model_name == 'establishmenttype':
if isinstance(instance, establishment_models.EstablishmentType):
establishments = Establishment.objects.filter(
establishment_type=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'establishmentsubtype':
if isinstance(instance, establishment_models.EstablishmentSubType):
establishments = Establishment.objects.filter(
establishment_subtypes=instance)
for establishment in establishments:
registry.update(establishment)
if app_label == 'tag':
if model_name == 'tag':
establishments = Establishment.objects.filter(tags=instance)
for establishment in establishments:
registry.update(establishment)
('establishment', 'establishmenttype'): 'establishment_type',
('establishment', 'establishmentsubtype'): 'establishment_subtypes',
('tag', 'tag'): 'tags',
}
filter_name = app_label_model_name_to_filter.get((app_label, model_name))
if filter_name:
qs = Establishment.objects.filter(**{filter_name: instance})
for product in qs:
registry.update(product)
@receiver(post_save)
@ -57,21 +33,35 @@ def update_news(sender, **kwargs):
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance']
app_label_model_name_to_filter = {
('location','country'): 'country',
('news','newstype'): 'news_type',
('tag', 'tag'): 'tags',
}
filter_name = app_label_model_name_to_filter.get((app_label, model_name))
if filter_name:
qs = News.objects.filter(**{filter_name: instance})
for product in qs:
registry.update(product)
if app_label == 'location':
if model_name == 'country':
qs = News.objects.filter(country=instance)
for news in qs:
registry.update(news)
if app_label == 'news':
if model_name == 'newstype':
qs = News.objects.filter(news_type=instance)
for news in qs:
registry.update(news)
if app_label == 'tag':
if model_name == 'tag':
qs = News.objects.filter(tags=instance)
for news in qs:
registry.update(news)
@receiver(post_save)
def update_product(sender, **kwargs):
from product.models import Product
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance']
app_label_model_name_to_filter = {
('product','productstandard'): 'standards',
('product', 'producttype'): 'product_type',
('tag','tag'): 'tags',
('location', 'wineregion'): 'wine_region',
('location', 'winesubregion'): 'wine_sub_region',
('location', 'winevillage'): 'wine_village',
('establishment', 'establishment'): 'establishment',
}
filter_name = app_label_model_name_to_filter.get((app_label, model_name))
if filter_name:
qs = Product.objects.filter(**{filter_name: instance})
for product in qs:
registry.update(product)

View File

@ -8,6 +8,7 @@ router = routers.SimpleRouter()
router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment')
router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile')
router.register(r'news', views.NewsDocumentViewSet, basename='news')
router.register(r'products', views.ProductDocumentViewSet, basename='product')
urlpatterns = router.urls

View File

@ -9,6 +9,7 @@ from django_elasticsearch_dsl_drf.filter_backends import (
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from search_indexes import serializers, filters
from search_indexes.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument
from utils.pagination import ProjectMobilePagination
@ -192,3 +193,71 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
]
}
}
class ProductDocumentViewSet(BaseDocumentViewSet):
"""Product document ViewSet."""
document = ProductDocument
lookup_field = 'slug'
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductDocumentSerializer
# def get_queryset(self):
# qs = super(ProductDocumentViewSet, self).get_queryset()
# qs = qs.filter('match', is_publish=True)
# return qs
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
]
search_fields = {
'name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'transliterated_name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'index_name': {'fuzziness': 'auto:3,4',
'boost': '2'},
'description': {'fuzziness': 'auto'},
}
translated_search_fields = (
'description',
)
filter_fields = {
'slug': 'slug',
'tags_id': {
'field': 'tags.id',
'lookups': [constants.LOOKUP_QUERY_IN]
},
'wine_colors_id': {
'field': 'wine_colors.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
]
},
'wine_from_country_code': {
'field': 'wine_region.country.code',
},
'for_establishment': {
'field': 'establishment.slug',
},
'type': {
'field': 'product_type.index_name',
},
'subtype': {
'field': 'subtypes.index_name',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
]
}
}
geo_spatial_filter_fields = {
}

View File

@ -24,7 +24,7 @@ services:
- 9200:9200
- 9300:9300
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
- discovery.type=single-node
- xpack.security.enabled=false

View File

@ -28,8 +28,9 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
'search_indexes.documents.news': 'development_news', # temporarily disabled
'search_indexes.documents.news': 'development_news',
'search_indexes.documents.establishment': 'development_establishment',
'search_indexes.documents.product': 'development_product',
}

View File

@ -98,6 +98,7 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'local_news',
'search_indexes.documents.establishment': 'local_establishment',
'search_indexes.documents.product': 'local_product',
}

View File

@ -32,6 +32,7 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
'search_indexes.documents.news': 'development_news', # temporarily disabled
'search_indexes.documents.establishment': 'development_establishment',
'search_indexes.documents.product': 'development_product',
}