Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions openedx/core/djangoapps/content/search/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from meilisearch import Client as MeilisearchClient
from meilisearch.errors import MeilisearchApiError, MeilisearchError
from meilisearch.models.task import TaskInfo
from opaque_keys import OpaqueKey
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2, LibraryCollectionLocator
from openedx_learning.api import authoring as authoring_api
Expand All @@ -42,7 +43,7 @@
searchable_doc_for_collection,
searchable_doc_for_container,
searchable_doc_for_library_block,
searchable_doc_for_usage_key,
searchable_doc_for_key,
searchable_doc_collections,
searchable_doc_tags,
searchable_doc_tags_for_collection,
Expand Down Expand Up @@ -486,8 +487,7 @@ def index_container_batch(batch, num_done, library_key) -> int:
container,
)
doc = searchable_doc_for_container(container_key)
# TODO: when we add container tags
# doc.update(searchable_doc_tags_for_container(container_key))
doc.update(searchable_doc_tags(container_key))
docs.append(doc)
except Exception as err: # pylint: disable=broad-except
status_cb(f"Error indexing container {container.key}: {err}")
Expand All @@ -511,7 +511,7 @@ def index_container_batch(batch, num_done, library_key) -> int:
collections = authoring_api.get_collections(library.learning_package_id, enabled=True)
num_collections = collections.count()
num_collections_done = 0
status_cb(f"{num_collections_done + 1}/{num_collections}. Now indexing collections in library {lib_key}")
status_cb(f"{num_collections_done}/{num_collections}. Now indexing collections in library {lib_key}")
paginator = Paginator(collections, 100)
for p in paginator.page_range:
num_collections_done = index_collection_batch(
Expand Down Expand Up @@ -620,7 +620,7 @@ def delete_index_doc(usage_key: UsageKey) -> None:
Args:
usage_key (UsageKey): The usage key of the XBlock to be removed from the index
"""
doc = searchable_doc_for_usage_key(usage_key)
doc = searchable_doc_for_key(usage_key)
_delete_index_doc(doc[Fields.id])


Expand Down Expand Up @@ -811,12 +811,12 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None:
_update_index_docs(docs)


def upsert_block_tags_index_docs(usage_key: UsageKey):
def upsert_content_object_tags_index_doc(key: OpaqueKey):
"""
Updates the tags data in documents for the given Course/Library block
Updates the tags data in document for the given Course/Library item
"""
doc = {Fields.id: meili_id_from_opaque_key(usage_key)}
doc.update(searchable_doc_tags(usage_key))
doc = {Fields.id: meili_id_from_opaque_key(key)}
doc.update(searchable_doc_tags(key))
_update_index_docs([doc])


Expand Down
29 changes: 15 additions & 14 deletions openedx/core/djangoapps/content/search/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from django.core.exceptions import ObjectDoesNotExist
from django.utils.text import slugify
from opaque_keys import OpaqueKey
from opaque_keys.edx.keys import LearningContextKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2
from openedx_learning.api import authoring as authoring_api
Expand Down Expand Up @@ -113,7 +114,7 @@ class PublishStatus:
modified = "modified"


def meili_id_from_opaque_key(usage_key: UsageKey) -> str:
def meili_id_from_opaque_key(key: OpaqueKey) -> 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 @@ -124,7 +125,7 @@ def meili_id_from_opaque_key(usage_key: UsageKey) -> str:
we could use PublishableEntity's primary key / UUID instead.
"""
# The slugified key _may_ not be unique so we append a hashed string to make it unique:
key_str = str(usage_key)
key_str = str(key)
key_bin = key_str.encode()

suffix = blake2b(key_bin, digest_size=4, usedforsecurity=False).hexdigest()
Expand All @@ -140,12 +141,12 @@ def _meili_access_id_from_context_key(context_key: LearningContextKey) -> int:
return access.id


def searchable_doc_for_usage_key(usage_key: UsageKey) -> dict:
def searchable_doc_for_key(key: OpaqueKey) -> dict:
"""
Generates a base document identified by its usage key.
Generates a base document identified by its opaque key.
"""
return {
Fields.id: meili_id_from_opaque_key(usage_key),
Fields.id: meili_id_from_opaque_key(key),
}


Expand Down Expand Up @@ -244,7 +245,7 @@ class implementation returns only:
return block_data


def _tags_for_content_object(object_id: UsageKey | LearningContextKey) -> dict:
def _tags_for_content_object(object_id: OpaqueKey) -> dict:
"""
Given an XBlock, course, library, etc., get the tag data for its index doc.

Expand Down Expand Up @@ -406,7 +407,7 @@ def searchable_doc_for_library_block(xblock_metadata: lib_api.LibraryXBlockMetad
block_published = None
publish_status = PublishStatus.never

doc = searchable_doc_for_usage_key(xblock_metadata.usage_key)
doc = searchable_doc_for_key(xblock_metadata.usage_key)
doc.update({
Fields.type: DocType.library_block,
Fields.breadcrumbs: [],
Expand All @@ -427,13 +428,13 @@ def searchable_doc_for_library_block(xblock_metadata: lib_api.LibraryXBlockMetad
return doc


def searchable_doc_tags(usage_key: UsageKey) -> dict:
def searchable_doc_tags(key: OpaqueKey) -> dict:
"""
Generate a dictionary document suitable for ingestion into a search engine
like Meilisearch or Elasticsearch, with the tags data for the given content object.
"""
doc = searchable_doc_for_usage_key(usage_key)
doc.update(_tags_for_content_object(usage_key))
doc = searchable_doc_for_key(key)
doc.update(_tags_for_content_object(key))

return doc

Expand All @@ -443,7 +444,7 @@ def searchable_doc_collections(usage_key: UsageKey) -> dict:
Generate a dictionary document suitable for ingestion into a search engine
like Meilisearch or Elasticsearch, with the collections data for the given content object.
"""
doc = searchable_doc_for_usage_key(usage_key)
doc = searchable_doc_for_key(usage_key)
doc.update(_collections_for_content_object(usage_key))

return doc
Expand All @@ -461,7 +462,7 @@ def searchable_doc_tags_for_collection(
library_key,
collection_key,
)
doc = searchable_doc_for_usage_key(collection_usage_key)
doc = searchable_doc_for_key(collection_usage_key)
doc.update(_tags_for_content_object(collection_usage_key))

return doc
Expand All @@ -473,7 +474,7 @@ def searchable_doc_for_course_block(block) -> dict:
like Meilisearch or Elasticsearch, so that the given course block can be
found using faceted search.
"""
doc = searchable_doc_for_usage_key(block.usage_key)
doc = searchable_doc_for_key(block.usage_key)
doc.update({
Fields.type: DocType.course_block,
})
Expand Down Expand Up @@ -503,7 +504,7 @@ def searchable_doc_for_collection(
collection_key,
)

doc = searchable_doc_for_usage_key(collection_usage_key)
doc = searchable_doc_for_key(collection_usage_key)

try:
collection = collection or lib_api.get_library_collection_from_usage_key(collection_usage_key)
Expand Down
19 changes: 12 additions & 7 deletions openedx/core/djangoapps/content/search/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.dispatch import receiver
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locator import LibraryCollectionLocator
from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryContainerLocator
from openedx_events.content_authoring.data import (
ContentLibraryData,
ContentObjectChangedData,
Expand Down Expand Up @@ -41,7 +41,7 @@
from .api import (
only_if_meilisearch_enabled,
upsert_block_collections_index_docs,
upsert_block_tags_index_docs,
upsert_content_object_tags_index_doc,
upsert_collection_tags_index_docs,
)
from .tasks import (
Expand Down Expand Up @@ -211,23 +211,28 @@ def content_object_associations_changed_handler(**kwargs) -> None:
return

try:
# Check if valid if course or library block
# Check if valid course or library block
usage_key = UsageKey.from_string(str(content_object.object_id))
except InvalidKeyError:
try:
# Check if valid if library collection
# Check if valid library collection
usage_key = LibraryCollectionLocator.from_string(str(content_object.object_id))
except InvalidKeyError:
log.error("Received invalid content object id")
return
try:
# Check if valid library container
usage_key = LibraryContainerLocator.from_string(str(content_object.object_id))
except InvalidKeyError:
# Invalid content object id
log.error("Received invalid content object id")
return

# This event's changes may contain both "tags" and "collections", but this will happen rarely, if ever.
# So we allow a potential double "upsert" here.
if not content_object.changes or "tags" in content_object.changes:
if isinstance(usage_key, LibraryCollectionLocator):
upsert_collection_tags_index_docs(usage_key)
else:
upsert_block_tags_index_docs(usage_key)
upsert_content_object_tags_index_doc(usage_key)
if not content_object.changes or "collections" in content_object.changes:
upsert_block_collections_index_docs(usage_key)

Expand Down
46 changes: 43 additions & 3 deletions openedx/core/djangoapps/content/search/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ def setUp(self):
"modified": created_date.timestamp(),
"access_id": lib_access.id,
"breadcrumbs": [{"display_name": "Library"}],
# "tags" should be here but we haven't implemented them yet
Comment thread
pomegranited marked this conversation as resolved.
# "published" is not set since we haven't published it yet
}

Expand Down Expand Up @@ -262,6 +261,7 @@ def test_reindex_meilisearch(self, mock_meilisearch):
doc_collection = copy.deepcopy(self.collection_dict)
doc_collection["tags"] = {}
doc_unit = copy.deepcopy(self.unit_dict)
doc_unit["tags"] = {}

api.rebuild_index()
assert mock_meilisearch.return_value.index.return_value.add_documents.call_count == 4
Expand Down Expand Up @@ -292,6 +292,7 @@ def test_reindex_meilisearch_incremental(self, mock_meilisearch):
doc_collection = copy.deepcopy(self.collection_dict)
doc_collection["tags"] = {}
doc_unit = copy.deepcopy(self.unit_dict)
doc_unit["tags"] = {}

api.rebuild_index(incremental=True)
assert mock_meilisearch.return_value.index.return_value.add_documents.call_count == 4
Expand Down Expand Up @@ -472,8 +473,7 @@ def test_index_xblock_tags(self, mock_meilisearch):
"""
Test indexing an XBlock with tags.
"""

# Tag XBlock (these internally call `upsert_block_tags_index_docs`)
# Tag XBlock (these internally call `upsert_content_object_tags_index_doc`)
tagging_api.tag_object(str(self.sequential.usage_key), self.taxonomyA, ["one", "two"])
tagging_api.tag_object(str(self.sequential.usage_key), self.taxonomyB, ["three", "four"])

Expand Down Expand Up @@ -866,3 +866,43 @@ def test_delete_collection(self, mock_meilisearch):
mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with([
doc_problem_without_collection,
])

@override_settings(MEILISEARCH_ENABLED=True)
def test_index_library_container_metadata(self, mock_meilisearch):
"""
Test indexing a Library Container.
"""
api.upsert_library_container_index_doc(self.unit.container_key)

mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with([self.unit_dict])

@override_settings(MEILISEARCH_ENABLED=True)
def test_index_tags_in_containers(self, mock_meilisearch):
# Tag collection
tagging_api.tag_object(self.unit_key, self.taxonomyA, ["one", "two"])
tagging_api.tag_object(self.unit_key, self.taxonomyB, ["three", "four"])

# Build expected docs with tags at each stage
doc_unit_with_tags1 = {
"id": "lctorg1libunitunit-1-e4527f7c",
"tags": {
'taxonomy': ['A'],
'level0': ['A > one', 'A > two']
}
}
doc_unit_with_tags2 = {
"id": "lctorg1libunitunit-1-e4527f7c",
"tags": {
'taxonomy': ['A', 'B'],
'level0': ['A > one', 'A > two', 'B > four', 'B > three']
}
}

assert mock_meilisearch.return_value.index.return_value.update_documents.call_count == 2
mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls(
[
call([doc_unit_with_tags1]),
call([doc_unit_with_tags2]),
],
any_order=True,
)
Loading