Skip to content

Commit

Permalink
Upgrade the search mixin
Browse files Browse the repository at this point in the history
- select the search query and vector based on the language code
- add cache invalidation
- add type hints
  • Loading branch information
tudoramariei committed Oct 30, 2024
1 parent a1b7ba7 commit 11403db
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 19 deletions.
4 changes: 4 additions & 0 deletions backend/civil_society_vote/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
DATA_UPLOAD_MAX_MEMORY_SIZE=(int, 3 * MEBIBYTE),
MAX_DOCUMENT_SIZE=(int, 50 * MEBIBYTE),
IMPERSONATE_READ_ONLY=(bool, False),
ENABLE_CACHE=(bool, True),
SEARCH_CACHE_LIMIT=(int, 50),
# db settings
# DATABASE_ENGINE=(str, "sqlite3"),
DATABASE_NAME=(str, "default"),
Expand Down Expand Up @@ -280,6 +282,8 @@ def show_toolbar(request):
"LOCATION": "/tmp/file_resubmit/",
}

SEARCH_CACHE_LIMIT = env.int("SEARCH_CACHE_LIMIT")

TIMEOUT_CACHE_SHORT = 60 # 1 minute
TIMEOUT_CACHE_NORMAL = 60 * 15 # 15 minutes
TIMEOUT_CACHE_LONG = 60 * 60 * 2 # 2 hours
Expand Down
89 changes: 70 additions & 19 deletions backend/hub/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Count, Q
from django.db.models import Count, Q, QuerySet
from django.db.utils import IntegrityError
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
Expand All @@ -37,6 +37,7 @@
)
from hub.models import (
FLAG_CHOICES,
SETTINGS_CHOICES,
BlogPost,
Candidate,
CandidateConfirmation,
Expand All @@ -46,7 +47,6 @@
Domain,
FeatureFlag,
Organization,
SETTINGS_CHOICES,
)
from hub.workers.update_organization import update_organization

Expand All @@ -60,7 +60,7 @@ class HealthView(View):
def get(self, request):
base_response = {
"status": "ok",
"timestamp": datetime.now().isoformat(),
"timestamp": timezone.now().isoformat(),
"version": settings.VERSION,
"revision": settings.REVISION,
}
Expand Down Expand Up @@ -126,22 +126,68 @@ def form_valid(self, form):


class SearchMixin(MenuMixin, ListView):
search_cache: Optional[Dict] = {}
search_cache: Dict[str, Dict[str, Union[datetime, QuerySet]]] = {}

def set_cache_key(self, key: str, value: QuerySet) -> None:
if len(self.search_cache) > settings.SEARCH_CACHE_LIMIT:
self.clean_cache()

self.search_cache[key] = {"time": timezone.now(), "value": value}

def get_cache_key(self, key: str) -> Optional[QuerySet]:
cache_entry = self.search_cache.get(key)

if cache_entry:
cache_key_expiration_limit: datetime = timezone.now() - timezone.timedelta(minutes=5)

if cache_entry["time"] > cache_key_expiration_limit:
return cache_entry["value"]

del self.search_cache[key]

return None

def clean_cache(self):
cache_key_expiration_limit: datetime = timezone.now() - timezone.timedelta(minutes=5)

for key, value in self.search_cache.items():
if value["time"] < cache_key_expiration_limit:
del self.search_cache[key]

@classmethod
def _configure_search_query(cls, query: str, language_code: str) -> SearchQuery:
if language_code == "ro":
return SearchQuery(query, config="romanian_unaccent")

return SearchQuery(query)

@classmethod
def _configure_search_vector(cls, language_code: str) -> SearchVector:
if language_code == "ro":
return SearchVector("name", weight="A", config="romanian_unaccent")

return SearchVector("name", weight="A")

def search(self, queryset):
# TODO: it should take into account selected language. Check only romanian for now.
query = self.request.GET.get("q")
language_code: str = settings.LANGUAGE_CODE
if hasattr(self.request, "LANGUAGE_CODE") and self.request.LANGUAGE_CODE:
language_code = self.request.LANGUAGE_CODE

language_code = language_code.lower()

query: str = self.request.GET.get("q")
if not query:
return queryset

if query_cache := self.search_cache.get(query):
return query_cache
cache_key: str = f"{query.lower()}-{language_code}"

search_query = SearchQuery(query, config="romanian_unaccent")
if cache_result := self.get_cache_key(key=cache_key):
return cache_result

vector = SearchVector("name", weight="A", config="romanian_unaccent")
search_query: SearchQuery = self._configure_search_query(query, language_code)
vector: SearchVector = self._configure_search_vector(language_code)

result = (
result: QuerySet = (
queryset.annotate(
rank=SearchRank(vector, search_query),
similarity=TrigramSimilarity("name", query),
Expand All @@ -151,7 +197,7 @@ def search(self, queryset):
.distinct("name")
)

self.search_cache[query] = result
self.set_cache_key(key=cache_key, value=result)

return result

Expand Down Expand Up @@ -244,7 +290,8 @@ class OrganizationListView(SearchMixin):
paginate_by = 9
template_name = "hub/ngo/list.html"

def group_organizations_by_domain(self, queryset) -> List[Dict[str, Union[Domain, List[Organization]]]]:
@classmethod
def group_organizations_by_domain(cls, queryset) -> List[Dict[str, Union[Domain, List[Organization]]]]:
organizations_by_domain: Dict[Domain, List[Organization]] = {}

for organization in queryset:
Expand Down Expand Up @@ -274,7 +321,8 @@ def get(self, request, *args, **kwargs):

return response

def get_qs(self):
@classmethod
def get_qs(cls):
return Organization.objects.filter(status=Organization.STATUS.accepted)

def get_queryset(self):
Expand Down Expand Up @@ -825,7 +873,8 @@ def candidate_revoke(request, pk):

candidate = get_object_or_404(Candidate, pk=pk)

if candidate.org != request.user.organization:
user: User = request.user
if candidate.org != user.organization:
return redirect("candidate-detail", pk=pk)

with transaction.atomic():
Expand Down Expand Up @@ -874,13 +923,14 @@ def candidate_status_confirm(request, pk):

candidate: Candidate = get_object_or_404(Candidate, pk=pk)

if candidate.org == request.user.organization or candidate.status == Candidate.STATUS.pending:
user: User = request.user
if candidate.org == user.organization or candidate.status == Candidate.STATUS.pending:
return redirect("candidate-detail", pk=pk)

confirmation, created = CandidateConfirmation.objects.get_or_create(user=request.user, candidate=candidate)
confirmation, created = CandidateConfirmation.objects.get_or_create(user=user, candidate=candidate)

if not created:
message = f"User {request.user} tried to confirm candidate {candidate} status again."
message = f"User {user} with ID {user.pk} tried to confirm candidate {candidate} status again."
logger.warning(message)
if settings.ENABLE_SENTRY:
capture_message(message, level="warning")
Expand Down Expand Up @@ -922,7 +972,8 @@ def update_organization_information(request, pk):


class CityAutocomplete(View):
def get(self, request):
@classmethod
def get(cls, request):
response = []
county = request.GET.get("county")
if county:
Expand Down

0 comments on commit 11403db

Please sign in to comment.