diff --git a/app/models.py b/app/models.py index b4d3fda4b9f0563394814cb97af4647a76cfdafd..fc88ba2ece8bdb5371cb8c88ad93e1130f81d3cb 100644 --- a/app/models.py +++ b/app/models.py @@ -50,17 +50,27 @@ class MateriManager(models.Manager): return SoftDeletionQuerySet(self.model) def search(self, search_text): - search_vector = search.SearchVector("title", weight="A") + search_vector = None + for field, weight in Materi.SEARCH_INDEX: + if search_vector is None: + search_vector = search.SearchVector(field, weight=weight) + else: + search_vector += search.SearchVector(field, weight=weight) + search_query = search.SearchQuery(search_text) search_rank = search.SearchRank(search_vector, search_query) search_result = ( - self.get_queryset().filter(_search_vector=search_query).annotate(rank=search_rank).order_by("-rank") + self.get_queryset() + .filter(_search_vector=search_query) + .annotate(rank=search_rank) + .order_by("-rank") ) return search_result + class SoftDeletionQuerySet(models.query.QuerySet): def delete(self): return super(SoftDeletionQuerySet, self).update(deleted_at=timezone.now()) @@ -95,13 +105,32 @@ class Materi(SoftDeleteModel): _search_vector = search.SearchVectorField(null=True, editable=False) + SEARCH_INDEX = ( + ("title", "A"), + ("author", "A"), + ("publisher", "C"), + ("descriptions", "C"), + ("uploader", "D"), + ) + objects = MateriManager() def save(self, *args, **kwargs): super().save(*args, **kwargs) + search_index = {field: weight for (field, weight) in Materi.SEARCH_INDEX} + if "update_fields" in kwargs: + is_search_index_updated = bool( + set(search_index.keys()) & set(kwargs["update_fields"]) + ) + + if ("update_fields" not in kwargs) or (is_search_index_updated): + self._search_vector = None + for field, weight in search_index.items(): + if self._search_vector is None: + self._search_vector = search.SearchVector(field, weight=weight) + else: + self._search_vector += search.SearchVector(field, weight=weight) - if "update_fields" not in kwargs or "_search_vector" not in kwargs["update_fields"]: - self._search_vector = search.SearchVector("title", weight="A") self.save(update_fields=["_search_vector"]) @property diff --git a/app/services.py b/app/services.py index d9b68cda0ab63f9e3709193987191dc69c311d10..d5990d1504b661f9aa1b4c88383acf5da9c92438 100644 --- a/app/services.py +++ b/app/services.py @@ -29,16 +29,7 @@ class DafterKatalogService: @staticmethod def search_materi(get_search, lst_materi, url): url = url + "&search={0}".format(get_search) - lst_materi = ( - lst_materi.search(get_search) - .filter( - Q(author__icontains=get_search) - | Q(uploader__name__icontains=get_search) - | Q(descriptions__icontains=get_search) - | Q(publisher__icontains=get_search) - ) - .distinct() - ) + lst_materi = lst_materi.search(get_search) return lst_materi, url @staticmethod diff --git a/app/tests.py b/app/tests.py index 83c8354be11574dd6d62197496718e5e75adfbd3..c71588983703b869f038359fd35eb75e12ff58cd 100644 --- a/app/tests.py +++ b/app/tests.py @@ -89,52 +89,6 @@ class DaftarKatalogTest(TestCase): resp = Materi.objects.get(id=materi.id) self.assertEqual(resp, materi) - def test_materi_model_generate_search_vector_after_save(self): - Materi(title="Eating book").save() - - search_vector_new_materi = list(Materi.objects.values_list("_search_vector", flat=True)) - expected_search_vector = ["'book':2A 'eat':1A"] - - self.assertSequenceEqual(search_vector_new_materi, expected_search_vector) - - def test_search_text_on_empty_database(self): - search_query = "test" - - search_result = list(Materi.objects.search(search_query)) - expected_search_result = [] - - self.assertSequenceEqual(search_result, expected_search_result) - - def test_search_text_on_unmatched_data(self): - Materi(title="test satu sekarang").save() - Materi(title="test dua nanti").save() - - search_query = "besok" - - search_result = list(Materi.objects.search(search_query)) - expected_search_result = [] - - self.assertSequenceEqual(search_result, expected_search_result) - - def test_search_text_return_list_matched_by_rank(self): - materi_2 = Materi(title="ini lumayan cocok lumayan cocok") - materi_2.save() - - materi_1 = Materi(title="ini sangat cocok sangat cocok sangat cocok") - materi_1.save() - - materi_4 = Materi(title="ini tidak") - materi_4.save() - - materi_3 = Materi(title="ini sedikit cocok") - materi_3.save() - - search_query = "ini cocok" - - search_result = list(Materi.objects.search(search_query)) - expected_search_result = [materi_1, materi_2, materi_3] - - self.assertSequenceEqual(search_result, expected_search_result) class DaftarKatalogSortingByJumlahUnduhTest(TestCase): def setUp(self): @@ -3631,6 +3585,151 @@ class MateriRecommendationTest(TestCase): list = [int(id) for id in re.findall(r"Materi\s(\d+)[^\d]", response.content.decode())] self.assertEqual(list, [1, 2]) + +class MateriSearchVectorTest(TestCase): + def setUp(self): + Materi.SEARCH_INDEX = (("title", "A"), ("author", "B")) + + def test_search_vector_constructed_on_create(self): + materi = Materi(title="Buku 1", author="Pembuat 1") + materi.save() + + search_vector_string = list( + Materi.objects.values_list("_search_vector", flat=True) + )[0] + + self.assertGreaterEqual(len(search_vector_string.split(",")), 1) + + def test_search_vector_based_on_indexed_attribute(self): + materi = Materi(title="Buku 1", author="Pembuat 1", descriptions="Deskripsi 1") + materi.save() + + search_vector_string = list( + Materi.objects.values_list("_search_vector", flat=True) + )[0] + self.assertIn("buku", search_vector_string) + self.assertIn("pembuat", search_vector_string) + + def test_search_vector_not_based_on_unindexed_attribute(self): + materi = Materi(title="Buku 1", author="Pembuat 1", descriptions="Deskripsi 1") + materi.save() + + search_vector_string = list( + Materi.objects.values_list("_search_vector", flat=True) + )[0] + + self.assertNotIn("deskripsi", search_vector_string) + + def test_search_vector_reconstructed_on_update_indexed_field(self): + materi = Materi(title="Sebelum reconstruct") + materi.save() + + search_vector = list(Materi.objects.values_list("_search_vector", flat=True))[0] + + materi.title = "Setelah reconstruct" + materi.save() + + search_vector = list(Materi.objects.values_list("_search_vector", flat=True))[0] + + self.assertIn("setelah", search_vector) + + def test_search_vector_not_reconstructed_on_update_unindexed_field(self): + materi = Materi(descriptions="sebelum reconstruct") + materi.save() + + search_vector = list(Materi.objects.values_list("_search_vector", flat=True))[0] + + materi.descriptions = "sebelum reconstruct" + materi.save() + + search_vector = list(Materi.objects.values_list("_search_vector", flat=True))[0] + + self.assertNotIn("setelah", search_vector) + + +class MateriSearchTest(TestCase): + def test_empty_result_on_empty_table(self): + search_query = "test" + + search_result = list(Materi.objects.search(search_query)) + expected_search_result = [] + + self.assertSequenceEqual(search_result, expected_search_result) + + def test_empty_result_on_unmatched_data(self): + Materi.SEARCH_INDEX = (("title", "A"), ("author", "B")) + + Materi(title="buku 1", author="bapak 1").save() + Materi(title="artikel 2", author="ibu 1").save() + + search_query = "majalah" + + search_result = list(Materi.objects.search(search_query)) + expected_search_result = [] + + self.assertSequenceEqual(search_result, expected_search_result) + + search_query = "kakak" + + search_result = list(Materi.objects.search(search_query)) + expected_search_result = [] + + self.assertSequenceEqual(search_result, expected_search_result) + + def test_correct_rank_on_result_tested_by_similiarity_words(self): + Materi.SEARCH_INDEX = (("descriptions", "A"),) + materi_2 = Materi(descriptions="ini lumayan cocok lumayan cocok") + materi_2.save() + + materi_1 = Materi(descriptions="ini sangat cocok sangat cocok sangat cocok") + materi_1.save() + + materi_4 = Materi(descriptions="ini tidak") + materi_4.save() + + materi_3 = Materi(descriptions="ini sedikit cocok") + materi_3.save() + + search_query = "ini cocok" + + search_result = list(Materi.objects.search(search_query)) + expected_search_result = [materi_1, materi_2, materi_3] + + self.assertSequenceEqual(search_result, expected_search_result) + + def test_correct_rank_on_result_tested_by_weight(self): + Materi.SEARCH_INDEX = ( + ("title", "A"), + ("author", "C"), + ("descriptions", "B"), + ("publisher", "D"), + ) + + materi_title = Materi(title="cocok") + materi_title.save() + + materi_author = Materi(author="cocok cocok cocok") + materi_author.save() + + materi_descriptions = Materi(descriptions="cocok cocok") + materi_descriptions.save() + + materi_publisher = Materi(publisher="cocok cocok cocok cocok") + materi_publisher.save() + + search_query = "cocok" + + search_result = list(Materi.objects.search(search_query)) + expected_search_result = [ + materi_title, + materi_descriptions, + materi_author, + materi_publisher, + ] + + self.assertSequenceEqual(search_result, expected_search_result) + + class BacaNantiTest(TestCase): def setUp(self): self.contributor_credential = { diff --git a/digipus/settings.py b/digipus/settings.py index a5c78be362117bb65ba17279f5f85e274c8304d8..7b0b547da75fd98b6085d6ba54cd700c68fc4d70 100644 --- a/digipus/settings.py +++ b/digipus/settings.py @@ -190,4 +190,4 @@ EMAIL_PORT = config('EMAIL_PORT', default=587) # use Google Mail SMTP as default EMAIL_HOST_USER = config('EMAIL_HOST_USER', default="pmplclass2020@gmail.com") EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default="pmpldigipusemail") EMAIL_USE_TLS = True -EMAIL_USE_SSL = False \ No newline at end of file +EMAIL_USE_SSL = False