import logging from django.db.models import Q from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity from products.models import Product def extract_search_filters(result_set): """ Returned object should look something like: { "tags": [], "attributes": [], } """ filter_dict = { "tags": { 'singles': set(), }, "attributes": { 'singles': set(), } } for item in result_set: try: # extract tags tags = item.tags.all() for tag in tags: if len(tag.name.split('/')) == 1: filter_dict['tags']['singles'].add(tag.name) else: # set penultimate tag as header chunks = tag.name.split('/') header = chunks[-2] name = chunks[-1] # check if entry = filter_dict['tags'].get(header) if entry is None: filter_dict['tags'][header] = set() filter_dict['tags'][header].add(name) # extract attributes attributes = item.attributes.all() for tag in attributes: if len(tag.name.split('/')) == 1: filter_dict['attributes']['singles'].add(tag.name) else: # set penultimate tag as header chunks = tag.name.split('/') header = chunks[-2] name = chunks[-1] # check if entry = filter_dict['attributes'].get(header) if entry is None: filter_dict['attributes'][header] = set() filter_dict['attributes'][header].add(name) except Exception as e: logging.error(f'Extacting filters for {item}') return filter_dict def find_related_products_v1(keyword): """ Classical approach to the search Using Q objects """ # search in tags tags = Product.tags.tag_model.objects.filter(name__icontains=keyword) # search in category categories = Product.category.tag_model.objects.filter(name__icontains=keyword) # search in attributes attributes = Product.attributes.tag_model.objects.filter(name__icontains=keyword) # unified tag search products_qs = Product.objects.filter( Q(name__icontains=keyword)| Q(description__icontains=keyword)| Q(tags__in=tags)| Q(category__in=categories)| Q(attributes__in=attributes) ) return products_qs def find_related_products_v5(keyword): """ Single query solution, using Q objects """ products_qs = Product.objects.filter( Q(name__icontains=keyword)| Q(description__icontains=keyword)| Q(tags__label__icontains=keyword)| Q(category__name__icontains=keyword)| Q(attributes__label__icontains=keyword) ) return set(products_qs) def find_related_products_v2(keyword): """ More advanced: using search vectors """ fields=('name', 'description', 'tags__label', 'attributes__label', 'category__name') vector = SearchVector(*fields) products_qs = Product.objects.annotate( search=vector ).filter(search=keyword) return set(products_qs) def find_related_products_v3(keyword): """ Ranked product search SearchVectors for the fields SearchQuery for the value SearchRank for relevancy scoring and ranking """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__name') query = SearchQuery(keyword) products_qs = Product.objects.annotate( rank=SearchRank(vector, query) ).filter(rank__gt=0.05) # removed order_by because its lost in casting return set(products_qs) def find_related_products_v6(keyword, shipping_cost=None, discount=None, category=None, tags=None, price_min=None,price_max=None): """ Ranked product search SearchVectors for the fields SearchQuery for the value SearchRank for relevancy scoring and ranking allow filtering by: - shipping cost """ vector = SearchVector('name') + SearchVector('description') + SearchVector('tags__label') + SearchVector('attributes__label') + SearchVector('category__name') query = SearchQuery(keyword) products_qs = Product.objects.annotate( rank=SearchRank(vector, query) ).filter(rank__gt=0.05) # removed order_by because its lost in casting # filter by category if category is not None: products_qs = products_qs.filter(category=category) # filter by tags if tags is not None: products_qs = products_qs.filter(tags=tags) # filter by shipping cost if shipping_cost is True: # only instances with shipping costs products_qs = products_qs.filter( Q(shipping_cost__isnull=False)& Q(shipping_cost__gte=1) ) elif shipping_cost is False: # only intances without shpping costs products_qs = products_qs.filter(Q(shipping_cost=None)|Q(shipping_cost=0.00)) # filter by discount if discount is True: # only instances with shipping costs products_qs = products_qs.filter( Q(discount__isnull=False)& Q(discount__gte=1) ) elif discount is False: # only intances without shpping costs products_qs = products_qs.filter(Q(discount=None)|Q(discount=0.00)) # filter by price if price_min is not None: products_qs = products_qs.filter(price__gt=price_min) if price_max is not None: products_qs = products_qs.filter(price__lt=price_max) return set(products_qs) def find_related_products_v4(keyword): """ Similarity-ranked search using trigrams Not working """ # fields=('name', 'description', 'tags__label', 'attributes__label', 'category__name') products_qs = Product.objects.annotate( similarity=TrigramSimilarity('name', keyword), ).order_by('-similarity') return set(products_qs)