exact match appears first on keyword/food/unit searches
This commit is contained in:
@ -4,13 +4,15 @@ from recipes import settings
|
||||
from django.contrib.postgres.search import (
|
||||
SearchQuery, SearchRank, TrigramSimilarity
|
||||
)
|
||||
from django.db.models import Count, Q, Subquery, Case, When, Value
|
||||
from django.db.models import Count, Max, Q, Subquery, Case, When, Value
|
||||
from django.utils import translation
|
||||
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import Food, Keyword, ViewLog
|
||||
|
||||
|
||||
# TODO create extensive tests to make sure ORs ANDs and various filters, sorting, etc work as expected
|
||||
# TODO consider creating a simpleListRecipe API that only includes minimum of recipe info and minimal filtering
|
||||
def search_recipes(request, queryset, params):
|
||||
search_prefs = request.user.searchpreference
|
||||
search_string = params.get('query', '')
|
||||
@ -19,6 +21,7 @@ def search_recipes(request, queryset, params):
|
||||
search_foods = params.getlist('foods', [])
|
||||
search_books = params.getlist('books', [])
|
||||
|
||||
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
||||
search_keywords_or = params.get('keywords_or', True)
|
||||
search_foods_or = params.get('foods_or', True)
|
||||
search_books_or = params.get('books_or', True)
|
||||
@ -33,18 +36,22 @@ def search_recipes(request, queryset, params):
|
||||
last_viewed_recipes = ViewLog.objects.filter(
|
||||
created_by=request.user, space=request.space,
|
||||
created_at__gte=datetime.now() - timedelta(days=14)
|
||||
).order_by('pk').values_list('recipe__pk', flat=True).distinct()
|
||||
).order_by('-pk').values_list('recipe__pk', flat=True)
|
||||
last_viewed_recipes = list(dict.fromkeys(last_viewed_recipes))[:search_last_viewed] # removes duplicates from list prior to slicing
|
||||
|
||||
return queryset.filter(pk__in=last_viewed_recipes[len(last_viewed_recipes) - min(len(last_viewed_recipes), search_last_viewed):])
|
||||
return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new')
|
||||
# queryset that only annotates most recent view (higher pk = lastest view)
|
||||
# TODO queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=Value(100)), default=Value(0))).order_by('-new')
|
||||
|
||||
orderby = []
|
||||
# TODO create setting for default ordering - most cooked, rating,
|
||||
# TODO create options for live sorting
|
||||
if search_new == 'true':
|
||||
queryset = queryset.annotate(
|
||||
new_recipe=Case(When(created_at__gte=(datetime.now() - timedelta(days=7)), then=Value(100)),
|
||||
default=Value(0), ))
|
||||
orderby += ['new_recipe']
|
||||
else:
|
||||
queryset = queryset
|
||||
queryset = (
|
||||
queryset.annotate(new_recipe=Case(
|
||||
When(created_at__gte=(datetime.now() - timedelta(days=81)), then=('pk')), default=Value(0),))
|
||||
)
|
||||
orderby += ['-new_recipe']
|
||||
|
||||
search_type = search_prefs.search or 'plain'
|
||||
if len(search_string) > 0:
|
||||
@ -124,12 +131,12 @@ def search_recipes(request, queryset, params):
|
||||
queryset = queryset.filter(query_filter)
|
||||
|
||||
if len(search_keywords) > 0:
|
||||
# TODO creating setting to include descendants of keywords a setting
|
||||
if search_keywords_or == 'true':
|
||||
# when performing an 'or' search all descendants are included in the OR condition
|
||||
# so descendants are appended to filter all at once
|
||||
for kw in Keyword.objects.filter(pk__in=search_keywords):
|
||||
search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
||||
# TODO creating setting to include descendants of keywords a setting
|
||||
# for kw in Keyword.objects.filter(pk__in=search_keywords):
|
||||
# search_keywords += list(kw.get_descendants().values_list('pk', flat=True))
|
||||
queryset = queryset.filter(keywords__id__in=search_keywords)
|
||||
else:
|
||||
# when performing an 'and' search returned recipes should include a parent OR any of its descedants
|
||||
@ -165,24 +172,31 @@ def search_recipes(request, queryset, params):
|
||||
return queryset
|
||||
|
||||
|
||||
def get_facet(qs, params):
|
||||
def get_facet(qs, params, space):
|
||||
# NOTE facet counts for tree models include self AND descendants
|
||||
facets = {}
|
||||
ratings = params.getlist('ratings', [])
|
||||
keyword_list = params.getlist('keywords', [])
|
||||
ingredient_list = params.getlist('ingredient', [])
|
||||
food_list = params.getlist('foods', [])
|
||||
book_list = params.getlist('book', [])
|
||||
search_keywords_or = params.get('keywords_or', True)
|
||||
search_foods_or = params.get('foods_or', True)
|
||||
search_books_or = params.get('books_or', True)
|
||||
|
||||
# this returns a list of keywords in the queryset and how many times it appears
|
||||
kws = Keyword.objects.filter(recipe__in=qs).annotate(kw_count=Count('recipe'))
|
||||
# if using an OR search, will annotate all keywords, otherwise, just those that appear in results
|
||||
if search_keywords_or:
|
||||
keywords = Keyword.objects.filter(space=space).annotate(recipe_count=Count('recipe'))
|
||||
else:
|
||||
keywords = Keyword.objects.filter(recipe__in=qs, space=space).annotate(recipe_count=Count('recipe'))
|
||||
# custom django-tree function annotates a queryset to make building a tree easier.
|
||||
# see https://django-treebeard.readthedocs.io/en/latest/api.html#treebeard.models.Node.get_annotated_list_qs for details
|
||||
kw_a = annotated_qs(kws, root=True, fill=True)
|
||||
kw_a = annotated_qs(keywords, root=True, fill=True)
|
||||
|
||||
# TODO add rating facet
|
||||
facets['Ratings'] = []
|
||||
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
||||
# TODO add food facet
|
||||
facets['Ingredients'] = []
|
||||
facets['Foods'] = []
|
||||
# TODO add book facet
|
||||
facets['Books'] = []
|
||||
|
||||
|
@ -12,7 +12,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db.models import Q
|
||||
from django.db.models import Case, Q, Value, When
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django_scopes import scopes_disabled
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
@ -106,10 +106,19 @@ class FuzzyFilterMixin(ViewSetMixin):
|
||||
|
||||
if query is not None and query not in ["''", '']:
|
||||
if fuzzy:
|
||||
self.queryset = self.queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||
.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2)
|
||||
.order_by('-exact').order_by("-trigram")
|
||||
)
|
||||
else:
|
||||
# TODO have this check unaccent search settings?
|
||||
self.queryset = self.queryset.filter(name__icontains=query)
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(name__icontains=query).order_by('-exact')
|
||||
)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
if updated_at is not None:
|
||||
@ -466,7 +475,7 @@ class RecipePagination(PageNumberPagination):
|
||||
max_page_size = 100
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
self.facets = get_facet(queryset, request.query_params)
|
||||
self.facets = get_facet(queryset, request.query_params, request.space)
|
||||
return super().paginate_queryset(queryset, request, view)
|
||||
|
||||
def get_paginated_response(self, data):
|
||||
|
Reference in New Issue
Block a user