Skip to content

Commit

Permalink
feat: meilisearch backend for notes search
Browse files Browse the repository at this point in the history
This is a very simple and basic backend. It is based on Django signals,
just like the Elasticsearch backend. But it is much simpler, in the
sense that there are just two signals: one for saving documents and one
for deletion.

This backend is limited, in the sense that it does not support
highlighting -- but that's probably not such a big deal.

To start using this backend, define the following settings:

	ES_DISABLED = True
	MEILISEARCH_ENABLED = True
	MEILISEARCH_URL = "http://meilisearch:7700"
	MEILISEARCH_API_KEY = "s3cr3t"
	MEILISEARCH_INDEX = "tutor_student_notes"
  • Loading branch information
regisb committed Oct 17, 2024
1 parent d6e3fdf commit 5a9de41
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 2 deletions.
5 changes: 4 additions & 1 deletion notesapi/v1/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ def get_views_module():
Import views from either mysql or elasticsearch backend
"""
if settings.ES_DISABLED:
from . import common as backend_module
if getattr(settings, "MEILISEARCH_ENABLED", False):
from . import meilisearch as backend_module
else:
from . import common as backend_module
else:
from . import elasticsearch as backend_module
return backend_module
151 changes: 151 additions & 0 deletions notesapi/v1/views/meilisearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Meilisearch views to search for annotations.
To enable this backend, define the following settings:
ES_DISABLED = True
MEILISEARCH_ENABLED = True
Then check the Client class for more information about Meilisearch credential settings.
"""

import traceback

import meilisearch
from django.conf import settings
from django.db.models import signals
from django.dispatch import receiver

from notesapi.v1.models import Note

from .common import AnnotationSearchView as BaseAnnotationSearchView
from .exceptions import SearchViewRuntimeError


class Client:
"""
Simple Meilisearch client class
It depends on the following Django settings:
- MEILISEARCH_URL
- MEILISEARCH_API_KEY
- MEILISEARCH_INDEX
"""

_CLIENT = None
_INDEX = None
FILTERABLES = ["user_id", "course_id"]

@property
def meilisearch_client(self) -> meilisearch.Client:
"""
Return a meilisearch client.
"""
if self._CLIENT is None:
self._CLIENT = meilisearch.Client(
getattr(settings, "MEILISEARCH_URL", "http://meilisearch:7700"),
getattr(settings, "MEILISEARCH_API_KEY", ""),
)
return self._CLIENT

@property
def meilisearch_index(self) -> meilisearch.index.Index:
"""
Return the meilisearch index used to store annotations.
If the index does not exist, it is created. And if it does not have the right
filterable fields, then it is updated.
"""
if self._INDEX is None:
index_name = getattr(settings, "MEILISEARCH_INDEX", "student_notes")
try:
self._INDEX = self.meilisearch_client.get_index(index_name)
except meilisearch.errors.MeilisearchApiError:
task = self.meilisearch_client.create_index(
index_name, {"primaryKey": "id"}
)
self.meilisearch_client.wait_for_task(task.task_uid, timeout_in_ms=2000)
self._INDEX = self.meilisearch_client.get_index(index_name)

# Checking filterable attributes
existing_filterables = set(self._INDEX.get_filterable_attributes())
if not set(self.FILTERABLES).issubset(existing_filterables):
all_filterables = list(existing_filterables.union(self.FILTERABLES))
self._INDEX.update_filterable_attributes(all_filterables)

return self._INDEX


class AnnotationSearchView(BaseAnnotationSearchView):
def get_queryset(self):
"""
Simple result filtering method based on test search.
We simply include in the query only those that match the text search query. Note
that this backend does not support highlighting (yet).
"""
if not self.is_text_search:
return super().get_queryset()

queryset = Note.objects.filter(**self.query_params).order_by("-updated")

# Define meilisearch params
filters = [
f"user_id = '{self.params['user']}'",
f"course_id = '{self.params['course_id']}'",
]
page_size = int(self.params["page_size"])
offset = (int(self.params["page"]) - 1) * page_size

# Perform search
search_results = Client().meilisearch_index.search(
self.params["text"],
{"offset": offset, "limit": page_size, "filter": filters},
)

# Limit to these ID
queryset.filter(id__in=[r["id"] for r in search_results["hits"]])
return queryset


