Skip to content

Commit

Permalink
Merge pull request TandoorRecipes#1347 from smilerz/fuzzy_search
Browse files Browse the repository at this point in the history
Fuzzy search
  • Loading branch information
vabene1111 committed Jan 17, 2022
2 parents d3b71e4 + 30421d0 commit 7c5ffda
Show file tree
Hide file tree
Showing 10 changed files with 1,430 additions and 1,095 deletions.
613 changes: 456 additions & 157 deletions cookbook/helper/recipe_search.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cookbook/helper/shopping_helper.py
Expand Up @@ -38,6 +38,7 @@ def shopping_helper(qs, request):
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')


# TODO refactor as class
def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None, ingredients=None, created_by=None, space=None, append=False):
"""
Creates ShoppingListRecipe and associated ShoppingListEntrys from a recipe or a meal plan with a recipe
Expand Down
1 change: 1 addition & 0 deletions cookbook/models.py
Expand Up @@ -151,6 +151,7 @@ def add_root(self, **kwargs):
return super().add_root(**kwargs)

# i'm 99% sure there is a more idiomatic way to do this subclassing MP_NodeQuerySet
@staticmethod
def include_descendants(queryset=None, filter=None):
"""
:param queryset: Model Queryset to add descendants
Expand Down
5 changes: 3 additions & 2 deletions cookbook/serializer.py
Expand Up @@ -519,7 +519,7 @@ def get_recipe_rating(self, obj):

def get_recipe_last_cooked(self, obj):
try:
last = obj.cooklog_set.filter(created_by=self.context['request'].user).last()
last = obj.cooklog_set.filter(created_by=self.context['request'].user).order_by('created_at').last()
if last:
return last.created_at
except TypeError:
Expand All @@ -539,6 +539,7 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
new = serializers.SerializerMethodField('is_recipe_new')
recent = serializers.ReadOnlyField()

