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 Nov 12, 2024
1 parent dbfffdf commit 71b63c8
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 157 deletions.
96 changes: 96 additions & 0 deletions notesapi/v1/tests/test_meilisearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from unittest.mock import Mock, patch

from django.test import TestCase

from notesapi.v1.models import Note
from notesapi.v1.views import meilisearch


class MeilisearchTest(TestCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
meilisearch.connect_signals()

@classmethod
def tearDownClass(cls):
super().tearDownClass()
meilisearch.disconnect_signals()

def setUp(self):
self.enterContext(
patch.object(meilisearch.Client, "meilisearch_client", Mock())
)
self.enterContext(patch.object(meilisearch.Client, "meilisearch_index", Mock()))

@property
def note_dict(self):
return {
"user": "test_user_id",
"usage_id": "i4x://org/course/html/52aa9816425a4ce98a07625b8cb70811",
"course_id": "org/course/run",
"text": "test note text",
"quote": "test note quote",
"ranges": [
{
"start": "/p[1]",
"end": "/p[1]",
"startOffset": 0,
"endOffset": 10,
}
],
"tags": ["apple", "pear"],
}

def test_save_delete_note(self):
note = Note.create(self.note_dict)
note.save()
note_id = note.id

meilisearch.Client.meilisearch_index.add_documents.assert_called_with(
[
{
"id": note_id,
"user_id": "test_user_id",
"course_id": "org/course/run",
"text": "test note text",
}
]
)

note.delete()
meilisearch.Client.meilisearch_index.delete_document.assert_called_with(note_id)

def test_get_queryset_no_result(self):
queryset = meilisearch.AnnotationSearchView().get_queryset()
assert not queryset.all()

def test_get_queryset_one_match(self):
note1 = Note.create(self.note_dict)
note2 = Note.create(self.note_dict)
note1.save()
note2.save()
view = meilisearch.AnnotationSearchView()
view.params = {
"text": "dummy text",
"user": "someuser",
"course_id": "course/id",
"page_size": 10,
"page": 2,
}
with patch.object(
meilisearch.Client.meilisearch_index,
"search",
Mock(return_value={"hits": [{"id": note2.id}]}),
) as mock_search:
queryset = view.get_queryset()
mock_search.assert_called_once_with(
"dummy text",
{
"offset": 10,
"limit": 10,
"filter": ["user_id = 'someuser'", "course_id = 'course/id'"],
},
)
assert [note2.id] == [note.id for note in queryset]
4 changes: 2 additions & 2 deletions notesapi/v1/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.urls import path, re_path

from notesapi.v1.views import (AnnotationDetailView, AnnotationListView,
AnnotationRetireView, get_views_module)
AnnotationRetireView, get_annotation_search_view_class)
app_name = "notesapi.v1"
urlpatterns = [
path('annotations/', AnnotationListView.as_view(), name='annotations'),
Expand All @@ -13,7 +13,7 @@
),
path(
'search/',
get_views_module().AnnotationSearchView.as_view(),
get_annotation_search_view_class().as_view(),
name='annotations_search'
),
]
19 changes: 13 additions & 6 deletions notesapi/v1/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import typing as t
from django.conf import settings

from .common import (
AnnotationDetailView,
AnnotationListView,
AnnotationRetireView,
AnnotationSearchView
)

from .exceptions import SearchViewRuntimeError

def get_views_module():

# pylint: disable=import-outside-toplevel
def get_annotation_search_view_class() -> t.Type[AnnotationSearchView]:
"""
Import views from either mysql or elasticsearch backend
Import views from either mysql, elasticsearch or meilisearch backend
"""
if settings.ES_DISABLED:
from . import common as backend_module
else:
from . import elasticsearch as backend_module
return backend_module
if getattr(settings, "MEILISEARCH_ENABLED", False):
from . import meilisearch
return meilisearch.AnnotationSearchView
else:
return AnnotationSearchView
from . import elasticsearch
return elasticsearch.AnnotationSearchView
Loading

0 comments on commit 71b63c8

Please sign in to comment.