@receiver(signals.post_save, sender=Note)
def on_note_save(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Create or update a document.
"""
document = {
"id": instance.id,
"user_id": instance.user_id,
"course_id": instance.course_id,
"text": instance.text,
}
Client().meilisearch_index.add_documents([document])


@receiver(signals.post_delete, sender=Note)
def on_note_delete(sender, instance, **kwargs): # pylint: disable=unused-argument
"""
Delete a document.
"""
Client().meilisearch_index.delete_document(instance.id)


def heartbeat():
"""
Check that the meilisearch client is healthy.
"""
if not Client().meilisearch_client.is_healthy():
raise SearchViewRuntimeError("meilisearch")


def selftest():
"""
Check that we can access the meilisearch index.
"""
try:
return {"meilisearch": Client().meilisearch_index.created_at}
except meilisearch.errors.MeilisearchError as e:
raise SearchViewRuntimeError(
{"meilisearch_error": traceback.format_exc()}
) from e
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ elasticsearch-dsl
django-elasticsearch-dsl
django-elasticsearch-dsl-drf
django-cors-headers
meilisearch
mysqlclient
PyJWT
gunicorn # MIT
Expand Down
16 changes: 15 additions & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
#
# make upgrade
#
annotated-types==0.7.0
# via pydantic
asgiref==3.8.1
# via django
attrs==24.2.0
# via
# jsonschema
# referencing
camel-converter[pydantic]==4.0.1
# via meilisearch
certifi==2024.8.30
# via
# elasticsearch
Expand Down Expand Up @@ -103,6 +107,8 @@ jsonschema==4.23.0
# via drf-spectacular
jsonschema-specifications==2023.12.1
# via jsonschema
meilisearch==0.31.5
# via -r requirements/base.in
mysqlclient==2.2.4
# via -r requirements/base.in
newrelic==10.0.0
Expand All @@ -123,6 +129,10 @@ psutil==6.0.0
# via edx-django-utils
pycparser==2.22
# via cffi
pydantic==2.9.2
# via camel-converter
pydantic-core==2.23.4
# via pydantic
pyjwt[crypto]==2.9.0
# via
# -r requirements/base.in
Expand Down Expand Up @@ -152,6 +162,7 @@ requests==2.25.0
# -c requirements/constraints.txt
# -r requirements/base.in
# edx-drf-extensions
# meilisearch
rpds-py==0.20.0
# via
# jsonschema
Expand All @@ -172,7 +183,10 @@ stevedore==5.3.0
# edx-django-utils
# edx-opaque-keys
typing-extensions==4.12.2
# via edx-opaque-keys
# via
# edx-opaque-keys
# pydantic
# pydantic-core
uritemplate==4.1.1
# via drf-spectacular
urllib3==1.26.20
Expand Down
21 changes: 21 additions & 0 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#
# make upgrade
#
annotated-types==0.7.0
# via
# -r requirements/base.txt
# pydantic
asgiref==3.8.1
# via
# -r requirements/base.txt
Expand All @@ -18,6 +22,10 @@ attrs==24.2.0
# -r requirements/base.txt
# jsonschema
# referencing
camel-converter[pydantic]==4.0.1
# via
# -r requirements/base.txt
# meilisearch
certifi==2024.8.30
# via
# -r requirements/base.txt
Expand Down Expand Up @@ -171,6 +179,8 @@ lazy-object-proxy==1.10.0
# via astroid
markupsafe==2.1.5
# via jinja2
meilisearch==0.31.5
# via -r requirements/base.txt
more-itertools==5.0.0
# via
# -c requirements/constraints.txt
Expand Down Expand Up @@ -215,6 +225,14 @@ pycparser==2.22
# via
# -r requirements/base.txt
# cffi
pydantic==2.9.2
# via
# -r requirements/base.txt
# camel-converter
pydantic-core==2.23.4
# via
# -r requirements/base.txt
# pydantic
pygments==2.18.0
# via diff-cover
pyjwt[crypto]==2.9.0
Expand Down Expand Up @@ -269,6 +287,7 @@ requests==2.25.0
# -c requirements/constraints.txt
# -r requirements/base.txt
# edx-drf-extensions
# meilisearch
rpds-py==0.20.0
# via
# -r requirements/base.txt
Expand Down Expand Up @@ -309,6 +328,8 @@ typing-extensions==4.12.2
# -r requirements/base.txt
# edx-opaque-keys
# faker
# pydantic
# pydantic-core
uritemplate==4.1.1
# via
# -r requirements/base.txt
Expand Down

0 comments on commit 5a9de41

Please sign in to comment.