Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update search index when course content is updated (TEMP) #645

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 455 additions & 0 deletions openedx/core/djangoapps/content/search/api.py

Large diffs are not rendered by default.

62 changes: 43 additions & 19 deletions openedx/core/djangoapps/content/search/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging

from django.utils.text import slugify
from opaque_keys.edx.keys import UsageKey, LearningContextKey
from opaque_keys.edx.keys import LearningContextKey, UsageKey

from openedx.core.djangoapps.content_libraries import api as lib_api
from openedx.core.djangoapps.content_tagging import api as tagging_api
Expand Down Expand Up @@ -62,7 +62,7 @@ class DocType:
library_block = "library_block"


def _meili_id_from_opaque_key(usage_key: UsageKey) -> str:
def meili_id_from_opaque_key(usage_key: UsageKey) -> str:
"""
Meilisearch requires each document to have a primary key that's either an
integer or a string composed of alphanumeric characters (a-z A-Z 0-9),
Expand All @@ -88,7 +88,6 @@ class implementation returns only:
{"content": {"display_name": "..."}, "content_type": "..."}
"""
block_data = {
Fields.id: _meili_id_from_opaque_key(block.usage_key),
Fields.usage_key: str(block.usage_key),
Fields.block_id: str(block.usage_key.block_id),
Fields.display_name: xblock_api.get_block_display_name(block),
Expand Down Expand Up @@ -196,33 +195,58 @@ def _tags_for_content_object(object_id: UsageKey | LearningContextKey) -> dict:
return {Fields.tags: result}


def searchable_doc_for_library_block(metadata: lib_api.LibraryXBlockMetadata) -> dict:
def searchable_doc_for_library_block(
xblock_metadata: lib_api.LibraryXBlockMetadata, include_metadata: bool = True, include_tags: bool = True
) -> dict:
"""
Generate a dictionary document suitable for ingestion into a search engine
like Meilisearch or Elasticsearch, so that the given library block can be
found using faceted search.

Args:
xblock_metadata: The XBlock component metadata to index
include_metadata: If True, include the block's metadata in the doc
include_tags: If True, include the block's tags in the doc
"""
library_name = lib_api.get_library(metadata.usage_key.context_key).title
doc = {}
try:
block = xblock_api.load_block(metadata.usage_key, user=None)
except Exception as err: # pylint: disable=broad-except
log.exception(f"Failed to load XBlock {metadata.usage_key}: {err}")
doc.update(_fields_from_block(block))
doc.update(_tags_for_content_object(metadata.usage_key))
doc[Fields.type] = DocType.library_block
# Add the breadcrumbs. In v2 libraries, the library itself is not a "parent" of the XBlocks so we add it here:
doc[Fields.breadcrumbs] = [{"display_name": library_name}]
library_name = lib_api.get_library(xblock_metadata.usage_key.context_key).title
block = xblock_api.load_block(xblock_metadata.usage_key, user=None)

doc = {
Fields.id: meili_id_from_opaque_key(xblock_metadata.usage_key),
Fields.type: DocType.library_block,
}

if include_metadata:
doc.update(_fields_from_block(block))
# Add the breadcrumbs. In v2 libraries, the library itself is not a "parent" of the XBlocks so we add it here:
doc[Fields.breadcrumbs] = [{"display_name": library_name}]

if include_tags:
doc.update(_tags_for_content_object(xblock_metadata.usage_key))

return doc


def searchable_doc_for_course_block(block) -> dict:
def searchable_doc_for_course_block(block, include_metadata: bool = True, include_tags: bool = True) -> dict:
"""
Generate a dictionary document suitable for ingestion into a search engine
like Meilisearch or Elasticsearch, so that the given course block can be
found using faceted search.

Args:
block: The XBlock instance to index
include_metadata: If True, include the block's metadata in the doc
include_tags: If True, include the block's tags in the doc
"""
doc = _fields_from_block(block)
doc.update(_tags_for_content_object(block.usage_key))
doc[Fields.type] = DocType.course_block
doc = {
Fields.id: meili_id_from_opaque_key(block.usage_key),
Fields.type: DocType.course_block,
}

if include_metadata:
doc.update(_fields_from_block(block))

if include_tags:
doc.update(_tags_for_content_object(block.usage_key))

return doc
106 changes: 106 additions & 0 deletions openedx/core/djangoapps/content/search/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Handlers for content indexing
"""

import logging

from django.dispatch import receiver
from openedx_events.content_authoring.data import LibraryBlockData, XBlockData
from openedx_events.content_authoring.signals import (
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_DELETED,
LIBRARY_BLOCK_UPDATED,
XBLOCK_CREATED,
XBLOCK_DELETED,
XBLOCK_UPDATED
)

from .api import only_if_meilisearch_enabled
from .tasks import (
delete_library_block_index_doc,
delete_xblock_index_doc,
upsert_library_block_index_doc,
upsert_xblock_index_doc
)

log = logging.getLogger(__name__)


@receiver(XBLOCK_CREATED)
@only_if_meilisearch_enabled
def xblock_created_handler(**kwargs) -> None:
"""
Create the index for the XBlock
"""
xblock_info = kwargs.get("xblock_info", None)
if not xblock_info or not isinstance(xblock_info, XBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

upsert_xblock_index_doc.delay(
str(xblock_info.usage_key),
recursive=False,
update_metadata=True,
update_tags=False,
)


@receiver(XBLOCK_UPDATED)
@only_if_meilisearch_enabled
def xblock_updated_handler(**kwargs) -> None:
"""
Update the index for the XBlock and its children
"""
xblock_info = kwargs.get("xblock_info", None)
if not xblock_info or not isinstance(xblock_info, XBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

upsert_xblock_index_doc.delay(
str(xblock_info.usage_key),
recursive=True, # Update all children because the breadcrumb may have changed
update_metadata=True,
update_tags=False,
)


@receiver(XBLOCK_DELETED)
@only_if_meilisearch_enabled
def xblock_deleted_handler(**kwargs) -> None:
"""
Delete the index for the XBlock
"""
xblock_info = kwargs.get("xblock_info", None)
if not xblock_info or not isinstance(xblock_info, XBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

delete_xblock_index_doc.delay(str(xblock_info.usage_key))


@receiver(LIBRARY_BLOCK_CREATED)
@only_if_meilisearch_enabled
def content_library_updated_handler(**kwargs) -> None:
"""
Create or update the index for the content library block
"""
library_block_data = kwargs.get("library_block", None)
if not library_block_data or not isinstance(library_block_data, LibraryBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

upsert_library_block_index_doc.delay(str(library_block_data.usage_key), update_metadata=True, update_tags=False)


@receiver(LIBRARY_BLOCK_DELETED)
@only_if_meilisearch_enabled
def content_library_deleted_handler(**kwargs) -> None:
"""
Delete the index for the content library block
"""
library_block_data = kwargs.get("library_block", None)
if not library_block_data or not isinstance(library_block_data, LibraryBlockData): # pragma: no cover
log.error("Received null or incorrect data for event")
return

delete_library_block_index_doc.delay(str(library_block_data.usage_key))

This file was deleted.

Loading
Loading