From 8d4d8e0f65a37bb7647fbe312e3259df7ea0071d Mon Sep 17 00:00:00 2001 From: mdonlon Date: Mon, 28 Oct 2024 10:30:40 -0400 Subject: [PATCH 1/8] initial dote-dev branch push --- app/configurations/models.py | 8 +++++++- app/es_api/utils/queries.py | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/configurations/models.py b/app/configurations/models.py index 78d56bf..13a969d 100644 --- a/app/configurations/models.py +++ b/app/configurations/models.py @@ -178,12 +178,18 @@ class CourseInformationMapping(TimeStampedModel): " elasticsearch") course_derived_from = models.CharField(max_length=200, - default="P2881_Core.DerivedFrom", + default="P2881-Core.DerivedFrom", help_text="Enter the mapping for " "the reference to the " "course derived from found in the" " elasticsearch") + course_competency = models.CharField(max_length=200, + default="p2881_course_profile.Competencies_Asserted", + help_text="Enter the mapping for the " + "reference to the competency the " + "course is aligned to") + xds_ui_configuration = models \ .OneToOneField(XDSUIConfiguration, on_delete=models.CASCADE, diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index 1b70ccd..70b8328 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -85,6 +85,7 @@ def search_by_keyword(self, keyword="", filters={}): course_mapping.course_code, course_mapping.course_provider, course_mapping.course_instructor, course_mapping.course_deliveryMode, + course_mapping.course_competency, 'Course.CourseTitle', 'Course.ShortDescription', 'Course.CourseCode', 'Course.CourseProviderName' ] From db11b95d901ebabc5ab5eb00f8061450b29baf97 Mon Sep 17 00:00:00 2001 From: mdonlon Date: Wed, 6 Nov 2024 13:12:09 -0500 Subject: [PATCH 2/8] added new query (similar courses) --- app/es_api/urls.py | 2 ++ app/es_api/utils/queries.py | 45 +++++++++++++++++++++++++++++++++--- app/es_api/views.py | 46 ++++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/app/es_api/urls.py b/app/es_api/urls.py index c3593fa..dbdd75d 100644 --- a/app/es_api/urls.py +++ b/app/es_api/urls.py @@ -15,4 +15,6 @@ name='search-derived'), path('teaches/', views.SearchCompetencyView.as_view(), name='search-competency'), + path('similar-courses//', views.GetSimilarCoursesView. + as_view(), name='get-similar-courses') ] diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index 3b5a977..8e3f503 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -13,7 +13,6 @@ logger = logging.getLogger('dict_config_logger') - class XSEQueries(BaseQueries): def get_page_start(self, page_number, page_size): @@ -86,8 +85,9 @@ def search_by_keyword(self, keyword="", filters={}): course_mapping.course_instructor, course_mapping.course_deliveryMode, course_mapping.course_competency, - 'Course.CourseTitle', 'Course.ShortDescription', - 'Course.CourseCode', 'Course.CourseProviderName' + 'Supplemental_Ledger.Frequency Offered', + 'Supplemental_Ledger.Course Proficiency Level', + 'p2881-core.Subject' ] q = Q("multi_match", @@ -212,6 +212,45 @@ def more_like_this(self, doc_id): return response + def similar_courses(self, keyword=""): + """This method takes in a doc ID and queries the elasticsearch index for + courses with similar competencies or titles""" + + course_mapping = CourseInformationMapping.objects.first() + fields = [ + course_mapping.course_competency, + 'p2881-core.Subject' + ] + + # We're going to match based only on two fields + q = Q("multi_match", + query=keyword, + fields=fields) + + # setting up the search object + self.search = self.search.query(q) + + self.user_organization_filtering() + + # getting the page size for result pagination + configuration = XDSConfiguration.objects.first() + uiConfig = configuration.xdsuiconfiguration + search_filters = SearchFilter.objects.filter( + xds_ui_configuration=uiConfig, active=True) + + # create aggregations for each filter + self.add_search_aggregations(filter_set=search_filters) + + # Skipping first response found and getting next three + # if len(self.search > 3): + self.search = self.search[1:4] + + # call to elasticsearch to execute the query + response = self.search.execute() + logger.info(self.search.to_dict()) + + return response + def spotlight_courses(self): """This method queries elasticsearch for courses with ids matching the ids of stored CourseSpotlight objects that are active""" diff --git a/app/es_api/views.py b/app/es_api/views.py index c56c6df..55bb849 100644 --- a/app/es_api/views.py +++ b/app/es_api/views.py @@ -208,7 +208,8 @@ def get(self, request): class GetMoreLikeThisView(APIView): """This method defines an API for fetching results using the - more_like_this feature from elasticsearch. """ + more_like_this feature from elasticsearch for + more like this courses section of UI. """ def get(self, request, doc_id): results = [] @@ -239,6 +240,49 @@ def get(self, request, doc_id): return HttpResponse(results, content_type="application/json") +class GetSimilarCoursesView(APIView): + """This method defines an API for fetching results using the + more_like_this feature from elasticsearch for similar courses. """ + + def get(self, request, key): + results = [] + + if key != '': + errorMsg = { + "message": "error executing ElasticSearch query; " + + "Please contact an administrator" + } + errorMsgJSON = json.dumps(errorMsg) + + try: + + queries = XSEQueries( + XDSConfiguration.objects.first().target_xse_host, + XDSConfiguration.objects.first().target_xse_index, + user=request.user) + response = queries.similar_courses( + keyword=key) + results = queries.get_results(response) + except HTTPError as http_err: + logger.error(http_err) + return HttpResponseServerError(errorMsgJSON, + content_type="application/json") + except Exception as err: + logger.error(err) + return HttpResponseServerError(errorMsgJSON, + content_type="application/json") + else: + logger.info(results) + return HttpResponse(results, content_type="application/json") + else: + error = { + "message": "Request is missing 'key' query paramater" + } + errorJson = json.dumps(error) + return HttpResponseBadRequest(errorJson, + content_type="application/json") + + class FiltersView(APIView): """This method defines an API for performing a filter search""" From 0c32b839d04ee0389187307db19e13b4b2f248bd Mon Sep 17 00:00:00 2001 From: mdonlon Date: Fri, 8 Nov 2024 15:23:25 -0500 Subject: [PATCH 3/8] added search field config + added subject to course mappings --- app/configurations/admin.py | 6 ++-- ...einformationmapping_course_derived_from.py | 18 +++++++++++ ...courseinformationmapping_course_subject.py | 18 +++++++++++ app/configurations/models.py | 5 +++ app/core/admin.py | 8 ++++- app/core/migrations/0009_searchfield.py | 32 +++++++++++++++++++ app/core/models.py | 22 +++++++++++++ app/es_api/utils/queries.py | 8 ++--- 8 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 app/configurations/migrations/0011_alter_courseinformationmapping_course_derived_from.py create mode 100644 app/configurations/migrations/0012_courseinformationmapping_course_subject.py create mode 100644 app/core/migrations/0009_searchfield.py diff --git a/app/configurations/admin.py b/app/configurations/admin.py index 05858a1..aee42a0 100644 --- a/app/configurations/admin.py +++ b/app/configurations/admin.py @@ -32,11 +32,13 @@ class CourseInformationMappingAdmin(admin.ModelAdmin): 'course_type', 'course_time', 'course_instructor', 'course_deliveryMode', 'course_thumbnail', 'course_derived_from', - 'course_competency', 'xds_ui_configuration') + 'course_competency', 'course_subject', + 'xds_ui_configuration') fields = ['course_title', 'course_description', 'course_url', 'course_code', 'course_startDate', 'course_endDate', 'course_provider', 'course_type', 'course_time', 'course_instructor', 'course_deliveryMode', 'course_thumbnail', 'course_derived_from', - 'course_competency', 'xds_ui_configuration'] + 'course_competency', 'course_subject', + 'xds_ui_configuration'] diff --git a/app/configurations/migrations/0011_alter_courseinformationmapping_course_derived_from.py b/app/configurations/migrations/0011_alter_courseinformationmapping_course_derived_from.py new file mode 100644 index 0000000..4eb156f --- /dev/null +++ b/app/configurations/migrations/0011_alter_courseinformationmapping_course_derived_from.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-08 18:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('configurations', '0010_merge_20241026_0049'), + ] + + operations = [ + migrations.AlterField( + model_name='courseinformationmapping', + name='course_derived_from', + field=models.CharField(default='P2881-Core.DerivedFrom', help_text='Enter the mapping for the reference to the course derived from found in the elasticsearch', max_length=200), + ), + ] diff --git a/app/configurations/migrations/0012_courseinformationmapping_course_subject.py b/app/configurations/migrations/0012_courseinformationmapping_course_subject.py new file mode 100644 index 0000000..e20ce22 --- /dev/null +++ b/app/configurations/migrations/0012_courseinformationmapping_course_subject.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-11-08 20:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('configurations', '0011_alter_courseinformationmapping_course_derived_from'), + ] + + operations = [ + migrations.AddField( + model_name='courseinformationmapping', + name='course_subject', + field=models.CharField(default='p2881-core.Subject', help_text='Enter the mapping for the course subject', max_length=200), + ), + ] diff --git a/app/configurations/models.py b/app/configurations/models.py index bddf605..a052a87 100644 --- a/app/configurations/models.py +++ b/app/configurations/models.py @@ -211,6 +211,11 @@ class CourseInformationMapping(TimeStampedModel): "the reference to the " "competency taught") + course_subject = models.CharField(max_length=200, + default="p2881-core.Subject", + help_text="Enter the mapping for " + "the course subject") + xds_ui_configuration = models \ .OneToOneField(XDSUIConfiguration, on_delete=models.CASCADE, diff --git a/app/core/admin.py b/app/core/admin.py index 1ea793a..bf5ae6f 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -1,6 +1,6 @@ from core.models import (CourseDetailHighlight, CourseSpotlight, Experience, InterestList, SavedFilter, SearchFilter, - SearchSortOption) + SearchSortOption, SearchField) from django.contrib import admin @@ -11,6 +11,12 @@ class SearchFilterAdmin(admin.ModelAdmin): fields = [('display_name', 'field_name', 'xds_ui_configuration', 'filter_type', 'active',)] +@admin.register(SearchField) +class SearchFieldAdmin(admin.ModelAdmin): + list_display = ('display_name', 'field_name', 'xds_ui_configuration', + 'active', 'created', 'modified',) + fields = [('display_name', 'field_name', 'xds_ui_configuration', + 'active',)] @admin.register(SearchSortOption) class SearchSortOptionAdmin(admin.ModelAdmin): diff --git a/app/core/migrations/0009_searchfield.py b/app/core/migrations/0009_searchfield.py new file mode 100644 index 0000000..e17871c --- /dev/null +++ b/app/core/migrations/0009_searchfield.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.16 on 2024-11-08 18:40 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('configurations', '0011_alter_courseinformationmapping_course_derived_from'), + ('core', '0008_alter_coursedetailhighlight_rank'), + ] + + operations = [ + migrations.CreateModel( + name='SearchField', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('display_name', models.CharField(help_text='Enter the display name of the field to search by on', max_length=200)), + ('field_name', models.CharField(help_text='Enter the metadata field name as displayed in Elasticsearch e.g. course.title', max_length=200)), + ('active', models.BooleanField(default=True)), + ('xds_ui_configuration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='configurations.xdsuiconfiguration')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/core/models.py b/app/core/models.py index f6f1c81..103ac7f 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -38,7 +38,29 @@ def __str__(self): """String for representing the Model object.""" return f'{self.id}' +class SearchField(TimeStampedModel): + """Model to add aditional fields to search by""" + display_name = models.CharField( + max_length=200, + help_text='Enter the display name of the field to search by on') + field_name = models.CharField( + max_length=200, + help_text='Enter the metadata field name as displayed in Elasticsearch' + ' e.g. course.title' + ) + xds_ui_configuration = models.ForeignKey(XDSUIConfiguration, + on_delete=models.CASCADE) + active = models.BooleanField(default=True) + + def get_absolute_url(self): + """ URL for displaying individual model records.""" + return reverse('Configuration-detail', args=[str(self.id)]) + + def __str__(self): + """String for representing the Model object.""" + return f'{self.id}' + class SearchSortOption(TimeStampedModel): """Model to contain options for sorting search results""" diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index 8e3f503..ec44e17 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -3,7 +3,7 @@ import logging from configurations.models import CourseInformationMapping, XDSConfiguration -from core.models import CourseSpotlight, SearchFilter, SearchSortOption +from core.models import CourseSpotlight, SearchFilter, SearchSortOption, SearchField from django.core.exceptions import ObjectDoesNotExist from elasticsearch_dsl import A, Document, Q from elasticsearch_dsl.query import MoreLikeThis @@ -85,9 +85,7 @@ def search_by_keyword(self, keyword="", filters={}): course_mapping.course_instructor, course_mapping.course_deliveryMode, course_mapping.course_competency, - 'Supplemental_Ledger.Frequency Offered', - 'Supplemental_Ledger.Course Proficiency Level', - 'p2881-core.Subject' + *SearchField.objects.filter(active=True).values_list('field_name', flat=True) ] q = Q("multi_match", @@ -219,7 +217,7 @@ def similar_courses(self, keyword=""): course_mapping = CourseInformationMapping.objects.first() fields = [ course_mapping.course_competency, - 'p2881-core.Subject' + course_mapping.course_subject ] # We're going to match based only on two fields From c3c5497f38c9984e54293ac3e9401573c62eb1c4 Mon Sep 17 00:00:00 2001 From: mdonlon Date: Fri, 8 Nov 2024 16:23:05 -0500 Subject: [PATCH 4/8] fixed coverage report errors --- app/core/admin.py | 4 +++- app/core/models.py | 4 +++- app/es_api/utils/queries.py | 9 ++++++--- app/es_api/views.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/core/admin.py b/app/core/admin.py index bf5ae6f..91802c7 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -11,12 +11,14 @@ class SearchFilterAdmin(admin.ModelAdmin): fields = [('display_name', 'field_name', 'xds_ui_configuration', 'filter_type', 'active',)] + @admin.register(SearchField) class SearchFieldAdmin(admin.ModelAdmin): list_display = ('display_name', 'field_name', 'xds_ui_configuration', 'active', 'created', 'modified',) fields = [('display_name', 'field_name', 'xds_ui_configuration', - 'active',)] + 'active',)] + @admin.register(SearchSortOption) class SearchSortOptionAdmin(admin.ModelAdmin): diff --git a/app/core/models.py b/app/core/models.py index 103ac7f..4ae6547 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -38,6 +38,7 @@ def __str__(self): """String for representing the Model object.""" return f'{self.id}' + class SearchField(TimeStampedModel): """Model to add aditional fields to search by""" display_name = models.CharField( @@ -60,7 +61,8 @@ def get_absolute_url(self): def __str__(self): """String for representing the Model object.""" return f'{self.id}' - + + class SearchSortOption(TimeStampedModel): """Model to contain options for sorting search results""" diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index ec44e17..a72281b 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -3,7 +3,8 @@ import logging from configurations.models import CourseInformationMapping, XDSConfiguration -from core.models import CourseSpotlight, SearchFilter, SearchSortOption, SearchField +from core.models import CourseSpotlight, SearchFilter +from core.models import SearchSortOption, SearchField from django.core.exceptions import ObjectDoesNotExist from elasticsearch_dsl import A, Document, Q from elasticsearch_dsl.query import MoreLikeThis @@ -13,6 +14,7 @@ logger = logging.getLogger('dict_config_logger') + class XSEQueries(BaseQueries): def get_page_start(self, page_number, page_size): @@ -85,7 +87,8 @@ def search_by_keyword(self, keyword="", filters={}): course_mapping.course_instructor, course_mapping.course_deliveryMode, course_mapping.course_competency, - *SearchField.objects.filter(active=True).values_list('field_name', flat=True) + *SearchField.objects.filter( + active=True).values_list('field_name', flat=True) ] q = Q("multi_match", @@ -224,7 +227,7 @@ def similar_courses(self, keyword=""): q = Q("multi_match", query=keyword, fields=fields) - + # setting up the search object self.search = self.search.query(q) diff --git a/app/es_api/views.py b/app/es_api/views.py index 55bb849..aa708b3 100644 --- a/app/es_api/views.py +++ b/app/es_api/views.py @@ -208,7 +208,7 @@ def get(self, request): class GetMoreLikeThisView(APIView): """This method defines an API for fetching results using the - more_like_this feature from elasticsearch for + more_like_this feature from elasticsearch for more like this courses section of UI. """ def get(self, request, doc_id): From eac6dd7186d7a5a6be923f74d701eb00d3165ebf Mon Sep 17 00:00:00 2001 From: mdonlon Date: Fri, 8 Nov 2024 16:27:24 -0500 Subject: [PATCH 5/8] final code test fix --- app/es_api/utils/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index a72281b..8c0586c 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -3,7 +3,7 @@ import logging from configurations.models import CourseInformationMapping, XDSConfiguration -from core.models import CourseSpotlight, SearchFilter +from core.models import CourseSpotlight, SearchFilter from core.models import SearchSortOption, SearchField from django.core.exceptions import ObjectDoesNotExist from elasticsearch_dsl import A, Document, Q From 495cbc5814f353d278ec2449df7ffccedd387b24 Mon Sep 17 00:00:00 2001 From: mdonlon Date: Wed, 13 Nov 2024 16:21:06 -0500 Subject: [PATCH 6/8] comment and import fix --- app/es_api/utils/queries.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index 8c0586c..3051182 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -3,8 +3,8 @@ import logging from configurations.models import CourseInformationMapping, XDSConfiguration -from core.models import CourseSpotlight, SearchFilter -from core.models import SearchSortOption, SearchField +from core.models import (CourseSpotlight, SearchFilter, + SearchSortOption, SearchField) from django.core.exceptions import ObjectDoesNotExist from elasticsearch_dsl import A, Document, Q from elasticsearch_dsl.query import MoreLikeThis @@ -214,8 +214,8 @@ def more_like_this(self, doc_id): return response def similar_courses(self, keyword=""): - """This method takes in a doc ID and queries the elasticsearch index for - courses with similar competencies or titles""" + """This method takes in a keyword and queries the elasticsearch index + for 3 courses with similar competnencies or subjects""" course_mapping = CourseInformationMapping.objects.first() fields = [ @@ -231,16 +231,16 @@ def similar_courses(self, keyword=""): # setting up the search object self.search = self.search.query(q) - self.user_organization_filtering() + # self.user_organization_filtering() - # getting the page size for result pagination - configuration = XDSConfiguration.objects.first() - uiConfig = configuration.xdsuiconfiguration - search_filters = SearchFilter.objects.filter( - xds_ui_configuration=uiConfig, active=True) + # # getting the page size for result pagination + # configuration = XDSConfiguration.objects.first() + # uiConfig = configuration.xdsuiconfiguration + # search_filters = SearchFilter.objects.filter( + # xds_ui_configuration=uiConfig, active=True) - # create aggregations for each filter - self.add_search_aggregations(filter_set=search_filters) + # # create aggregations for each filter + # self.add_search_aggregations(filter_set=search_filters) # Skipping first response found and getting next three # if len(self.search > 3): From 5bbcac59833a0801f77f317d1fa76d16310b72ac Mon Sep 17 00:00:00 2001 From: mdonlon Date: Wed, 13 Nov 2024 16:31:45 -0500 Subject: [PATCH 7/8] code check fix --- app/es_api/utils/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index 3051182..e79e7e3 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -214,7 +214,7 @@ def more_like_this(self, doc_id): return response def similar_courses(self, keyword=""): - """This method takes in a keyword and queries the elasticsearch index + """This method takes in a keyword and queries the elasticsearch index for 3 courses with similar competnencies or subjects""" course_mapping = CourseInformationMapping.objects.first() From e9d24c5fce0e196849e33679ab66f80c9f025eaf Mon Sep 17 00:00:00 2001 From: mdonlon Date: Thu, 14 Nov 2024 10:36:49 -0500 Subject: [PATCH 8/8] addressing comments --- app/es_api/utils/queries.py | 18 ++++-------------- app/es_api/views.py | 4 ++-- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/es_api/utils/queries.py b/app/es_api/utils/queries.py index e79e7e3..830f9b6 100644 --- a/app/es_api/utils/queries.py +++ b/app/es_api/utils/queries.py @@ -215,7 +215,7 @@ def more_like_this(self, doc_id): def similar_courses(self, keyword=""): """This method takes in a keyword and queries the elasticsearch index - for 3 courses with similar competnencies or subjects""" + for 4 courses with similar competencies or subjects""" course_mapping = CourseInformationMapping.objects.first() fields = [ @@ -231,20 +231,10 @@ def similar_courses(self, keyword=""): # setting up the search object self.search = self.search.query(q) - # self.user_organization_filtering() - - # # getting the page size for result pagination - # configuration = XDSConfiguration.objects.first() - # uiConfig = configuration.xdsuiconfiguration - # search_filters = SearchFilter.objects.filter( - # xds_ui_configuration=uiConfig, active=True) - - # # create aggregations for each filter - # self.add_search_aggregations(filter_set=search_filters) + self.user_organization_filtering() - # Skipping first response found and getting next three - # if len(self.search > 3): - self.search = self.search[1:4] + # sending back 4 responses + self.search = self.search[0:4] # call to elasticsearch to execute the query response = self.search.execute() diff --git a/app/es_api/views.py b/app/es_api/views.py index aa708b3..7895b10 100644 --- a/app/es_api/views.py +++ b/app/es_api/views.py @@ -241,8 +241,8 @@ def get(self, request, doc_id): class GetSimilarCoursesView(APIView): - """This method defines an API for fetching results using the - more_like_this feature from elasticsearch for similar courses. """ + """This method defines an API for fetching results by sending key words + to elasticsearch and looking for similar courses. """ def get(self, request, key): results = []