From beab74a4f46e5e9ed44cf2783b247db6c5b8db83 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 05:07:25 +0300 Subject: [PATCH 01/11] added queried search --- app/conf/settings/base.py | 1 + app/search/api/serializers.py | 12 +++++- app/search/api/views.py | 43 +++++++++++++-------- app/search/models.py | 10 ++++- app/search/services/autocomplete_schema.py | 45 +++++++++++----------- app/search/services/hints.py | 6 +-- app/search/services/search.py | 44 +++++++++++++++++++-- pg.sql | 2 + 8 files changed, 117 insertions(+), 46 deletions(-) create mode 100644 pg.sql diff --git a/app/conf/settings/base.py b/app/conf/settings/base.py index 9f06045..0cf4f96 100644 --- a/app/conf/settings/base.py +++ b/app/conf/settings/base.py @@ -60,6 +60,7 @@ DJANGO_APPS = [ "django.contrib.humanize", "django.contrib.admin", "django.forms", + "django.contrib.postgres", ] THIRD_PARTY_APPS = ["rest_framework", "corsheaders", "drf_yasg"] diff --git a/app/search/api/serializers.py b/app/search/api/serializers.py index aa79b4b..227e69f 100644 --- a/app/search/api/serializers.py +++ b/app/search/api/serializers.py @@ -2,9 +2,19 @@ from rest_framework import serializers from django.core.validators import MinLengthValidator, MinValueValidator +class QueryFilterSerializer(serializers.Serializer): + value = serializers.CharField(max_length=100) + type = serializers.CharField(max_length=100) + + def create(self, validated_data): + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError + class SearchSerializer(serializers.Serializer): - body = serializers.CharField(max_length=200) + body = serializers.ListSerializer(child=QueryFilterSerializer()) def create(self, validated_data): raise NotImplementedError diff --git a/app/search/api/views.py b/app/search/api/views.py index 81e5993..b81d958 100644 --- a/app/search/api/views.py +++ b/app/search/api/views.py @@ -5,47 +5,60 @@ from rest_framework.response import Response from rest_framework.views import APIView from search.api.serializers import HintRequestSerializer -from search.api.serializers import SearchSerializer, ResponseSerializer, HintResponseSerializer, AutoCompleteRequestSerializer, AutoCompleteResponseSerializer -from search.services.search import process_string +from search.api.serializers import ( + SearchSerializer, + ResponseSerializer, + HintResponseSerializer, + AutoCompleteRequestSerializer, + AutoCompleteResponseSerializer, +) +from search.services.search import process_search from search.services.autocomplete_schema import autocomplete_schema from search.services.hints import get_hints user_response = openapi.Response("search results", ResponseSerializer) hint_response = openapi.Response("hints", HintResponseSerializer) -autocomplete_response = openapi.Response("autocomplete schema", AutoCompleteResponseSerializer) +autocomplete_response = openapi.Response( + "autocomplete schema", AutoCompleteResponseSerializer +) class SearchApi(APIView): @swagger_auto_schema(request_body=SearchSerializer, responses={200: user_response}) - def post(self, request, format=None): + def post(self, request): serializer = SearchSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( - process_string(serializer.data["body"]), status=status.HTTP_200_OK + process_search(serializer.data["body"]), status=status.HTTP_200_OK ) class HintApi(APIView): - @swagger_auto_schema(request_body=HintRequestSerializer, responses={200: hint_response}) - def post(self, request, format=None): + @swagger_auto_schema( + request_body=HintRequestSerializer, responses={200: hint_response} + ) + def post(self, request): serializer = HintRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( { - 'type': get_hints(serializer.data['content']), - 'value': serializer.data['content'] + "type": get_hints(serializer.data["content"]), + "value": serializer.data["content"], }, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) + class AutoCompleteApi(APIView): - @swagger_auto_schema(request_body=AutoCompleteRequestSerializer, responses={200: autocomplete_response}) - def post(self, request, format=None): + @swagger_auto_schema( + request_body=AutoCompleteRequestSerializer, + responses={200: autocomplete_response}, + ) + def post(self, request): serializer = AutoCompleteRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( - { - 'nodes': autocomplete_schema(serializer.data['content']) - }, status=status.HTTP_200_OK + {"nodes": autocomplete_schema(serializer.data["content"])}, + status=status.HTTP_200_OK, ) diff --git a/app/search/models.py b/app/search/models.py index 6dc7e69..8cec9db 100644 --- a/app/search/models.py +++ b/app/search/models.py @@ -49,6 +49,8 @@ class Product(models.Model): Category, related_name="products", on_delete=models.CASCADE ) + # score = models.IntegerField(default=0) + def __str__(self): return str(self.name) @@ -56,13 +58,17 @@ class Product(models.Model): return { "name": self.name, "characteristic": [ - x.serialize_self() for x in self.characteristics.objects.all() + x.characteristic.serialize_self() for x in self.characteristics.all() ] - + [x.serialize_self() for x in self.unit_characteristics.objects.all()], + + [ + x.characteristic.serialize_self() + for x in self.unit_characteristics.all() + ], } class Meta: db_table = "product" + # ordering = ["score"] class ProductCharacteristic(models.Model): diff --git a/app/search/services/autocomplete_schema.py b/app/search/services/autocomplete_schema.py index 9e6b185..69db958 100644 --- a/app/search/services/autocomplete_schema.py +++ b/app/search/services/autocomplete_schema.py @@ -1,37 +1,38 @@ from search.models import Product, Category, Characteristic + def autocomplete_schema(val: str): schema = [] schema.extend( [ { - 'coordinate': product['name'].index(val), - 'value': { - 'type': 'Name', - 'value': product['name'], - } - } for product in Product.objects.filter(name__contains=val).values('name')] - ) - schema.extend( - [ - { - 'coordinate': cat['name'].index(val), - 'value': { - 'type': 'Category', - 'value': cat['name'] - } - } for cat in Category.objects.filter(name__contains=val).values('name') + "coordinate": product["name"].index(val), + "value": { + "type": "Name", + "value": product["name"], + }, + } + for product in Product.objects.filter(name__contains=val).values("name") ] ) schema.extend( [ { - 'coordinate': char.name.index(val), - 'value': { - 'type': char.name, - 'value': char.value - } - } for char in Characteristic.objects.filter(name__contains=val).values('name', 'value') + "coordinate": cat["name"].index(val), + "value": {"type": "Category", "value": cat["name"]}, + } + for cat in Category.objects.filter(name__contains=val).values("name") + ] + ) + schema.extend( + [ + { + "coordinate": char.name.index(val), + "value": {"type": char.name, "value": char.value}, + } + for char in Characteristic.objects.filter(name__contains=val).values( + "name", "value" + ) ] ) return schema diff --git a/app/search/services/hints.py b/app/search/services/hints.py index d7d7c42..b4579de 100644 --- a/app/search/services/hints.py +++ b/app/search/services/hints.py @@ -2,11 +2,11 @@ from search.models import Product, Category, Characteristic def get_hints(content: str) -> str: - category = 'Unknown' + category = "Unknown" if content in list(map(lambda product: product.name, Product.objects.all())): - category = 'Name' + category = "Name" elif content in list(map(lambda category: category.name, Category.objects.all())): - category = 'Category' + category = "Category" elif content in list(map(lambda char: char.value, Characteristic.objects.all())): category = Characteristic.objects.get(value=content).name return category diff --git a/app/search/services/search.py b/app/search/services/search.py index 5ebf07e..ed51108 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -1,6 +1,44 @@ -from search.models import Product +from search.models import Product, Characteristic, ProductCharacteristic from typing import List -def process_string(text: str) -> List[dict]: - return [x.serialize_self() for x in Product.objects.filter(name__contains=text)[5:]] +def process_search(data: List[dict]) -> List[dict]: + prep_data = [] + prep_dict = {} + prep_dict_char_type = {} + + for x in data: + dat = dict(x) + if x["type"] in ["Name", "Category", "Unknown"]: + prep_data.append(dat) + else: + if x["type"] in list(prep_dict.keys()): + prep_dict[x["type"]] = prep_dict[ + x["type"] + ] | ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__unaccent__trigram_similar=x["value"], + ) + else: + prep_dict_char_type[x["type"]] = Characteristic.objects.filter( + name__contains=x["type"] + ) + prep_dict[x["type"]] = ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__unaccent__trigram_similar=x["value"], + ) + for el, val in prep_dict.items(): + prep_data.append({"type": el, "value": val}) + qs = Product.objects.filter() + for x in prep_data: + typ = x["type"] + val = x["value"] + if typ == "Name": + qs = qs.filter(name__unaccent__trigram_similar=val) + elif typ == "Category": + qs = qs.filter(category__name__unaccent__trigram_similar=val) + elif typ == "Unknown": + continue + else: + qs = qs.filter(characteristics__in=val) + return [x.serialize_self() for x in qs[:5]] diff --git a/pg.sql b/pg.sql new file mode 100644 index 0000000..84bc687 --- /dev/null +++ b/pg.sql @@ -0,0 +1,2 @@ +CREATE EXTENSION unaccent; +CREATE EXTENSION pg_trgm; From 8b5fddbd679867c2394af8c86aecbe2331c81aad Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 10:55:10 +0300 Subject: [PATCH 02/11] improved search: spelling, unit characteristic lookup, type gassing --- app/conf/settings/base.py | 2 + app/search/models.py | 3 +- app/search/services/autocomplete_schema.py | 4 +- app/search/services/load_products.py | 14 +++ app/search/services/search.py | 103 +++++++++++++++++---- app/search/services/spell_check.py | 8 +- app/search/services/translate.py | 13 ++- 7 files changed, 119 insertions(+), 28 deletions(-) diff --git a/app/conf/settings/base.py b/app/conf/settings/base.py index 0cf4f96..3122d57 100644 --- a/app/conf/settings/base.py +++ b/app/conf/settings/base.py @@ -214,3 +214,5 @@ REST_FRAMEWORK = { # django-cors-headers CORS_ALLOW_ALL_ORIGINS = True + +YANDEX_DICT_API_KEY = "dict.1.1.20221022T010312Z.55cce597a6cfa505.f416aba85e7642eedc1b76b8e21c06506fb17f83" diff --git a/app/search/models.py b/app/search/models.py index 8cec9db..55cf53f 100644 --- a/app/search/models.py +++ b/app/search/models.py @@ -18,13 +18,14 @@ class Characteristic(models.Model): class UnitCharacteristic(models.Model): name = models.TextField("Имя", blank=False) value = models.TextField("Значение", blank=False) + numeric_value = models.IntegerField(default=0) unit = models.TextField("Размерность", blank=False) def __str__(self): return str(self.name) def serialize_self(self): - return {"name": self.name, "value": self.value, "unit": self.unit} + return {"name": self.name, "value": self.numeric_value, "unit": self.unit} class Meta: db_table = "unit_characteristic" diff --git a/app/search/services/autocomplete_schema.py b/app/search/services/autocomplete_schema.py index 69db958..feb6392 100644 --- a/app/search/services/autocomplete_schema.py +++ b/app/search/services/autocomplete_schema.py @@ -27,8 +27,8 @@ def autocomplete_schema(val: str): schema.extend( [ { - "coordinate": char.name.index(val), - "value": {"type": char.name, "value": char.value}, + "coordinate": char["name"].index(val), + "value": {"type": char["name"], "value": char["value"]}, } for char in Characteristic.objects.filter(name__contains=val).values( "name", "value" diff --git a/app/search/services/load_products.py b/app/search/services/load_products.py index c28f54c..af0c235 100644 --- a/app/search/services/load_products.py +++ b/app/search/services/load_products.py @@ -1,3 +1,4 @@ +import re from ast import literal_eval import pandas as pd @@ -59,3 +60,16 @@ def load_excel(): # malformed node or string: nan \ duplicate key print("СКОРОСШИВАТЕЛЬ") continue + + +def process_unit_character(): + for el in UnitCharacteristic.objects.all(): + nums = re.findall("[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d+)?", el.value) + if len(nums) != 1: + el.delete() + else: + try: + el.numeric_value = int(float(nums[0].replace(",", "."))) + el.save() + except ValueError: + el.delete() diff --git a/app/search/services/search.py b/app/search/services/search.py index ed51108..67ba410 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -1,32 +1,96 @@ -from search.models import Product, Characteristic, ProductCharacteristic +import string + +from search.models import ( + Product, + Characteristic, + ProductCharacteristic, + ProductUnitCharacteristic, + UnitCharacteristic, +) from typing import List +from search.services.hints import get_hints +from search.services.spell_check import spell_check +from search.services.translate import translate_en_ru, translate_ru_en -def process_search(data: List[dict]) -> List[dict]: + +def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: str): + if operation.startswith("<=") or operation.startswith("=<"): + return unit.filter(characteristic__numeric_value__lte=int(float(operation[:2]))) + elif operation.startswith("=>") or operation.startswith(">="): + return unit.filter(characteristic__numeric_value__gte=int(float(operation[:2]))) + elif operation.startswith(">"): + return unit.filter(characteristic__numeric_value__gt=int(float(operation[:1]))) + elif operation.startswith("<"): + return unit.filter(characteristic__numeric_value__lt=int(float(operation[:1]))) + elif operation.startswith("="): + return unit.filter(characteristic__numeric_value__gt=int(float(operation[:1]))) + return unit + + +def process_search(data: List[dict], limit=10, offset=0) -> List[dict]: prep_data = [] prep_dict = {} prep_dict_char_type = {} - + # --------------------------------------- prepare filters -------------------------------------------------------- # for x in data: dat = dict(x) - if x["type"] in ["Name", "Category", "Unknown"]: - prep_data.append(dat) + if x["type"] in ["Name", "Category"]: + prep_data.append( + { + "type": dat["type"], + "value": spell_check( + dat["value"], + ), + } + ) + elif x["type"] == "Unknown": + type = get_hints(dat["value"]) + prep_data.append( + { + "type": type, + "value": spell_check( + dat["value"], + ), + } + ) else: + val = spell_check( + dat["value"], + ) if x["type"] in list(prep_dict.keys()): - prep_dict[x["type"]] = prep_dict[ - x["type"] - ] | ProductCharacteristic.objects.filter( - characteristic__in=prep_dict_char_type[x["type"]], - characteristic__value__unaccent__trigram_similar=x["value"], - ) + if x["type"].startswith("*"): + unit = ProductUnitCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + ) + prep_dict[x["type"]] = prep_dict[ + x["type"] + ] | process_unit_operation(unit, x["value"]) + else: + prep_dict[x["type"]] = prep_dict[ + x["type"] + ] | ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__unaccent__trigram_similar=val, + ) else: - prep_dict_char_type[x["type"]] = Characteristic.objects.filter( - name__contains=x["type"] - ) - prep_dict[x["type"]] = ProductCharacteristic.objects.filter( - characteristic__in=prep_dict_char_type[x["type"]], - characteristic__value__unaccent__trigram_similar=x["value"], - ) + if x["type"].startswith("*"): + prep_dict_char_type[x["type"]] = UnitCharacteristic.objects.filter( + name__unaccent__trigram_similar=x["type"] + ) + unit = ProductUnitCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + ) + prep_dict[x["type"]] = process_unit_operation(unit, x["value"]) + else: + prep_dict_char_type[x["type"]] = Characteristic.objects.filter( + name__unaccent__trigram_similar=x["type"] + ) + prep_dict[x["type"]] = ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__unaccent__trigram_similar=val, + ) + # ----------------------------------- apply filters on QuerySet -------------------------------------------------- # for el, val in prep_dict.items(): prep_data.append({"type": el, "value": val}) qs = Product.objects.filter() @@ -38,7 +102,8 @@ def process_search(data: List[dict]) -> List[dict]: elif typ == "Category": qs = qs.filter(category__name__unaccent__trigram_similar=val) elif typ == "Unknown": + # add translate continue else: qs = qs.filter(characteristics__in=val) - return [x.serialize_self() for x in qs[:5]] + return [x.serialize_self() for x in qs[offset: offset + limit]] diff --git a/app/search/services/spell_check.py b/app/search/services/spell_check.py index 120c0d2..fe8f73e 100644 --- a/app/search/services/spell_check.py +++ b/app/search/services/spell_check.py @@ -2,5 +2,9 @@ import requests as r def spell_check(word: str) -> str: - res = r.get(f'https://speller.yandex.net/services/spellservice.json/checkText?text={word}') - return res.json()[0]['s'][0] + res = r.get( + f"https://speller.yandex.net/services/spellservice.json/checkText?text={word}" + ) + if not res.json(): + return word + return res.json()[0]["s"][0] diff --git a/app/search/services/translate.py b/app/search/services/translate.py index 8fa8f39..35e540e 100644 --- a/app/search/services/translate.py +++ b/app/search/services/translate.py @@ -5,9 +5,14 @@ from typing import List def translate_ru_en(word: str) -> List[str]: - res = r.get(f"https://dictionary.yandex.net/api/v1/dicservice.json/lookup?key={YANDEX_DICT_API_KEY}&lang=ru-en&text={word}") - return [i['text'] for i in chain(*[j['tr']for j in res.json()['def']])] + res = r.get( + f"https://dictionary.yandex.net/api/v1/dicservice.json/lookup?key={YANDEX_DICT_API_KEY}&lang=ru-en&text={word}" + ) + return [i["text"] for i in chain(*[j["tr"] for j in res.json()["def"]])] + def translate_en_ru(word: str) -> List[str]: - res = r.get(f"https://dictionary.yandex.net/api/v1/dicservice.json/lookup?key={YANDEX_DICT_API_KEY}&lang=en-ru&text={word}") - return [i['text'] for i in chain(*[j['tr']for j in res.json()['def']])] + res = r.get( + f"https://dictionary.yandex.net/api/v1/dicservice.json/lookup?key={YANDEX_DICT_API_KEY}&lang=en-ru&text={word}" + ) + return [i["text"] for i in chain(*[j["tr"] for j in res.json()["def"]])] From be3159dc74c6b112e24f488e739ba2b9f2d84acd Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 11:06:49 +0300 Subject: [PATCH 03/11] fixed unit characteristic lookup --- app/search/services/search.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/search/services/search.py b/app/search/services/search.py index 67ba410..879bfe3 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -16,15 +16,15 @@ from search.services.translate import translate_en_ru, translate_ru_en def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: str): if operation.startswith("<=") or operation.startswith("=<"): - return unit.filter(characteristic__numeric_value__lte=int(float(operation[:2]))) + return unit.filter(characteristic__numeric_value__lte=int(float(operation[2:]))) elif operation.startswith("=>") or operation.startswith(">="): - return unit.filter(characteristic__numeric_value__gte=int(float(operation[:2]))) + return unit.filter(characteristic__numeric_value__gte=int(float(operation[2:]))) elif operation.startswith(">"): - return unit.filter(characteristic__numeric_value__gt=int(float(operation[:1]))) + return unit.filter(characteristic__numeric_value__gt=int(float(operation[1:]))) elif operation.startswith("<"): - return unit.filter(characteristic__numeric_value__lt=int(float(operation[:1]))) + return unit.filter(characteristic__numeric_value__lt=int(float(operation[1:]))) elif operation.startswith("="): - return unit.filter(characteristic__numeric_value__gt=int(float(operation[:1]))) + return unit.filter(characteristic__numeric_value=int(float(operation[1:]))) return unit @@ -105,5 +105,8 @@ def process_search(data: List[dict], limit=10, offset=0) -> List[dict]: # add translate continue else: - qs = qs.filter(characteristics__in=val) - return [x.serialize_self() for x in qs[offset: offset + limit]] + if typ.startswith("*"): + qs = qs.filter(unit_characteristics__in=val) + else: + qs = qs.filter(characteristics__in=val) + return [x.serialize_self() for x in qs[offset : offset + limit]] From 0ff8e5674bd0c82ac99b54feb25af7835c6f6717 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 11:20:24 +0300 Subject: [PATCH 04/11] added translation check for Unknown --- app/search/services/search.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/search/services/search.py b/app/search/services/search.py index 879bfe3..507e47f 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -28,7 +28,7 @@ def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: s return unit -def process_search(data: List[dict], limit=10, offset=0) -> List[dict]: +def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: prep_data = [] prep_dict = {} prep_dict_char_type = {} @@ -90,9 +90,9 @@ def process_search(data: List[dict], limit=10, offset=0) -> List[dict]: characteristic__in=prep_dict_char_type[x["type"]], characteristic__value__unaccent__trigram_similar=val, ) - # ----------------------------------- apply filters on QuerySet -------------------------------------------------- # for el, val in prep_dict.items(): prep_data.append({"type": el, "value": val}) + # ----------------------------------- apply filters on QuerySet -------------------------------------------------- # qs = Product.objects.filter() for x in prep_data: typ = x["type"] @@ -102,7 +102,21 @@ def process_search(data: List[dict], limit=10, offset=0) -> List[dict]: elif typ == "Category": qs = qs.filter(category__name__unaccent__trigram_similar=val) elif typ == "Unknown": - # add translate + if val[0] in string.printable: + val = "".join(translate_en_ru(val)) + else: + val = "".join(translate_ru_en(val)) + type = get_hints(val) + if type == "Name": + qs = qs.filter(name__unaccent__trigram_similar=val) + elif type == "Category": + qs = qs.filter(category__name__unaccent__trigram_similar=val) + elif type == "Unknown": + continue + else: + qs = qs.filter( + characteristics__characteristic__name__unaccent__trigram_similar=val + ) continue else: if typ.startswith("*"): From e249aef80915bcfec19a5813a3547da05a83bfe1 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 11:43:30 +0300 Subject: [PATCH 05/11] added offset:limit for search --- app/search/api/serializers.py | 31 ++++++++++++++++++++++++++++++- app/search/api/views.py | 7 ++++++- app/search/services/hints.py | 2 +- app/search/services/search.py | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/search/api/serializers.py b/app/search/api/serializers.py index 227e69f..ce144f8 100644 --- a/app/search/api/serializers.py +++ b/app/search/api/serializers.py @@ -15,6 +15,8 @@ class QueryFilterSerializer(serializers.Serializer): class SearchSerializer(serializers.Serializer): body = serializers.ListSerializer(child=QueryFilterSerializer()) + limit = serializers.IntegerField(default=5, min_value=1) + offset = serializers.IntegerField(default=0, min_value=0) def create(self, validated_data): raise NotImplementedError @@ -37,22 +39,49 @@ class HintRequestSerializer(serializers.Serializer): content = serializers.CharField() def create(self, validated_data): - raise NotImplemented + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError class HintResponseSerializer(serializers.Serializer): type = serializers.CharField() content = serializers.CharField() + def create(self, validated_data): + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError + class AutoCompleteRequestSerializer(serializers.Serializer): content = serializers.CharField(validators=[MinLengthValidator(3)]) + def create(self, validated_data): + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError + class AutoCompleteSerializerNode(serializers.Serializer): coordinate = serializers.IntegerField(validators=[MinValueValidator(0)]) value = HintResponseSerializer() + def create(self, validated_data): + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError + class AutoCompleteResponseSerializer(serializers.Serializer): nodes = serializers.ListField(child=AutoCompleteSerializerNode()) + + def create(self, validated_data): + raise NotImplementedError + + def update(self, instance, validated_data): + raise NotImplementedError diff --git a/app/search/api/views.py b/app/search/api/views.py index b81d958..af6b407 100644 --- a/app/search/api/views.py +++ b/app/search/api/views.py @@ -30,7 +30,12 @@ class SearchApi(APIView): serializer = SearchSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( - process_search(serializer.data["body"]), status=status.HTTP_200_OK + process_search( + serializer.data["body"], + serializer.data["limit"], + serializer.data["offset"], + ), + status=status.HTTP_200_OK, ) diff --git a/app/search/services/hints.py b/app/search/services/hints.py index b4579de..a6c7765 100644 --- a/app/search/services/hints.py +++ b/app/search/services/hints.py @@ -8,5 +8,5 @@ def get_hints(content: str) -> str: elif content in list(map(lambda category: category.name, Category.objects.all())): category = "Category" elif content in list(map(lambda char: char.value, Characteristic.objects.all())): - category = Characteristic.objects.get(value=content).name + category = Characteristic.objects.filter(value=content).first().name return category diff --git a/app/search/services/search.py b/app/search/services/search.py index 507e47f..d9088e2 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -123,4 +123,4 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: qs = qs.filter(unit_characteristics__in=val) else: qs = qs.filter(characteristics__in=val) - return [x.serialize_self() for x in qs[offset : offset + limit]] + return [x.serialize_self() for x in qs[offset:offset+limit]] From 23e30bbcb828b225bdcf39fdced08cb66614336c Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 12:29:51 +0300 Subject: [PATCH 06/11] improved autocomplete --- app/search/services/autocomplete_schema.py | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/app/search/services/autocomplete_schema.py b/app/search/services/autocomplete_schema.py index feb6392..c18deee 100644 --- a/app/search/services/autocomplete_schema.py +++ b/app/search/services/autocomplete_schema.py @@ -2,37 +2,38 @@ from search.models import Product, Category, Characteristic def autocomplete_schema(val: str): - schema = [] + schema = [ + { + "coordinate": product["name"].lower().index(val.lower()), + "value": { + "type": "Name", + "value": product["name"], + }, + } + for product in Product.objects.filter(name__unaccent__icontains=val).values( + "name" + ) + ] schema.extend( [ { - "coordinate": product["name"].index(val), - "value": { - "type": "Name", - "value": product["name"], - }, - } - for product in Product.objects.filter(name__contains=val).values("name") - ] - ) - schema.extend( - [ - { - "coordinate": cat["name"].index(val), + "coordinate": cat["name"].lower().index(val.lower()), "value": {"type": "Category", "value": cat["name"]}, } - for cat in Category.objects.filter(name__contains=val).values("name") - ] - ) - schema.extend( - [ - { - "coordinate": char["name"].index(val), - "value": {"type": char["name"], "value": char["value"]}, - } - for char in Characteristic.objects.filter(name__contains=val).values( - "name", "value" + for cat in Category.objects.filter(name__unaccent__icontains=val).values( + "name" ) ] ) + schema.extend( + [ + { + "coordinate": char["value"].lower().index(val.lower()), + "value": {"type": char["name"], "value": char["value"]}, + } + for char in Characteristic.objects.filter( + value__unaccent__icontains=val + ).values("name", "value") + ] + ) return schema From b301b8103cac82c1773dc67b7b265a92d18666e1 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 12:52:23 +0300 Subject: [PATCH 07/11] improved word search --- app/search/services/search.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/search/services/search.py b/app/search/services/search.py index d9088e2..83c37d7 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -28,6 +28,15 @@ def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: s return unit +def apply_qs_search(qs: Product.objects, text: str): + for st in [".", ",", "!", "?"]: + text = text.replace(st, " ") + text = text.split() + for word in text: + qs = qs.filter(name__unaccent__trigram_similar=word) | qs.filter(name__unaccent__icontains=word) + return qs + + def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: prep_data = [] prep_dict = {} @@ -98,7 +107,7 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: typ = x["type"] val = x["value"] if typ == "Name": - qs = qs.filter(name__unaccent__trigram_similar=val) + qs = apply_qs_search(qs, val) elif typ == "Category": qs = qs.filter(category__name__unaccent__trigram_similar=val) elif typ == "Unknown": @@ -108,7 +117,7 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: val = "".join(translate_ru_en(val)) type = get_hints(val) if type == "Name": - qs = qs.filter(name__unaccent__trigram_similar=val) + qs = apply_qs_search(qs, val) elif type == "Category": qs = qs.filter(category__name__unaccent__trigram_similar=val) elif type == "Unknown": @@ -123,4 +132,4 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: qs = qs.filter(unit_characteristics__in=val) else: qs = qs.filter(characteristics__in=val) - return [x.serialize_self() for x in qs[offset:offset+limit]] + return [x.serialize_self() for x in qs[offset : offset + limit]] From c68dfce10a396683c7308ff1f4af1b1977f2414d Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 13:11:30 +0300 Subject: [PATCH 08/11] added exclude autocomplete --- app/search/api/serializers.py | 1 + app/search/api/views.py | 2 +- app/search/services/autocomplete_schema.py | 56 +++++++++++++--------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app/search/api/serializers.py b/app/search/api/serializers.py index ce144f8..acc6ac5 100644 --- a/app/search/api/serializers.py +++ b/app/search/api/serializers.py @@ -58,6 +58,7 @@ class HintResponseSerializer(serializers.Serializer): class AutoCompleteRequestSerializer(serializers.Serializer): content = serializers.CharField(validators=[MinLengthValidator(3)]) + exclude = serializers.ListSerializer(child=QueryFilterSerializer()) def create(self, validated_data): raise NotImplementedError diff --git a/app/search/api/views.py b/app/search/api/views.py index af6b407..0b4d574 100644 --- a/app/search/api/views.py +++ b/app/search/api/views.py @@ -64,6 +64,6 @@ class AutoCompleteApi(APIView): serializer = AutoCompleteRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( - {"nodes": autocomplete_schema(serializer.data["content"])}, + {"nodes": autocomplete_schema(serializer.data["content"], serializer.data["exclude"])}, status=status.HTTP_200_OK, ) diff --git a/app/search/services/autocomplete_schema.py b/app/search/services/autocomplete_schema.py index c18deee..d0568ee 100644 --- a/app/search/services/autocomplete_schema.py +++ b/app/search/services/autocomplete_schema.py @@ -1,30 +1,40 @@ +from typing import List, Dict + from search.models import Product, Category, Characteristic -def autocomplete_schema(val: str): - schema = [ - { - "coordinate": product["name"].lower().index(val.lower()), - "value": { - "type": "Name", - "value": product["name"], - }, - } - for product in Product.objects.filter(name__unaccent__icontains=val).values( - "name" +def autocomplete_schema(val: str, exclude: List[Dict]): + exclude = [dict(x) for x in exclude] + name_exclude = [x["value"] for x in exclude if x["type"] == "Name"] + category_exclude = [x["value"] for x in exclude if x["type"] == "Category"] + schema = [] + if not name_exclude: + schema.extend( + [ + { + "coordinate": product["name"].lower().index(val.lower()), + "value": { + "type": "Name", + "value": product["name"], + }, + } + for product in Product.objects.filter( + name__unaccent__icontains=val + ).values("name") + ] + ) + if not category_exclude: + schema.extend( + [ + { + "coordinate": cat["name"].lower().index(val.lower()), + "value": {"type": "Category", "value": cat["name"]}, + } + for cat in Category.objects.filter( + name__unaccent__icontains=val + ).values("name") + ] ) - ] - schema.extend( - [ - { - "coordinate": cat["name"].lower().index(val.lower()), - "value": {"type": "Category", "value": cat["name"]}, - } - for cat in Category.objects.filter(name__unaccent__icontains=val).values( - "name" - ) - ] - ) schema.extend( [ { From faaccff7dca5e5c8f6a9bd642288a0d5b7c9e8b4 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 16:14:43 +0300 Subject: [PATCH 09/11] added score system, range unit characteristics, minor improvements --- app/conf/api.py | 12 +++++++--- app/search/api/serializers.py | 2 +- app/search/api/views.py | 26 +++++++++++++++++++++- app/search/models.py | 18 +++++++++++---- app/search/services/autocomplete_schema.py | 18 +++++++-------- app/search/services/load_products.py | 21 ++++++++++++----- app/search/services/search.py | 25 ++++++++++++++++----- 7 files changed, 93 insertions(+), 29 deletions(-) diff --git a/app/conf/api.py b/app/conf/api.py index ad55947..472a8f8 100644 --- a/app/conf/api.py +++ b/app/conf/api.py @@ -1,9 +1,15 @@ from django.urls import path -from search.api.views import SearchApi, HintApi, AutoCompleteApi +from search.api.views import ( + SearchApi, + HintApi, + AutoCompleteApi, + IncreaseProductScoreApi, +) urlpatterns = [ path("search", SearchApi.as_view(), name="search_api"), - path("hint", HintApi.as_view(), name="hint api"), - path('autocomplete_schema', AutoCompleteApi.as_view(), name='autocomplete api') + path("hint", HintApi.as_view(), name="hint_api"), + path("autocomplete_schema", AutoCompleteApi.as_view(), name="autocomplete_api"), + path("score/", IncreaseProductScoreApi.as_view(), name="score_api"), ] diff --git a/app/search/api/serializers.py b/app/search/api/serializers.py index acc6ac5..e408e3a 100644 --- a/app/search/api/serializers.py +++ b/app/search/api/serializers.py @@ -58,7 +58,7 @@ class HintResponseSerializer(serializers.Serializer): class AutoCompleteRequestSerializer(serializers.Serializer): content = serializers.CharField(validators=[MinLengthValidator(3)]) - exclude = serializers.ListSerializer(child=QueryFilterSerializer()) + exclude = serializers.ListSerializer(child=QueryFilterSerializer(), default=[]) def create(self, validated_data): raise NotImplementedError diff --git a/app/search/api/views.py b/app/search/api/views.py index 0b4d574..e450be0 100644 --- a/app/search/api/views.py +++ b/app/search/api/views.py @@ -1,6 +1,7 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status +from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from rest_framework.views import APIView from search.api.serializers import HintRequestSerializer @@ -12,6 +13,7 @@ from search.api.serializers import ( AutoCompleteRequestSerializer, AutoCompleteResponseSerializer, ) +from search.models import Product from search.services.search import process_search from search.services.autocomplete_schema import autocomplete_schema @@ -64,6 +66,28 @@ class AutoCompleteApi(APIView): serializer = AutoCompleteRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response( - {"nodes": autocomplete_schema(serializer.data["content"], serializer.data["exclude"])}, + { + "nodes": autocomplete_schema( + serializer.data["content"], serializer.data["exclude"] + ) + }, status=status.HTTP_200_OK, ) + + +class IncreaseProductScoreApi(APIView): + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + "id", + openapi.IN_PATH, + description="Product id", + type=openapi.TYPE_INTEGER, + ) + ] + ) + def post(self, request, pk): + product = get_object_or_404(Product, id=pk) + product.score += 1 + product.save(update_fields=["score"]) + return Response({"score": product.score}, status=status.HTTP_200_OK) diff --git a/app/search/models.py b/app/search/models.py index 55cf53f..329293c 100644 --- a/app/search/models.py +++ b/app/search/models.py @@ -18,14 +18,22 @@ class Characteristic(models.Model): class UnitCharacteristic(models.Model): name = models.TextField("Имя", blank=False) value = models.TextField("Значение", blank=False) - numeric_value = models.IntegerField(default=0) + numeric_value_min = models.IntegerField(default=0) + numeric_value_max = models.IntegerField(default=0) unit = models.TextField("Размерность", blank=False) def __str__(self): return str(self.name) def serialize_self(self): - return {"name": self.name, "value": self.numeric_value, "unit": self.unit} + return { + "id": self.id, + "name": self.name, + "value": self.numeric_value_min + if self.numeric_value_min == self.numeric_value_max + else f"{self.numeric_value_min}:{self.numeric_value_max}", + "unit": self.unit, + } class Meta: db_table = "unit_characteristic" @@ -50,14 +58,16 @@ class Product(models.Model): Category, related_name="products", on_delete=models.CASCADE ) - # score = models.IntegerField(default=0) + score = models.IntegerField(default=0) def __str__(self): return str(self.name) def serialize_self(self) -> dict: return { + "id": self.id, "name": self.name, + "score": self.score, "characteristic": [ x.characteristic.serialize_self() for x in self.characteristics.all() ] @@ -69,7 +79,7 @@ class Product(models.Model): class Meta: db_table = "product" - # ordering = ["score"] + ordering = ["-score"] class ProductCharacteristic(models.Model): diff --git a/app/search/services/autocomplete_schema.py b/app/search/services/autocomplete_schema.py index d0568ee..f7f003e 100644 --- a/app/search/services/autocomplete_schema.py +++ b/app/search/services/autocomplete_schema.py @@ -18,9 +18,9 @@ def autocomplete_schema(val: str, exclude: List[Dict]): "value": product["name"], }, } - for product in Product.objects.filter( - name__unaccent__icontains=val - ).values("name") + for product in Product.objects.filter(name__unaccent__icontains=val)[ + :20 + ].values("name") ] ) if not category_exclude: @@ -30,9 +30,9 @@ def autocomplete_schema(val: str, exclude: List[Dict]): "coordinate": cat["name"].lower().index(val.lower()), "value": {"type": "Category", "value": cat["name"]}, } - for cat in Category.objects.filter( - name__unaccent__icontains=val - ).values("name") + for cat in Category.objects.filter(name__unaccent__icontains=val)[ + :20 + ].values("name") ] ) schema.extend( @@ -41,9 +41,9 @@ def autocomplete_schema(val: str, exclude: List[Dict]): "coordinate": char["value"].lower().index(val.lower()), "value": {"type": char["name"], "value": char["value"]}, } - for char in Characteristic.objects.filter( - value__unaccent__icontains=val - ).values("name", "value") + for char in Characteristic.objects.filter(value__unaccent__icontains=val)[ + :20 + ].values("name", "value") ] ) return schema diff --git a/app/search/services/load_products.py b/app/search/services/load_products.py index af0c235..192c4d4 100644 --- a/app/search/services/load_products.py +++ b/app/search/services/load_products.py @@ -64,12 +64,23 @@ def load_excel(): def process_unit_character(): for el in UnitCharacteristic.objects.all(): - nums = re.findall("[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d+)?", el.value) - if len(nums) != 1: - el.delete() - else: + nums = re.findall( + "[-+]?[.]?[\d]+(?:,\d\d\d)*[\.]?\d*(?:[eE][-+]?\d+)?", el.value + ) + if len(nums) == 1: try: - el.numeric_value = int(float(nums[0].replace(",", "."))) + el.numeric_value_min = int(float(nums[0].replace(",", "."))) + el.numeric_value_max = int(float(nums[0].replace(",", "."))) + el.save() + except ValueError: + el.delete() + elif len(nums): + try: + nums = [int(float(x.replace(",", "."))) for x in nums] + min_num = min(nums) + max_num = max(nums) + el.numeric_value_min = min_num + el.numeric_value_max = max_num el.save() except ValueError: el.delete() diff --git a/app/search/services/search.py b/app/search/services/search.py index 83c37d7..6d221aa 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -16,15 +16,26 @@ from search.services.translate import translate_en_ru, translate_ru_en def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: str): if operation.startswith("<=") or operation.startswith("=<"): - return unit.filter(characteristic__numeric_value__lte=int(float(operation[2:]))) + return unit.filter( + characteristic__numeric_value_max__lte=int(float(operation[2:])) + ) elif operation.startswith("=>") or operation.startswith(">="): - return unit.filter(characteristic__numeric_value__gte=int(float(operation[2:]))) + return unit.filter( + characteristic__numeric_value_min__gte=int(float(operation[2:])) + ) elif operation.startswith(">"): - return unit.filter(characteristic__numeric_value__gt=int(float(operation[1:]))) + return unit.filter( + characteristic__numeric_value_min__gt=int(float(operation[1:])) + ) elif operation.startswith("<"): - return unit.filter(characteristic__numeric_value__lt=int(float(operation[1:]))) + return unit.filter( + characteristic__numeric_value_max__lt=int(float(operation[1:])) + ) elif operation.startswith("="): - return unit.filter(characteristic__numeric_value=int(float(operation[1:]))) + return unit.filter( + characteristic__numeric_value_min__gte=int(float(operation[1:])), + characteristic__numeric_value_max__lte=int(float(operation[1:])), + ) return unit @@ -33,7 +44,9 @@ def apply_qs_search(qs: Product.objects, text: str): text = text.replace(st, " ") text = text.split() for word in text: - qs = qs.filter(name__unaccent__trigram_similar=word) | qs.filter(name__unaccent__icontains=word) + qs = qs.filter(name__unaccent__trigram_similar=word) | qs.filter( + name__unaccent__icontains=word + ) return qs From b683d2e444a29f99212645d4254589d7412ca860 Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 18:20:58 +0300 Subject: [PATCH 10/11] fixed global lookup --- app/search/models.py | 1 - app/search/services/search.py | 84 +++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/app/search/models.py b/app/search/models.py index 329293c..456b0b8 100644 --- a/app/search/models.py +++ b/app/search/models.py @@ -27,7 +27,6 @@ class UnitCharacteristic(models.Model): def serialize_self(self): return { - "id": self.id, "name": self.name, "value": self.numeric_value_min if self.numeric_value_min == self.numeric_value_max diff --git a/app/search/services/search.py b/app/search/services/search.py index 6d221aa..2b11939 100644 --- a/app/search/services/search.py +++ b/app/search/services/search.py @@ -1,11 +1,14 @@ import string +from django.db.models import QuerySet + from search.models import ( Product, Characteristic, ProductCharacteristic, ProductUnitCharacteristic, UnitCharacteristic, + Category, ) from typing import List @@ -39,17 +42,51 @@ def process_unit_operation(unit: ProductUnitCharacteristic.objects, operation: s return unit -def apply_qs_search(qs: Product.objects, text: str): +def _clean_text(text: str) -> List[str]: for st in [".", ",", "!", "?"]: text = text.replace(st, " ") text = text.split() + return text + + +def apply_qs_search(qs: Product.objects, text: str): + text = _clean_text(text) + words = Product.objects.none() for word in text: - qs = qs.filter(name__unaccent__trigram_similar=word) | qs.filter( - name__unaccent__icontains=word + words = ( + words + | Product.objects.filter(name__unaccent__trigram_similar=word) + | Product.objects.filter(name__unaccent__icontains=word) ) + print(words) + qs = qs | words + print(qs) return qs +def apply_all_qs_search(orig_qs, text: str): + # words + qs = apply_qs_search(Product.objects.none(), text) + text = _clean_text(text) + + # categories + cats = Category.objects.none() + for word in text: + cats = cats | cats.filter(name__icontains=word) + qs = qs | Product.objects.filter(category__in=cats) + + # characteristics + chars = Characteristic.objects.none() + for word in text: + chars = chars | chars.filter( + value__icontains=word, + ) + qs = qs | Product.objects.filter(characteristics__characteristic__in=chars) + # print(qs) + + return qs & orig_qs + + def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: prep_data = [] prep_dict = {} @@ -57,7 +94,7 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: # --------------------------------------- prepare filters -------------------------------------------------------- # for x in data: dat = dict(x) - if x["type"] in ["Name", "Category"]: + if x["type"] in ["Name", "Category", "Characteristic", "All"]: prep_data.append( { "type": dat["type"], @@ -89,17 +126,22 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: x["type"] ] | process_unit_operation(unit, x["value"]) else: - prep_dict[x["type"]] = prep_dict[ - x["type"] - ] | ProductCharacteristic.objects.filter( - characteristic__in=prep_dict_char_type[x["type"]], - characteristic__value__unaccent__trigram_similar=val, + prep_dict[x["type"]] = ( + prep_dict[x["type"]] + | ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__unaccent__trigram_similar=val, + ) + | ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__icontains=val, + ) ) else: if x["type"].startswith("*"): prep_dict_char_type[x["type"]] = UnitCharacteristic.objects.filter( name__unaccent__trigram_similar=x["type"] - ) + ) | UnitCharacteristic.objects.filter(name__icontains=x["type"]) unit = ProductUnitCharacteristic.objects.filter( characteristic__in=prep_dict_char_type[x["type"]], ) @@ -107,10 +149,13 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: else: prep_dict_char_type[x["type"]] = Characteristic.objects.filter( name__unaccent__trigram_similar=x["type"] - ) + ) | Characteristic.objects.filter(name__icontains=x["type"]) prep_dict[x["type"]] = ProductCharacteristic.objects.filter( characteristic__in=prep_dict_char_type[x["type"]], characteristic__value__unaccent__trigram_similar=val, + ) | ProductCharacteristic.objects.filter( + characteristic__in=prep_dict_char_type[x["type"]], + characteristic__value__icontains=val, ) for el, val in prep_dict.items(): prep_data.append({"type": el, "value": val}) @@ -121,8 +166,18 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: val = x["value"] if typ == "Name": qs = apply_qs_search(qs, val) + elif typ == "All": + qs = apply_all_qs_search(qs, val) elif typ == "Category": - qs = qs.filter(category__name__unaccent__trigram_similar=val) + qs = qs.filter(category__name__unaccent__trigram_similar=val) | qs.filter( + category__name__icontains=val + ) + elif typ == "Characteristic": + char = ProductCharacteristic.objects.filter(product__in=qs) + char = char.filter(characteristic__value__icontains=val) | char.filter( + characteristic__value__unaccent__trigram_similar=val + ) + qs = qs.filter(characteristics__in=char) elif typ == "Unknown": if val[0] in string.printable: val = "".join(translate_en_ru(val)) @@ -145,4 +200,7 @@ def process_search(data: List[dict], limit=5, offset=0) -> List[dict]: qs = qs.filter(unit_characteristics__in=val) else: qs = qs.filter(characteristics__in=val) - return [x.serialize_self() for x in qs[offset : offset + limit]] + return [ + x.serialize_self() + for x in qs.distinct().order_by("-score")[offset : offset + limit] + ] From dff46d371757f7ef60009ee8db60dd8c43ac492e Mon Sep 17 00:00:00 2001 From: Alexandr Karpov Date: Sat, 22 Oct 2022 18:21:42 +0300 Subject: [PATCH 11/11] fixed hints --- app/search/services/hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/search/services/hints.py b/app/search/services/hints.py index a6c7765..7763a0b 100644 --- a/app/search/services/hints.py +++ b/app/search/services/hints.py @@ -2,7 +2,7 @@ from search.models import Product, Category, Characteristic def get_hints(content: str) -> str: - category = "Unknown" + category = "All" if content in list(map(lambda product: product.name, Product.objects.all())): category = "Name" elif content in list(map(lambda category: category.name, Category.objects.all())):