diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 608fd8a2..13c06fe5 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -201,12 +201,11 @@ def search_recipes(request, queryset, params): return queryset -class CacheEmpty(Exception): - pass - - class RecipeFacet(): - def __init__(self, request, queryset=None, hash_key=None, cache_timeout=600): + class CacheEmpty(Exception): + pass + + def __init__(self, request, queryset=None, hash_key=None, cache_timeout=3600): if hash_key is None and queryset is None: raise ValueError(_("One of queryset or hash_key must be provided")) @@ -215,15 +214,15 @@ class RecipeFacet(): self.hash_key = hash_key or str(hash(frozenset(self._queryset.values_list('pk')))) self._SEARCH_CACHE_KEY = f"recipes_filter_{self.hash_key}" self._cache_timeout = cache_timeout - self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, None) + self._cache = caches['default'].get(self._SEARCH_CACHE_KEY, {}) if self._cache is None and self._queryset is None: - raise CacheEmpty("No queryset provided and cache empty") + raise self.CacheEmpty("No queryset provided and cache empty") - self.Keywords = getattr(self._cache, 'Keywords', None) - self.Foods = getattr(self._cache, 'Foods', None) - self.Books = getattr(self._cache, 'Books', None) - self.Ratings = getattr(self._cache, 'Ratings', None) - self.Recent = getattr(self._cache, 'Recent', None) + self.Keywords = self._cache.get('Keywords', None) + self.Foods = self._cache.get('Foods', None) + self.Books = self._cache.get('Books', None) + self.Ratings = self._cache.get('Ratings', None) + self.Recent = self._cache.get('Recent', None) if self._queryset: self._recipe_list = list(self._queryset.values_list('id', flat=True)) @@ -292,16 +291,9 @@ class RecipeFacet(): else: keywords = Keyword.objects.filter(Q(recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct() - # Subquery that counts recipes for keyword including children - kw_recipe_count = Recipe.objects.filter(**{'keywords__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space - ).values(kw=Substr('keywords__path', 1, Keyword.steplen) - ).annotate(count=Count('pk', distinct=True)).values('count') - # set keywords to root objects only - keywords = keywords.annotate(count=Coalesce(Subquery(kw_recipe_count), 0) - ).filter(depth=1, count__gt=0 - ).values('id', 'name', 'count', 'numchild').order_by('name') - self.Keywords = list(keywords) + keywords = self._keyword_queryset(keywords) + self.Keywords = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)] self.set_cache('Keywords', self.Keywords) return self.Keywords @@ -313,16 +305,10 @@ class RecipeFacet(): else: foods = Food.objects.filter(Q(ingredient__step__recipe__in=self._recipe_list) | Q(depth=1)).filter(space=self._request.space).distinct() - food_recipe_count = Recipe.objects.filter(**{'steps__ingredients__food__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space - ).values(kw=Substr('steps__ingredients__food__path', 1, Food.steplen) - ).annotate(count=Count('pk', distinct=True)).values('count') - # set keywords to root objects only - foods = foods.annotate(count=Coalesce(Subquery(food_recipe_count), 0) - ).filter(depth=1, count__gt=0 - ).values('id', 'name', 'count', 'numchild' - ).order_by('name') - self.Foods = list(foods) + foods = self._food_queryset(foods) + + self.Foods = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)] self.set_cache('Foods', self.Foods) return self.Foods @@ -349,6 +335,59 @@ class RecipeFacet(): self.set_cache('Recent', self.Recent) return self.Recent + def add_food_children(self, id): + try: + food = Food.objects.get(id=id) + nodes = food.get_ancestors() + except Food.DoesNotExist: + return self.get_facets() + foods = self._food_queryset(Food.objects.filter(path__startswith=food.path, depth=food.depth+1), food) + deep_search = self.Foods + for node in nodes: + index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None) + deep_search = deep_search[index]['children'] + index = next((i for i, x in enumerate(deep_search) if x["id"] == food.id), None) + deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(foods)] + self.set_cache('Foods', self.Foods) + return self.get_facets() + + def add_keyword_children(self, id): + try: + keyword = Keyword.objects.get(id=id) + nodes = keyword.get_ancestors() + except Keyword.DoesNotExist: + return self.get_facets() + keywords = self._keyword_queryset(Keyword.objects.filter(path__startswith=keyword.path, depth=keyword.depth+1), keyword) + deep_search = self.Keywords + for node in nodes: + index = next((i for i, x in enumerate(deep_search) if x["id"] == node.id), None) + deep_search = deep_search[index]['children'] + index = next((i for i, x in enumerate(deep_search) if x["id"] == keyword.id), None) + deep_search[index]['children'] = [{**x, 'children': None} if x['numchild'] > 0 else x for x in list(keywords)] + self.set_cache('Keywords', self.Keywords) + return self.get_facets() + + def _recipe_count_queryset(self, field, depth=1, steplen=4): + return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space + ).values(child=Substr(f'{field}__path', 1, steplen) + ).annotate(count=Count('pk', distinct=True)).values('count') + + def _keyword_queryset(self, queryset, keyword=None): + depth = getattr(keyword, 'depth', 0) + 1 + steplen = depth * Keyword.steplen + + return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('keywords', depth, steplen)), 0) + ).filter(depth=depth, count__gt=0 + ).values('id', 'name', 'count', 'numchild').order_by('name') + + def _food_queryset(self, queryset, food=None): + depth = getattr(food, 'depth', 0) + 1 + steplen = depth * Food.steplen + + return queryset.annotate(count=Coalesce(Subquery(self._recipe_count_queryset('steps__ingredients__food', depth, steplen)), 0) + ).filter(depth__lte=depth, count__gt=0 + ).values('id', 'name', 'count', 'numchild').order_by('name') + # # TODO: This might be faster https://github.com/django-treebeard/django-treebeard/issues/115 # def get_facet(qs=None, request=None, use_cache=True, hash_key=None, food=None, keyword=None): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index e9ba46ee..333e0c8b 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1135,9 +1135,16 @@ def get_facets(request): keyword = request.GET.get('keyword', None) facets = RecipeFacet(request, hash_key=key) + if food: + results = facets.add_food_children(food) + elif keyword: + results = facets.add_keyword_children(keyword) + else: + results = facets.get_facets() + return JsonResponse( { - 'facets': facets.get_facets(), + 'facets': results, }, status=200 ) diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index b6c71789..0e8d25a0 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -80,7 +80,7 @@