TandoorRecipes/cookbook/helper/recipe_search.py

154 lines
6.6 KiB
Python

from datetime import datetime, timedelta
from recipes import settings
from django.contrib.postgres.search import (
SearchQuery, SearchRank, TrigramSimilarity
)
from django.db.models import Q, Subquery, Case, When, Value
from django.utils import translation
from cookbook.managers import DICTIONARY
from cookbook.models import Food, Keyword, ViewLog
def search_recipes(request, queryset, params):
fields = {
'name': 'name',
'description': 'description',
'instructions': 'steps__instruction',
'foods': 'steps__ingredients__food__name',
'keywords': 'keywords__name'
}
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_new = params.get('new', 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')
search_type = None
search_sort = None
if len(search_string) > 0:
# TODO move all of these to settings somewhere - probably user settings
unaccent_include = ['name', 'description', 'instructions', 'keywords', 'foods'] # can also contain: description, instructions, keywords, foods
# TODO when setting up settings length of arrays below must be >=1
icontains_include = [] # can contain: name, description, instructions, keywords, foods
istartswith_include = ['name'] # can also contain: description, instructions, keywords, foods
trigram_include = ['name', 'description', 'instructions'] # only these choices - keywords and foods are really, really, really slow maybe add to subquery?
fulltext_include = ['name', 'description', 'instructions', 'foods', 'keywords']
# END OF SETTINGS SECTION
for f in unaccent_include:
fields[f] += '__unaccent'
filters = []
for f in icontains_include:
filters += [Q(**{"%s__icontains" % fields[f]: search_string})]
for f in istartswith_include:
filters += [Q(**{"%s__istartswith" % fields[f]: search_string})]
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
language = DICTIONARY.get(translation.get_language(), 'simple')
# django full text search https://docs.djangoproject.com/en/3.2/ref/contrib/postgres/search/#searchquery
search_type = 'websearch' # other postgress options are phrase or plain or raw (websearch and trigrams are mutually exclusive)
search_trigram = False
search_query = SearchQuery(
search_string,
search_type=search_type,
config=language,
)
# iterate through fields to use in trigrams generating a single trigram
if search_trigram & len(trigram_include) > 1:
trigram = None
for f in trigram_include:
if trigram:
trigram += TrigramSimilarity(fields[f], search_string)
else:
trigram = TrigramSimilarity(fields[f], search_string)
queryset.annotate(simularity=trigram)
filters += [Q(simularity__gt=0.5)]
if 'name' in fulltext_include:
filters += [Q(name_search_vector=search_query)]
if 'description' in fulltext_include:
filters += [Q(desc_search_vector=search_query)]
if 'instructions' in fulltext_include:
filters += [Q(steps__search_vector=search_query)]
if 'keywords' in fulltext_include:
filters += [Q(keywords__in=Subquery(Keyword.objects.filter(name__search=search_query).values_list('id', flat=True)))]
if 'foods' in fulltext_include:
filters += [Q(steps__ingredients__food__in=Subquery(Food.objects.filter(name__search=search_query).values_list('id', flat=True)))]
query_filter = None
for f in filters:
if query_filter:
query_filter |= f
else:
query_filter = f
# TODO this is kind of a dumb method to sort. create settings to choose rank vs most often made, date created or rating
search_rank = (
SearchRank('name_search_vector', search_query, cover_density=True)
+ SearchRank('desc_search_vector', search_query, cover_density=True)
+ SearchRank('steps__search_vector', search_query, cover_density=True)
)
queryset = queryset.filter(query_filter).annotate(rank=search_rank)
else:
queryset = queryset.filter(query_filter)
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 search_sort == 'rank':
queryset = queryset.order_by('-rank')
return queryset