Skip to content
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
6 changes: 6 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,11 @@ services:
redis:
image: redis

meilisearch:
image: getmeili/meilisearch:v1.12.5
volumes:
- search_data:/meili_data

volumes:
postgres_data:
search_data:
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"google-api-python-client>=2.196.0",
"google-auth>=2.53.0",
"pygithub>=2.9.1",
"meilisearch>=0.43.0",
]

[tool.ruff.lint]
Expand Down
56 changes: 56 additions & 0 deletions trojstenid/schools/management/commands/indexschools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.core.management.base import BaseCommand

from trojstenid import search
from trojstenid.schools.models import School


class Command(BaseCommand):
help = "Send schools to Meilisearch for indexing"

def handle(self, *args, **options):
self.stdout.write("Indexing schools...")

schools = []
for s in School.objects.filter(is_selectable=True).all():
schools.append(s.to_dict())

search.client.index("schools").update_settings(
{
"filterableAttributes": ["types"],
"sortableAttributes": ["name", "address"],
"rankingRules": [
"words",
"sort",
"typo",
"proximity",
"attribute",
"exactness",
],
"synonyms": {
"zš": [
"základná škola",
"základní škola",
"elemi iskola",
"alapiskola",
],
"základná škola": ["zš"],
"základní škola": ["zš"],
"elemi iskola": ["zš"],
"alapiskola": ["zš"],
"sš": [
"stredná škola",
"strední škola",
"középiskola",
"střední škola",
],
"stredná škola": ["sš"],
"strední škola": ["sš"],
"középiskola": ["sš"],
"střední škola": ["sš"],
},
}
)

search.client.index("schools").add_documents(schools)

self.stdout.write(self.style.SUCCESS("Schools indexed successfully."))
4 changes: 4 additions & 0 deletions trojstenid/schools/management/commands/syncschools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from trojstenid.schools.services.school_sync import create_null_school, sync_schools

from .indexschools import Command as IndexSchoolsCommand


class Command(BaseCommand):
help = "Sync school database."
Expand All @@ -12,3 +14,5 @@ def handle(self, *args: Any, **options: Any) -> str | None:
sync_schools()
create_null_school()
self.stdout.write(self.style.SUCCESS("School sync sucessfull."))

IndexSchoolsCommand().handle()
2 changes: 1 addition & 1 deletion trojstenid/schools/templates/settings/school_search.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% for school in schools %}
<a href="?school_id={{ school.id }}" class="block py-2 px-3 rounded-sm border hover:bg-gray-100">{{ school }}</a>
<a href="?school_id={{ school.id }}" class="block py-2 px-3 rounded-sm border hover:bg-gray-100">{{ school.name }}{% if school.address %}, {{ school.address }}{% endif %}</a>
{% endfor %}
<a href="?school_id=-1" class="block py-2 px-3 rounded-sm border hover:bg-gray-100 text-gray-600">
Moja škola nie je v zozname.
Expand Down
9 changes: 8 additions & 1 deletion trojstenid/schools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.views import View
from django.views.generic import FormView, ListView, TemplateView

from trojstenid import search
from trojstenid.schools.forms import SchoolRecordForm
from trojstenid.schools.models import School, UserSchoolRecord
from trojstenid.users.models import User
Expand Down Expand Up @@ -111,5 +112,11 @@ class SchoolSearchView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
ctx = super().get_context_data(**kwargs)
query = self.request.GET.get("q", "")
ctx["schools"] = School.objects.search(query)[:10]
result = search.client.index("schools").search(
query,
{
"limit": 10,
},
)
ctx["schools"] = result["hits"]
return ctx
78 changes: 78 additions & 0 deletions trojstenid/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from functools import cache

import meilisearch
from django.conf import settings
from django.core.paginator import Page

enabled = bool(settings.MEILISEARCH_URL)
client = meilisearch.Client(settings.MEILISEARCH_URL, settings.MEILISEARCH_KEY)


class MeiliQuerySet:
"""
MeiliQuerySet provides a Paginator-compatible API to allow the use of Meilisearch
results in ListViews.

When using our admin_paginator, it is advisable to use `nerf_page()` on the Page
object.
"""

def __init__(self, queryset, index, search_term, opt_params=None):
self.queryset = queryset
self.index = index
self.search_term = search_term
self.opt_params = opt_params

@cache
def get_results(self, offset=0, limit=20):
params = self.opt_params or {}
params["offset"] = offset
params["limit"] = limit

res = client.index(self.index).search(self.search_term, params)

hits = res["hits"]
ids = [hit["id"] for hit in hits]
objects = self.queryset.filter(id__in=ids)
objects = sorted(objects, key=lambda o: ids.index(o.id))
return objects

def __len__(self):
return 10**10

def __getitem__(self, item):
if not isinstance(item, slice):
raise ValueError(
"Instance of MeiliQuerySet does not support getting exact items."
)

return self.get_results(item.start, item.stop - item.start)


class DumbPage(Page):
"""
A dumb implementation of Page object that does not need to know the
total number of pages.
"""

def __repr__(self):
return f"<Page {self.number} of many>"

def __len__(self):
return len(self.object_list)

def has_next(self):
return len(self) == self.paginator.per_page

def next_page_number(self):
return self.number + 1

def previous_page_number(self):
return self.number - 1


def nerf_page(page: Page) -> DumbPage:
"""
Converts a normal Page object into a dumb one.
"""
return DumbPage(page.object_list, page.number, page.paginator)
2 changes: 2 additions & 0 deletions trojstenid/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@
},
}

MEILISEARCH_URL = env("MEILISEARCH_URL", default="http://meilisearch:7700/")
MEILISEARCH_KEY = env("MEILISEARCH_KEY", default=None)

if DEBUG:
import socket # only if you haven't already imported this
Expand Down
2 changes: 1 addition & 1 deletion trojstenid/users/views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,5 @@ def get_object(self):

try:
return qs.get(id=int(lookup_value))
except (ValueError, User.DoesNotExist):
except ValueError, User.DoesNotExist:
return qs.get(username=lookup_value)
Loading
Loading