diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 9473dabbe427..4262b8a7b13f 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -467,6 +467,29 @@ def delete_index_doc(usage_key: UsageKey) -> None: _wait_for_meili_tasks(tasks) +def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: + """ + Deletes draft documents for the given XBlocks from the search index + """ + current_rebuild_index_name = _get_running_rebuild_index_name() + client = _get_meilisearch_client() + # Delete all documents where last_published is null i.e. never published before. + delete_filter = [ + f'{Fields.context_key}="{library_key}"', + # This field should only be NULL or have a value, but we're also checking IS EMPTY just in case. + # Inner arrays are connected by an OR + [f'{Fields.last_published} IS EMPTY', f'{Fields.last_published} IS NULL'], + ] + + tasks = [] + if current_rebuild_index_name: + # If there is a rebuild in progress, the documents will also be deleted from the new index. + tasks.append(client.index(current_rebuild_index_name).delete_documents(filter=delete_filter)) + tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(filter=delete_filter)) + + _wait_for_meili_tasks(tasks) + + def upsert_library_block_index_doc(usage_key: UsageKey) -> None: """ Creates or updates the document for the given Library Block in the search index diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 06ea3e777c61..dfd603776981 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -9,9 +9,9 @@ from celery import shared_task from celery_utils.logged_task import LoggedTask from edx_django_utils.monitoring import set_code_owner_attribute +from meilisearch.errors import MeilisearchError from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 -from meilisearch.errors import MeilisearchError from . import api @@ -81,3 +81,6 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) api.upsert_content_library_index_docs(library_key) + # Delete all documents in this library that were not published by above function + # as this task is also triggered on discard event. + api.delete_all_draft_docs_for_library(library_key) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index 9dcdfb76b4a6..e8616cee60a8 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -388,3 +388,18 @@ def test_index_content_library_metadata(self, mock_meilisearch): mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with( [self.doc_problem1, self.doc_problem2] ) + + @override_settings(MEILISEARCH_ENABLED=True) + def test_delete_all_drafts(self, mock_meilisearch): + """ + Test deleting all draft documents from the index. + """ + api.delete_all_draft_docs_for_library(self.library.key) + + delete_filter = [ + f'context_key="{self.library.key}"', + ['last_published IS EMPTY', 'last_published IS NULL'], + ] + mock_meilisearch.return_value.index.return_value.delete_documents.assert_called_once_with( + filter=delete_filter + )