Files
recipes/cookbook/helper/recipe_search.py

139 lines
5.6 KiB
Python

from datetime import datetime, timedelta
from recipes import settings
from django.contrib.postgres.aggregates import StringAgg
from django.contrib.postgres.search import (
SearchQuery, SearchRank, SearchVector,
)
from django.db.models import Q, Case, When, Value, Count, Sum
from django.utils import translation
from cookbook.models import ViewLog
DICTIONARY = {
# TODO find custom dictionaries - maybe from here https://www.postgresql.org/message-id/CAF4Au4x6X_wSXFwsQYE8q5o0aQZANrvYjZJ8uOnsiHDnOVPPEg%40mail.gmail.com
# 'hy': 'Armenian',
# 'ca': 'Catalan',
# 'cs': 'Czech',
'nl': 'dutch',
'en': 'english',
'fr': 'french',
'de': 'german',
'it': 'italian',
# 'lv': 'Latvian',
'es': 'spanish',
}
def search_recipes(request, queryset, params):
search_string = params.get('query', '')
search_keywords = params.getlist('keywords', [])
search_foods = params.getlist('foods', [])
search_books = params.getlist('books', [])
search_keywords_or = params.get('keywords_or', True)
search_foods_or = params.get('foods_or', True)
search_books_or = params.get('books_or', True)
search_internal = params.get('internal', None)
search_random = params.get('random', False)
search_last_viewed = int(params.get('last_viewed', 0))
if search_last_viewed > 0:
last_viewed_recipes = ViewLog.objects.filter(
created_by=request.user, space=request.space,
created_at__gte=datetime.now() - timedelta(days=14)).values_list(
'recipe__pk', flat=True).distinct()
return queryset.filter(pk__in=list(set(last_viewed_recipes))[-search_last_viewed:])
queryset = queryset.annotate(
new_recipe=Case(When(
created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
default=Value(0), )).order_by('-new_recipe', 'name')
rank_results = False
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql'] and search_string != '':
rank_results = True
# queryset = queryset.annotate(similarity=TrigramSimilarity('name', search_string), )
# .filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=search_string)).order_by('-similarity')
language = DICTIONARY.get(translation.get_language(), 'simple')
search_query = SearchQuery(
search_string,
search_type="websearch",
config=language,
)
# TODO create user options to add/remove query elements from search so that they can fine tune their own experience
# trigrams, icontains, unaccent and startswith all impact results and performance significantly
search_vectors = (
# SearchVector('search_vector') <-- this can be searched like a field
SearchVector(StringAgg('steps__ingredients__food__name__unaccent', delimiter=' '), weight='B')
+ SearchVector(StringAgg('keywords__name__unaccent', delimiter=' '), weight='B')
)
# trigrams don't seem to add anything and severely limit accuracy of results.
# TODO add trigrams as an on/off feature
# trigram = (
# TrigramSimilarity('name__unaccent', search_string)
# + TrigramSimilarity('description__unaccent', search_string)
# # adding trigrams to ingredients and keywords causes duplicate results that can't be made unique
# + TrigramSimilarity('steps__ingredients__food__name__unaccent', search_string)
# + TrigramSimilarity('keywords__name__unaccent', search_string)
# )
search_rank = (
SearchRank('name_search_vector', search_query)
+ SearchRank('desc_search_vector', search_query)
+ SearchRank('steps__search_vector', search_query)
+ SearchRank(search_vectors, search_query)
)
queryset = (
queryset.annotate(
vector=search_vectors,
rank=search_rank
)
.filter(
# vector=search_query
Q(name_search_vector=search_query)
| Q(desc_search_vector=search_query)
| Q(steps__search_vector=search_query)
| Q(vector=search_query)
# adding trigrams to ingredients causes duplicate results that can't be made unique
# | Q(trigram__gt=0.2)
| Q(name__istartswith=search_string)
)
)
else:
queryset = queryset.filter(name__icontains=search_string)
if len(search_keywords) > 0:
if search_keywords_or == 'true':
queryset = queryset.filter(keywords__id__in=search_keywords)
else:
for k in search_keywords:
queryset = queryset.filter(keywords__id=k)
if len(search_foods) > 0:
if search_foods_or == 'true':
queryset = queryset.filter(steps__ingredients__food__id__in=search_foods)
else:
for k in search_foods:
queryset = queryset.filter(steps__ingredients__food__id=k)
if len(search_books) > 0:
if search_books_or == 'true':
queryset = queryset.filter(recipebookentry__book__id__in=search_books)
else:
for k in search_books:
queryset = queryset.filter(recipebookentry__book__id=k)
if search_internal == 'true':
queryset = queryset.filter(internal=True)
queryset = queryset.distinct()
if search_random == 'true':
queryset = queryset.order_by("?")
elif rank_results:
queryset = queryset.order_by('-rank')
return queryset