def create(self, validated_data):
pass
Expand All @@ -551,7 +552,7 @@ class Meta:
fields = (
'id', 'name', 'description', 'image', 'keywords', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new'
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
read_only_fields = ['image', 'created_by', 'created_at']

Expand Down
126 changes: 88 additions & 38 deletions cookbook/templates/url_import.html
Expand Up @@ -498,6 +498,8 @@ <h3>{% trans 'Discovered Attributes' %}</h3>
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="{% trans 'Select one' %}"
tag-placeholder="{% trans 'Select' %}"
label="text"
Expand Down Expand Up @@ -536,6 +538,8 @@ <h3>{% trans 'Discovered Attributes' %}</h3>
:clear-on-select="true"
:allow-empty="false"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
label="text"
track-by="id"
:multiple="false"
Expand Down Expand Up @@ -586,6 +590,8 @@ <h3>{% trans 'Discovered Attributes' %}</h3>
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="{% trans 'Select one' %}"
tag-placeholder="{% trans 'Add Keyword' %}"
:taggable="true"
Expand Down Expand Up @@ -660,6 +666,7 @@ <h5 class="card-title">{% trans 'Information' %}</h5>
Vue.http.headers.common['X-CSRFToken'] = csrftoken;

Vue.component('vue-multiselect', window.VueMultiselect.default)
import { ApiApiFactory } from "@/utils/openapi/api"

let app = new Vue({
components: {
Expand Down Expand Up @@ -693,7 +700,8 @@ <h5 class="card-title">{% trans 'Information' %}</h5>
import_duplicates: false,
recipe_files: [],
images: [],
mode: 'url'
mode: 'url',
options_limit:25
},
directives: {
tabindex: {
Expand All @@ -703,9 +711,9 @@ <h5 class="card-title">{% trans 'Information' %}</h5>
}
},
mounted: function () {
this.searchKeywords('')
this.searchUnits('')
this.searchIngredients('')
// this.searchKeywords('')
// this.searchUnits('')
// this.searchIngredients('')
let uri = window.location.search.substring(1);
let params = new URLSearchParams(uri);
q = params.get("id")
Expand Down Expand Up @@ -877,51 +885,93 @@ <h5 class="card-title">{% trans 'Information' %}</h5>
this.$set(this.$refs.ingredient[index].$data, 'search', this.recipe_data.recipeIngredient[index].ingredient.text)
},
searchKeywords: function (query) {
// this.keywords_loading = true
// this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
// this.keywords = response.data.results;
// this.keywords_loading = false
// }).catch((err) => {
// console.log(err)
// this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
// })
let apiFactory = new ApiApiFactory()

this.keywords_loading = true
this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
this.keywords = response.data.results;
this.keywords_loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
apiFactory
.listKeywords(query, undefined, undefined, 1, this.options_limit)
.then((response) => {
this.keywords = response.data.results
this.keywords_loading = false
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
searchUnits: function (query) {
let apiFactory = new ApiApiFactory()

this.units_loading = true
this.$http.get("{% url 'dal_unit' %}" + '?q=' + query).then((response) => {
this.units = response.data.results;
if (this.recipe_data !== undefined) {
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
if (x.unit !== null && x.unit.text !== '') {
this.units = this.units.filter(item => item.text !== x.unit.text)
this.units.push(x.unit)
apiFactory
.listUnits(query, 1, this.options_limit)
.then((response) => {
this.units = response.data.results

if (this.recipe !== undefined) {
for (let s of this.recipe.steps) {
for (let i of s.ingredients) {
if (i.unit !== null && i.unit.id === undefined) {
this.units.push(i.unit)
}
}
}
}
}
this.units_loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
this.units_loading = false
})
.catch((err) => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
searchIngredients: function (query) {
this.ingredients_loading = true
this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
this.ingredients = response.data.results
if (this.recipe_data !== undefined) {
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
if (x.ingredient.text !== '') {
this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
this.ingredients.push(x.ingredient)
// this.ingredients_loading = true
// this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
// this.ingredients = response.data.results
// if (this.recipe_data !== undefined) {
// for (let x of Array.from(this.recipe_data.recipeIngredient)) {
// if (x.ingredient.text !== '') {
// this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
// this.ingredients.push(x.ingredient)
// }
// }
// }

// this.ingredients_loading = false
// }).catch((err) => {
// console.log(err)
// this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
// })
let apiFactory = new ApiApiFactory()

this.foods_loading = true
apiFactory
.listFoods(query, undefined, undefined, 1, this.options_limit)
.then((response) => {
this.foods = response.data.results

if (this.recipe !== undefined) {
for (let s of this.recipe.steps) {
for (let i of s.ingredients) {
if (i.food !== null && i.food.id === undefined) {
this.foods.push(i.food)
}
}
}
}
}

this.ingredients_loading = false
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
this.foods_loading = false
})
.catch((err) => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
},
deleteNode: function (node, item, e) {
e.stopPropagation()
Expand Down
25 changes: 14 additions & 11 deletions cookbook/views/api.py
Expand Up @@ -12,7 +12,8 @@
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 Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When
from django.db.models import (Case, Count, Exists, F, IntegerField, OuterRef, ProtectedError, Q,
Subquery, Value, When)
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce
from django.http import FileResponse, HttpResponse, JsonResponse
Expand All @@ -38,7 +39,7 @@
CustomIsShare, CustomIsShared, CustomIsUser,
group_required)
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_search import RecipeFacet, old_search, search_recipes
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
from cookbook.helper.recipe_url_import import get_from_scraper
from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField,
Expand Down Expand Up @@ -145,18 +146,18 @@ def get_queryset(self):
if fuzzy:
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', '-trigram')
.annotate(starts=Case(When(name__istartswith=query, then=(Value(.3, output_field=IntegerField()))), default=Value(0)))
.annotate(trigram=TrigramSimilarity('name', query))
.annotate(sort=F('starts')+F('trigram'))
.order_by('-sort')
)
else:
# 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', 'name')
.annotate(starts=Case(When(name__istartswith=query, then=(Value(100))),
default=Value(0))) # put exact matches at the top of the result set
.filter(name__icontains=query).order_by('-starts', 'name')
)

updated_at = self.request.query_params.get('updated_at', None)
Expand Down Expand Up @@ -652,8 +653,10 @@ def get_queryset(self):
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)

self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
return super().get_queryset().prefetch_related('cooklog_set')
# self.queryset = search_recipes(self.request, self.queryset, self.request.GET)
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x in list(self.request.GET)}
self.queryset = RecipeSearch(self.request, **params).get_queryset(self.queryset).prefetch_related('cooklog_set')
return self.queryset

def list(self, request, *args, **kwargs):
if self.request.GET.get('debug', False):
Expand Down

0 comments on commit 7c5ffda

Please sign in to comment.