Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add unaccent to search #110

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions apis_ontology/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,52 @@
from apis_core.generic.filtersets import django_filters
from apis_core.relations.filtersets import RelationFilterSet
from django.db import models
from django.db.models import Q, CharField, TextField
from apis_ontology.forms import RelationFilterSetForm, EntityFilterSetForm
from django.contrib.postgres.search import SearchVector, SearchQuery


def generic_search_filter(queryset, name, value, fields=None):
"""
A generic filter that searches across specified fields using unaccent__icontains with OR logic.

Priority for fields selection:
1. Explicitly provided fields parameter
2. _default_search_fields attribute on the model
3. All CharField and TextField fields from the model

Args:
queryset: The queryset to filter
name: The name of the filter (not used)
value: The search value
fields: Optional list of specific field names to search in

Returns:
Filtered queryset
"""
if not value:
return queryset

# If no fields specified, check for _default_search_fields or use all text fields
if fields is None:
model = queryset.model

# Check if model has _default_search_fields attribute
if hasattr(model, "_default_search_fields"):
fields = model._default_search_fields
else:
# Fall back to all CharField and TextField fields
fields = []
for field in model._meta.get_fields():
if isinstance(field, (CharField, TextField)) and not field.primary_key:
fields.append(field.name)

# Build Q objects for each field with OR logic
q_objects = Q()
for field in fields:
q_objects |= Q(**{f"{field}__unaccent__icontains": value})

return queryset.filter(q_objects)


class NomanslandMixinFilterSet(AbstractEntityFilterSet):
Expand All @@ -28,17 +73,30 @@ class Meta(AbstractEntityFilterSet.Meta):
models.CharField: {
"filter_class": django_filters.CharFilter,
"extra": lambda f: {
"lookup_expr": "icontains",
"lookup_expr": "unaccent__icontains",
},
},
models.TextField: {
"filter_class": django_filters.CharFilter,
"extra": lambda f: {
"lookup_expr": "icontains",
"lookup_expr": "unaccent__icontains",
},
},
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for filter in self.filters.values():
if (
hasattr(filter, "label")
and filter.label
and "unaccent contains" in filter.label
):
filter.label = filter.label.replace("unaccent contains", "")
self.filters["search"] = django_filters.CharFilter(
method=generic_search_filter, label="Search"
)


class NomanslandRelationMixinFilterSet(RelationFilterSet):
class Meta(RelationFilterSet.Meta):
Expand Down Expand Up @@ -67,3 +125,12 @@ class Meta(RelationFilterSet.Meta):
},
},
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for filter in self.filters.values():
if hasattr(filter, "label") and filter.label and "contains" in filter.label:
filter.label = filter.label.replace("contains", "")
self.filters["search"] = django_filters.CharFilter(
method=generic_search_filter, label="Search"
)
7 changes: 7 additions & 0 deletions apis_ontology/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Meta:


class Person(E21_Person, VersionMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["forename", "surname"]
GENDERS = [
("male", "Male"),
("female", "Female"),
Expand Down Expand Up @@ -145,6 +146,7 @@ class Meta:
class Place(
E53_Place, VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity
):
_default_search_fields = ["label"]
class_uri = "http://id.loc.gov/ontologies/bibframe/Place"
kind = models.ForeignKey(
PlaceType, blank=True, null=True, on_delete=models.SET_NULL
Expand Down Expand Up @@ -176,6 +178,7 @@ class Meta:


class Institution(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["name"]
name = models.CharField(max_length=255)
kind = models.ForeignKey(
InstitutionType, blank=True, null=True, on_delete=models.SET_NULL
Expand All @@ -201,6 +204,7 @@ class Meta:


class Event(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["name"]
name = models.CharField(max_length=255)
kind = models.ForeignKey(
EventType, blank=True, null=True, on_delete=models.SET_NULL
Expand Down Expand Up @@ -237,6 +241,7 @@ class Meta:


class Work(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["name"]
name = models.CharField(max_length=255)
kind = models.ForeignKey(WorkType, blank=True, null=True, on_delete=models.SET_NULL)
subject_heading = models.ManyToManyField(SubjectHeading, blank=True)
Expand Down Expand Up @@ -296,6 +301,7 @@ def get_queryset(self):


class Expression(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["title"]
title = models.CharField(max_length=255, blank=True, null=True)
locus = models.CharField(max_length=255, blank=True, null=True)
script_type_title = models.ForeignKey(
Expand Down Expand Up @@ -337,6 +343,7 @@ class Meta:


class Manuscript(VersionMixin, NomanslandDateMixin, NomanslandMixin, AbstractEntity):
_default_search_fields = ["name", "identifier", "description", "notes", "additions"]
name = models.CharField(max_length=255, blank=True, null=True)
identifier = models.CharField(max_length=255, blank=True, null=True)
extent = models.CharField(max_length=255, blank=True, null=True)
Expand Down