fuzzy match on lookups

This commit is contained in:
smilerz 2021-06-06 14:12:19 -05:00
parent 32c488f4a8
commit b556bed56e
8 changed files with 57 additions and 29 deletions

View File

@ -479,10 +479,11 @@ class SearchPreferenceForm(forms.ModelForm):
class Meta:
model = SearchPreference
fields = ('search', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext')
fields = ('search', 'lookup', 'unaccent', 'icontains', 'istartswith', 'trigram', 'fulltext')
help_texts = {
'search': _('Select type method of search. Click <a href="/docs/search/">here</a> for full desciption of choices.'),
'lookup': _('Use fuzzy matching on units, keywords and ingredients when editing and importing recipes.'),
'unaccent': _('Fields to search ignoring accents. Selecting this option can improve or degrade search quality depending on language'),
'icontains': _("Fields to search for partial matches. (e.g. searching for 'Pie' will return 'pie' and 'piece' and 'soapie')"),
'istartswith': _("Fields to search for beginning of word matches. (e.g. searching for 'sa' will return 'salad' and 'sandwich')"),
@ -492,6 +493,7 @@ class SearchPreferenceForm(forms.ModelForm):
labels = {
'search': _('Search Method'),
'lookup': _('Fuzzy Lookups'),
'unaccent': _('Ignore Accent'),
'icontains': _("Partial Match"),
'istartswith': _("Starts Wtih"),

View File

@ -95,7 +95,8 @@ class Migration(migrations.Migration):
name='SearchPreference',
fields=[
('user', annoying.fields.AutoOneToOneField(on_delete=deletion.CASCADE, primary_key=True, serialize=False, to='auth.user')),
('search', models.CharField(choices=[('plain', 'Plain'), ('phrase', 'Phrase'), ('websearch', 'Web'), ('raw', 'Raw')], default='plain', max_length=32)),
('search', models.CharField(choices=[('plain', 'Simple'), ('phrase', 'Phrase'), ('websearch', 'Web'), ('raw', 'Raw')], default='plain', max_length=32)),
('lookup', models.BooleanField(default=False)),
('fulltext', models.ManyToManyField(blank=True, related_name='fulltext_fields', to='cookbook.SearchFields')),
('icontains', models.ManyToManyField(blank=True, default=nameSearchField, related_name='icontains_fields', to='cookbook.SearchFields')),
('istartswith', models.ManyToManyField(blank=True, related_name='istartswith_fields', to='cookbook.SearchFields')),

View File

@ -760,6 +760,7 @@ class SearchPreference(models.Model, PermissionModelMixin):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
search = models.CharField(choices=SEARCH_STYLE, max_length=32, default=SIMPLE)
lookup = models.BooleanField(default=False)
unaccent = models.ManyToManyField(SearchFields, related_name="unaccent_fields", blank=True, default=allSearchFields)
icontains = models.ManyToManyField(SearchFields, related_name="icontains_fields", blank=True, default=nameSearchField)
istartswith = models.ManyToManyField(SearchFields, related_name="istartswith_fields", blank=True)

File diff suppressed because one or more lines are too long

View File

@ -171,5 +171,15 @@
token.select();
document.execCommand("copy");
}
// Javascript to enable link to tab
var hash = location.hash.replace(/^#/, ''); // ^ means starting, meaning only match the first hash
if (hash) {
$('.nav-tabs a[href="#' + hash + '"]').tab('show');
}
// Change hash for page-reload
$('.nav-tabs a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash;
})
</script>
{% endblock %}

View File

@ -9,6 +9,7 @@ from annoying.decorators import ajax_request
from annoying.functions import get_object_or_None
from django.contrib import messages
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
@ -88,6 +89,38 @@ class StandardFilterMixin(ViewSetMixin):
return queryset
class FuzzyFilterMixin(ViewSetMixin):
def get_queryset(self):
queryset = self.queryset
query = self.request.query_params.get('query', None)
fuzzy = self.request.user.searchpreference.lookup
if query is not None or query != '':
if fuzzy:
queryset = queryset.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2).order_by("-trigram")
else:
# TODO have this check unaccent search settings?
queryset = queryset.filter(name__icontains=query)
updated_at = self.request.query_params.get('updated_at', None)
if updated_at is not None:
try:
queryset = queryset.filter(updated_at__gte=updated_at)
except FieldError:
pass
except ValidationError:
raise APIException(_('Parameter updated_at incorrectly formatted'))
limit = self.request.query_params.get('limit', None)
random = self.request.query_params.get('random', False)
if limit is not None:
if random:
queryset = queryset.order_by("?")
queryset = queryset[:int(limit)]
return queryset
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
"""
list:
@ -159,27 +192,7 @@ class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class SupermarketCategoryViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategory.objects
serializer_class = SupermarketCategorySerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMixin):
queryset = SupermarketCategoryRelation.objects
serializer_class = SupermarketCategoryRelationSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
self.queryset = self.queryset.filter(supermarket__space=self.request.space)
return super().get_queryset()
class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class KeywordViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
"""
list:
optional parameters
@ -197,7 +210,7 @@ class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class UnitViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
queryset = Unit.objects
serializer_class = UnitSerializer
permission_classes = [CustomIsUser]
@ -207,7 +220,7 @@ class UnitViewSet(viewsets.ModelViewSet, StandardFilterMixin):
return super().get_queryset()
class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin):
class FoodViewSet(viewsets.ModelViewSet, FuzzyFilterMixin):
queryset = Food.objects
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]

View File

@ -34,7 +34,6 @@ from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object
from recipes import settings
from recipes.settings import DEMO
from recipes.version import BUILD_REF, VERSION_NUMBER
@ -374,6 +373,7 @@ def user_settings(request):
search_error = True
else:
sp.search = search_form.cleaned_data['search']
sp.lookup = search_form.cleaned_data['lookup']
sp.unaccent.set(search_form.cleaned_data['unaccent'])
sp.icontains.set(search_form.cleaned_data['icontains'])
sp.istartswith.set(search_form.cleaned_data['istartswith'])
@ -398,6 +398,7 @@ def user_settings(request):
# these fields require postgress - just disable them if postgress isn't available
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
search_form.fields['search'].disabled = True
search_form.fields['lookup'].disabled = True
search_form.fields['trigram'].disabled = True
search_form.fields['fulltext'].disabled = True

View File

@ -119,7 +119,7 @@
</div>
<div class="row" style="margin-top: 1vh">
<div class="col-12">
<a :href="resolveDjangoUrl('view_settings')">{{ $t('Advanced Search Settings') }}</a>
<a :href="resolveDjangoUrl('view_settings') + '#search'">{{ $t('Advanced Search Settings') }}</a>
</div>
</div>
<div class="row" style="margin-top: 1vh">