exact match appears first on keyword/food/unit searches

This commit is contained in:
smilerz
2021-08-25 16:37:43 -05:00
parent b00c189fd6
commit b74fdb3825
2 changed files with 46 additions and 23 deletions

View File

@ -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'] = []

View File

@ -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):