From 93cf23311e3367b656e392f0288b120b4d35cbf6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:44:35 -0400 Subject: [PATCH 1/9] Update dependency litellm to v1.83.0 [SECURITY] (#3213) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f04451cc20..23dce1a7a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "langchain>=0.3.11,<0.4", "langchain-experimental>=0.3.4,<0.4", "langchain-openai>=0.3.2,<0.4", - "litellm==1.81.13", + "litellm==1.83.0", "llama-index>=0.14.0,<0.15", "llama-index-llms-openai>=0.6.0,<0.7", "lxml>=6.0.0,<7", diff --git a/uv.lock b/uv.lock index 25c925a54a..f2d46e1925 100644 --- a/uv.lock +++ b/uv.lock @@ -2139,7 +2139,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.81.13" +version = "1.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2155,9 +2155,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/80/b6cb799e7100953d848e106d0575db34c75bc3b57f31f2eefdfb1e23655f/litellm-1.81.13.tar.gz", hash = "sha256:083788d9c94e3371ff1c42e40e0e8198c497772643292a65b1bc91a3b3b537ea", size = 16562861, upload-time = "2026-02-17T02:00:47.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/f3/fffb7932870163cea7addc392165647a9a8a5489967de486c854226f1141/litellm-1.81.13-py3-none-any.whl", hash = "sha256:ae4aea2a55e85993f5f6dd36d036519422d24812a1a3e8540d9e987f2d7a4304", size = 14587505, upload-time = "2026-02-17T02:00:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, ] [[package]] @@ -2699,7 +2699,7 @@ requires-dist = [ { name = "langchain-experimental", specifier = ">=0.3.4,<0.4" }, { name = "langchain-litellm", specifier = ">=0.5.1" }, { name = "langchain-openai", specifier = ">=0.3.2,<0.4" }, - { name = "litellm", specifier = "==1.81.13" }, + { name = "litellm", specifier = "==1.83.0" }, { name = "llama-index", specifier = ">=0.14.0,<0.15" }, { name = "llama-index-llms-openai", specifier = ">=0.6.0,<0.7" }, { name = "lxml", specifier = ">=6.0.0,<7" }, From 320bb76e6b23ef52f0056f28f41f61aafc48db69 Mon Sep 17 00:00:00 2001 From: Ahtesham Quraish Date: Thu, 16 Apr 2026 13:53:56 +0500 Subject: [PATCH 2/9] fix: minor improvements on video collection pages (#3205) Co-authored-by: Ahtesham Quraish --- .../FeaturedVideo.tsx | 10 ++---- .../VideoPlaylistCollectionPage/VideoCard.tsx | 8 ++--- .../VideoDetailPage.tsx | 34 ++++++++++++++++++- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx index 3ba3d82805..f329052083 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx @@ -19,7 +19,7 @@ const FeaturedGrid = styled.div(({ theme }) => ({ display: "grid", gridTemplateColumns: "3.95fr 4.6fr", columnGap: "40px", - alignItems: "flex-start", + alignItems: "center", [theme.breakpoints.down("sm")]: { gridTemplateColumns: "1fr", }, @@ -60,14 +60,10 @@ const FeaturedTitle = styled.h2(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, color: theme.custom.colors.darkGray2, letterSpacing: "-1.28px", - lineHeight: "110%", + lineHeight: "120%", margin: "0 0 16px", cursor: "pointer", - fontSize: "64px", - overflow: "hidden", - display: "-webkit-box", - WebkitLineClamp: 2, - WebkitBoxOrient: "vertical", + fontSize: "48px", transition: "color 0.2s", "& .mobile-title": { display: "none", diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx index 41429b7b76..386f04429b 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx @@ -22,11 +22,11 @@ const VideoCardItem = styled(Link)({ }, "&:hover .play-overlay": { - opacity: 1, + opacity: 0.5, }, "&:focus-visible .play-overlay": { - opacity: 1, + opacity: 0.5, }, [theme.breakpoints.down("sm")]: { @@ -108,8 +108,8 @@ const CardTitle = styled(Typography)(({ theme }) => ({ })) const PlayIcon = styled(RiPlayCircleFill)({ - width: 48, - height: 48, + width: 36, + height: 36, }) const CardMetaRow = styled.div({ diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx index daff1d30a4..2cbde3d7f3 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx @@ -7,7 +7,7 @@ import Image from "next/image" import { useFeatureFlagEnabled } from "posthog-js/react" import { Typography, styled, theme, Skeleton, Breadcrumbs } from "ol-components" import VideoContainer from "./VideoContainer" -import { RiShareForwardFill } from "@remixicon/react" +import { RiShareForwardFill, RiPlayCircleFill } from "@remixicon/react" import { useQuery } from "@tanstack/react-query" import { useLearningResourcesDetail, @@ -76,6 +76,11 @@ const StyledBreadcrumbs = styled(Breadcrumbs)(() => ({ "& > span > span": { paddingBottom: 0, paddingLeft: "4px" }, })) +const PlayIcon = styled(RiPlayCircleFill)({ + width: 36, + height: 36, +}) + const ContentArea = styled.div(({ theme }) => ({ padding: "56px 0 80px", [theme.breakpoints.down("sm")]: { @@ -237,6 +242,18 @@ const MoreFromItem = styled(Link)({ textDecoration: "none", "&:hover .mf-title": { color: theme.custom.colors.red }, + "&:hover .video-card-title, &:focus-visible .video-card-title": { + color: theme.custom.colors.red, + }, + + "&:hover .play-overlay": { + opacity: 0.5, + }, + + "&:focus-visible .play-overlay": { + opacity: 0.5, + }, + "&:first-child": { padding: "0 0 24px 0", }, @@ -346,6 +363,18 @@ const DurationText = styled.span(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, })) +const PlayOverlay = styled.div({ + position: "absolute", + inset: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#fff", + opacity: 0, + transition: "opacity 0.2s", + backgroundColor: "rgba(0, 0, 0, 0.18)", +}) + const ScreenReaderOnly = styled.span({ position: "absolute", width: 1, @@ -660,6 +689,9 @@ const VideoDetailPage: React.FC = ({ {itemDuration && ( {itemDuration} )} + + + From daed199e5d5a3ef28c8bfcc1f3c0bd70a692bcda Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Thu, 16 Apr 2026 09:06:25 -0400 Subject: [PATCH 3/9] Filtering for similarity endpoints (#3204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(vector_search): extract LearningResourcesSearchFiltersSerializer base Split LearningResourcesVectorSearchRequestSerializer into a base class containing the filter fields that map to QDRANT_RESOURCE_PARAM_MAP (resource_type, platform, topic, etc.) and a subclass that adds the search-specific fields (q, offset, limit, hybrid_search). The base is reused by the vector_similar endpoint in a follow-up commit. Co-Authored-By: Claude Opus 4.6 (1M context) * feat(learning_resources_search): thread query_filter through similarity pipeline Adds an optional query_filter kwarg to get_similar_resources, get_similar_resources_qdrant, and _qdrant_similar_results. The filter is forwarded to client.query_points(), letting callers narrow similarity results using Qdrant FieldConditions. Existing callers pass no filter, so behavior is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) * feat(learning_resources): support filters on vector_similar endpoint Wires LearningResourcesSearchFiltersSerializer into the vector_similar action so query params like resource_type, platform, topic, etc. are validated and translated to a Qdrant query_filter via qdrant_query_conditions. Declares the serializer on @extend_schema so the generated OpenAPI spec matches the endpoint's actual behavior. Closes mitodl/hq#10896 Co-Authored-By: Claude Opus 4.6 (1M context) * refactor(vector_search): move readable_id out of shared filter base serializer readable_id has a distinct role in LearningResourcesVectorSearchRequestSerializer (identifying the query resource by readable_id rather than by Qdrant point id) rather than being a result filter. Moving it back to the subclass keeps LearningResourcesSearchFiltersSerializer as a clean set of result-only filters and prevents vector_similar from exposing readable_id as a filter param. Regenerate OpenAPI spec and TypeScript client. Co-Authored-By: Claude Sonnet 4.6 * fix(learning_resources): exempt vector_similar from LearningResourceFilter in schema The ViewSet inherits filter_backends = [MultipleOptionsFilterBackend] and filterset_class = LearningResourceFilter from BaseLearningResourceViewSet. drf-spectacular was advertising all LearningResourceFilter fields (resource_id, sortby, readable_id, etc.) on vector_similar even though that action ignores them entirely. Override filter_backends as a property to return [] for vector_similar so the generated schema only shows the Qdrant filter params that the endpoint actually uses. Regenerate OpenAPI spec and TypeScript client. Co-Authored-By: Claude Sonnet 4.6 * feat(learning_resources): add filter support to OpenSearch similarity endpoint Both similar (OpenSearch) and vector_similar (Qdrant) endpoints now accept the same LearningResourcesSearchFiltersSerializer filter params. Each backend handles translation internally: - OpenSearch path uses generate_filter_clauses() to add filter clauses to the MoreLikeThis bool query - Qdrant path uses qdrant_query_conditions() inside get_similar_resources_qdrant (moved from the view layer) The shared dispatcher get_similar_resources() now accepts filter_params (raw validated dict) instead of a pre-translated query_filter, making the interface symmetric across backends. Also exempts the similar action from LearningResourceFilter in the schema, so both endpoints advertise only the filters they actually support. Regenerate OpenAPI spec and TypeScript client. Co-Authored-By: Claude Sonnet 4.6 * fix(learning_resources): fix boolean filter handling on OpenSearch similarity path generate_filter_clauses expects list values, but ArrayWrappedBoolean fields (free, professional, certification) produce scalar booleans from validated_data. Scalar True would raise TypeError when iterated; scalar False would be silently skipped by the truthiness guard. Fix by normalizing scalar booleans to single-element lists before passing to generate_filter_clauses in get_similar_resources_opensearch. Also move url__isnull and title__isnull out of LearningResourcesSearchFiltersSerializer into LearningResourcesVectorSearchRequestSerializer — these are Qdrant-only filters (not in SEARCH_FILTERS) and should not be advertised on the OpenSearch-backed similar endpoint. Regenerate OpenAPI spec and TypeScript client. Co-Authored-By: Claude Sonnet 4.6 * fix(learning_resources): fix boolean filter handling on OpenSearch similarity path Wrap all OpenSearch filters in a bool/must dict rather than passing a bare Python list to the filter= kwarg. opensearch-dsl may not serialize a list correctly; the main search path (construct_search) uses the same bool/must pattern via post_filter("bool", must=list(filter_clauses.values())). This bug caused resource_type and other filters to be silently ignored on the /similar/ endpoint, returning unfiltered results. Co-Authored-By: Claude Sonnet 4.6 * remove skip nplusone * _clean_filter_params * fix: address PR review follow-ups - Correct similarity return type hints to QuerySet[LearningResource]\n- Clarify _qdrant_similar_results input_query docstring\n- Remove redundant query in similar()\n- Remove unnecessary django_db marker in serializer tests\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) --- frontends/api/src/generated/v1/api.ts | 576 +++++++--------- learning_resources/views.py | 44 +- learning_resources/views_test.py | 124 ++++ learning_resources_search/api.py | 89 ++- learning_resources_search/api_test.py | 103 +++ openapi/specs/v1.yaml | 920 ++++++++++++-------------- vector_search/serializers.py | 39 +- vector_search/serializers_test.py | 40 +- 8 files changed, 1039 insertions(+), 896 deletions(-) diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index ca87060fc9..392613c715 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -16443,47 +16443,43 @@ export const LearningResourcesApiAxiosParamCreator = function ( } }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ learningResourcesSimilarList: async ( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesSimilarListSortbyEnum, topic?: Array, options: RawAxiosRequestConfig = {}, ): Promise => { @@ -16540,6 +16536,10 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -16552,14 +16552,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } - if (readable_id) { - localVarQueryParameter["readable_id"] = readable_id - } - - if (resource_id) { - localVarQueryParameter["resource_id"] = resource_id - } - if (resource_type) { localVarQueryParameter["resource_type"] = resource_type } @@ -16568,10 +16560,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["resource_type_group"] = resource_type_group } - if (sortby !== undefined) { - localVarQueryParameter["sortby"] = sortby - } - if (topic) { localVarQueryParameter["topic"] = topic } @@ -16799,47 +16787,43 @@ export const LearningResourcesApiAxiosParamCreator = function ( } }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesVectorSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ learningResourcesVectorSimilarList: async ( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesVectorSimilarListSortbyEnum, topic?: Array, options: RawAxiosRequestConfig = {}, ): Promise => { @@ -16897,6 +16881,10 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -16909,14 +16897,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } - if (readable_id) { - localVarQueryParameter["readable_id"] = readable_id - } - - if (resource_id) { - localVarQueryParameter["resource_id"] = resource_id - } - if (resource_type) { localVarQueryParameter["resource_type"] = resource_type } @@ -16925,10 +16905,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["resource_type_group"] = resource_type_group } - if (sortby !== undefined) { - localVarQueryParameter["sortby"] = sortby - } - if (topic) { localVarQueryParameter["topic"] = topic } @@ -17287,47 +17263,43 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { )(axios, operationBasePath || basePath) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ async learningResourcesSimilarList( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesSimilarListSortbyEnum, topic?: Array, options?: RawAxiosRequestConfig, ): Promise< @@ -17347,14 +17319,12 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { free, level, limit, + ocw_topic, offered_by, platform, professional, - readable_id, - resource_id, resource_type, resource_type_group, - sortby, topic, options, ) @@ -17497,47 +17467,43 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { )(axios, operationBasePath || basePath) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesVectorSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ async learningResourcesVectorSimilarList( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesVectorSimilarListSortbyEnum, topic?: Array, options?: RawAxiosRequestConfig, ): Promise< @@ -17557,14 +17523,12 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { free, level, limit, + ocw_topic, offered_by, platform, professional, - readable_id, - resource_id, resource_type, resource_type_group, - sortby, topic, options, ) @@ -17751,7 +17715,7 @@ export const LearningResourcesApiFactory = function ( .then((request) => request(axios, basePath)) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {LearningResourcesApiLearningResourcesSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -17772,14 +17736,12 @@ export const LearningResourcesApiFactory = function ( requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -17841,7 +17803,7 @@ export const LearningResourcesApiFactory = function ( .then((request) => request(axios, basePath)) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {LearningResourcesApiLearningResourcesVectorSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -17862,14 +17824,12 @@ export const LearningResourcesApiFactory = function ( requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -18214,50 +18174,50 @@ export interface LearningResourcesApiLearningResourcesSimilarListRequest { readonly id: number /** - * + * True if the learning resource offers a certificate * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly certification?: boolean + readonly certification?: boolean | null /** - * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly certification_type?: Array /** - * Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features + * The course feature. Possible options are at api/v1/course_features/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly course_feature?: Array /** - * The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @type {Array>} + * The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @type {Array<'online' | 'hybrid' | 'in_person' | 'offline'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly delivery?: Array> + readonly delivery?: Array /** - * The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @type {Array<'1' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '2' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} + * The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @type {Array<'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly department?: Array /** - * The course/program is offered for free + * * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly free?: boolean + readonly free?: boolean | null /** - * The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory - * @type {Array<'advanced' | 'graduate' | 'high_school' | 'intermediate' | 'introductory' | 'noncredit' | 'undergraduate'>} + * + * @type {Array<'undergraduate' | 'graduate' | 'high_school' | 'noncredit' | 'advanced' | 'intermediate' | 'introductory'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly level?: Array @@ -18270,63 +18230,49 @@ export interface LearningResourcesApiLearningResourcesSimilarListRequest { readonly limit?: number /** - * The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @type {Array<'bootcamps' | 'climate' | 'mitpe' | 'mitx' | 'ocw' | 'see' | 'xpro'>} - * @memberof LearningResourcesApiLearningResourcesSimilarList - */ - readonly offered_by?: Array - - /** - * The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @type {Array<'bootcamps' | 'canvas' | 'climate' | 'csail' | 'ctl' | 'edx' | 'emeritus' | 'globalalumni' | 'mitpe' | 'mitxonline' | 'ocw' | 'oll' | 'ovs' | 'podcast' | 'scc' | 'see' | 'simplilearn' | 'susskind' | 'whu' | 'xpro' | 'youtube'>} + * The ocw topic name. + * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly platform?: Array + readonly ocw_topic?: Array /** - * - * @type {boolean} + * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see' | 'climate'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly professional?: boolean + readonly offered_by?: Array /** - * A unique text identifier for the resources - * @type {Array} + * The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @type {Array<'edx' | 'ocw' | 'oll' | 'mitxonline' | 'bootcamps' | 'xpro' | 'csail' | 'mitpe' | 'see' | 'scc' | 'ctl' | 'whu' | 'susskind' | 'globalalumni' | 'simplilearn' | 'emeritus' | 'podcast' | 'youtube' | 'canvas' | 'climate' | 'ovs'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly readable_id?: Array + readonly platform?: Array /** - * Comma-separated list of learning resource IDs - * @type {Array} + * + * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly resource_id?: Array + readonly professional?: boolean | null /** - * The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @type {Array<'course' | 'document' | 'learning_path' | 'podcast' | 'podcast_episode' | 'program' | 'video' | 'video_playlist'>} + * The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @type {Array<'course' | 'program' | 'learning_path' | 'podcast' | 'podcast_episode' | 'video' | 'video_playlist' | 'document'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly resource_type?: Array /** - * The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @type {Array<'course' | 'learning_material' | 'program'>} + * The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @type {Array<'course' | 'program' | 'learning_material'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly resource_type_group?: Array /** - * Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @type {'-id' | '-last_modified' | '-mitcoursenumber' | '-readable_id' | '-start_date' | '-views' | 'id' | 'last_modified' | 'mitcoursenumber' | 'new' | 'readable_id' | 'start_date' | 'upcoming' | 'views'} - * @memberof LearningResourcesApiLearningResourcesSimilarList - */ - readonly sortby?: LearningResourcesSimilarListSortbyEnum - - /** - * Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * The topic name. To see a list of options go to api/v1/topics/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ @@ -18508,52 +18454,50 @@ export interface LearningResourcesApiLearningResourcesVectorSimilarListRequest { readonly id: number /** - * + * True if the learning resource offers a certificate * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly certification?: boolean + readonly certification?: boolean | null /** - * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly certification_type?: Array /** - * Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features + * The course feature. Possible options are at api/v1/course_features/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly course_feature?: Array /** - * The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @type {Array>} + * The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @type {Array<'online' | 'hybrid' | 'in_person' | 'offline'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly delivery?: Array< - Array - > + readonly delivery?: Array /** - * The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @type {Array<'1' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '2' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} + * The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @type {Array<'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly department?: Array /** - * The course/program is offered for free + * * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly free?: boolean + readonly free?: boolean | null /** - * The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory - * @type {Array<'advanced' | 'graduate' | 'high_school' | 'intermediate' | 'introductory' | 'noncredit' | 'undergraduate'>} + * + * @type {Array<'undergraduate' | 'graduate' | 'high_school' | 'noncredit' | 'advanced' | 'intermediate' | 'introductory'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly level?: Array @@ -18566,63 +18510,49 @@ export interface LearningResourcesApiLearningResourcesVectorSimilarListRequest { readonly limit?: number /** - * The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @type {Array<'bootcamps' | 'climate' | 'mitpe' | 'mitx' | 'ocw' | 'see' | 'xpro'>} - * @memberof LearningResourcesApiLearningResourcesVectorSimilarList - */ - readonly offered_by?: Array - - /** - * The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @type {Array<'bootcamps' | 'canvas' | 'climate' | 'csail' | 'ctl' | 'edx' | 'emeritus' | 'globalalumni' | 'mitpe' | 'mitxonline' | 'ocw' | 'oll' | 'ovs' | 'podcast' | 'scc' | 'see' | 'simplilearn' | 'susskind' | 'whu' | 'xpro' | 'youtube'>} + * The ocw topic name. + * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly platform?: Array + readonly ocw_topic?: Array /** - * - * @type {boolean} + * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see' | 'climate'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly professional?: boolean + readonly offered_by?: Array /** - * A unique text identifier for the resources - * @type {Array} + * The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @type {Array<'edx' | 'ocw' | 'oll' | 'mitxonline' | 'bootcamps' | 'xpro' | 'csail' | 'mitpe' | 'see' | 'scc' | 'ctl' | 'whu' | 'susskind' | 'globalalumni' | 'simplilearn' | 'emeritus' | 'podcast' | 'youtube' | 'canvas' | 'climate' | 'ovs'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly readable_id?: Array + readonly platform?: Array /** - * Comma-separated list of learning resource IDs - * @type {Array} + * + * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly resource_id?: Array + readonly professional?: boolean | null /** - * The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @type {Array<'course' | 'document' | 'learning_path' | 'podcast' | 'podcast_episode' | 'program' | 'video' | 'video_playlist'>} + * The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @type {Array<'course' | 'program' | 'learning_path' | 'podcast' | 'podcast_episode' | 'video' | 'video_playlist' | 'document'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly resource_type?: Array /** - * The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @type {Array<'course' | 'learning_material' | 'program'>} + * The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @type {Array<'course' | 'program' | 'learning_material'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly resource_type_group?: Array /** - * Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @type {'-id' | '-last_modified' | '-mitcoursenumber' | '-readable_id' | '-start_date' | '-views' | 'id' | 'last_modified' | 'mitcoursenumber' | 'new' | 'readable_id' | 'start_date' | 'upcoming' | 'views'} - * @memberof LearningResourcesApiLearningResourcesVectorSimilarList - */ - readonly sortby?: LearningResourcesVectorSimilarListSortbyEnum - - /** - * Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * The topic name. To see a list of options go to api/v1/topics/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ @@ -18806,7 +18736,7 @@ export class LearningResourcesApi extends BaseAPI { } /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {LearningResourcesApiLearningResourcesSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -18828,14 +18758,12 @@ export class LearningResourcesApi extends BaseAPI { requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -18902,7 +18830,7 @@ export class LearningResourcesApi extends BaseAPI { } /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {LearningResourcesApiLearningResourcesVectorSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -18924,14 +18852,12 @@ export class LearningResourcesApi extends BaseAPI { requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -19153,10 +19079,10 @@ export type LearningResourcesListSortbyEnum = * @export */ export const LearningResourcesSimilarListCertificationTypeEnum = { - Completion: "completion", Micromasters: "micromasters", - None: "none", Professional: "professional", + Completion: "completion", + None: "none", } as const export type LearningResourcesSimilarListCertificationTypeEnum = (typeof LearningResourcesSimilarListCertificationTypeEnum)[keyof typeof LearningResourcesSimilarListCertificationTypeEnum] @@ -19176,6 +19102,14 @@ export type LearningResourcesSimilarListDeliveryEnum = */ export const LearningResourcesSimilarListDepartmentEnum = { _1: "1", + _2: "2", + _3: "3", + _4: "4", + _5: "5", + _6: "6", + _7: "7", + _8: "8", + _9: "9", _10: "10", _11: "11", _12: "12", @@ -19184,7 +19118,6 @@ export const LearningResourcesSimilarListDepartmentEnum = { _16: "16", _17: "17", _18: "18", - _2: "2", _20: "20", _21A: "21A", _21G: "21G", @@ -19193,13 +19126,6 @@ export const LearningResourcesSimilarListDepartmentEnum = { _21M: "21M", _22: "22", _24: "24", - _3: "3", - _4: "4", - _5: "5", - _6: "6", - _7: "7", - _8: "8", - _9: "9", Cc: "CC", CmsW: "CMS-W", Ec: "EC", @@ -19219,13 +19145,13 @@ export type LearningResourcesSimilarListDepartmentEnum = * @export */ export const LearningResourcesSimilarListLevelEnum = { - Advanced: "advanced", + Undergraduate: "undergraduate", Graduate: "graduate", HighSchool: "high_school", + Noncredit: "noncredit", + Advanced: "advanced", Intermediate: "intermediate", Introductory: "introductory", - Noncredit: "noncredit", - Undergraduate: "undergraduate", } as const export type LearningResourcesSimilarListLevelEnum = (typeof LearningResourcesSimilarListLevelEnum)[keyof typeof LearningResourcesSimilarListLevelEnum] @@ -19233,13 +19159,13 @@ export type LearningResourcesSimilarListLevelEnum = * @export */ export const LearningResourcesSimilarListOfferedByEnum = { - Bootcamps: "bootcamps", - Climate: "climate", - Mitpe: "mitpe", Mitx: "mitx", Ocw: "ocw", - See: "see", + Bootcamps: "bootcamps", Xpro: "xpro", + Mitpe: "mitpe", + See: "see", + Climate: "climate", } as const export type LearningResourcesSimilarListOfferedByEnum = (typeof LearningResourcesSimilarListOfferedByEnum)[keyof typeof LearningResourcesSimilarListOfferedByEnum] @@ -19247,27 +19173,27 @@ export type LearningResourcesSimilarListOfferedByEnum = * @export */ export const LearningResourcesSimilarListPlatformEnum = { - Bootcamps: "bootcamps", - Canvas: "canvas", - Climate: "climate", - Csail: "csail", - Ctl: "ctl", Edx: "edx", - Emeritus: "emeritus", - Globalalumni: "globalalumni", - Mitpe: "mitpe", - Mitxonline: "mitxonline", Ocw: "ocw", Oll: "oll", - Ovs: "ovs", - Podcast: "podcast", - Scc: "scc", + Mitxonline: "mitxonline", + Bootcamps: "bootcamps", + Xpro: "xpro", + Csail: "csail", + Mitpe: "mitpe", See: "see", - Simplilearn: "simplilearn", - Susskind: "susskind", + Scc: "scc", + Ctl: "ctl", Whu: "whu", - Xpro: "xpro", + Susskind: "susskind", + Globalalumni: "globalalumni", + Simplilearn: "simplilearn", + Emeritus: "emeritus", + Podcast: "podcast", Youtube: "youtube", + Canvas: "canvas", + Climate: "climate", + Ovs: "ovs", } as const export type LearningResourcesSimilarListPlatformEnum = (typeof LearningResourcesSimilarListPlatformEnum)[keyof typeof LearningResourcesSimilarListPlatformEnum] @@ -19276,13 +19202,13 @@ export type LearningResourcesSimilarListPlatformEnum = */ export const LearningResourcesSimilarListResourceTypeEnum = { Course: "course", - Document: "document", + Program: "program", LearningPath: "learning_path", Podcast: "podcast", PodcastEpisode: "podcast_episode", - Program: "program", Video: "video", VideoPlaylist: "video_playlist", + Document: "document", } as const export type LearningResourcesSimilarListResourceTypeEnum = (typeof LearningResourcesSimilarListResourceTypeEnum)[keyof typeof LearningResourcesSimilarListResourceTypeEnum] @@ -19291,35 +19217,14 @@ export type LearningResourcesSimilarListResourceTypeEnum = */ export const LearningResourcesSimilarListResourceTypeGroupEnum = { Course: "course", - LearningMaterial: "learning_material", Program: "program", + LearningMaterial: "learning_material", } as const export type LearningResourcesSimilarListResourceTypeGroupEnum = (typeof LearningResourcesSimilarListResourceTypeGroupEnum)[keyof typeof LearningResourcesSimilarListResourceTypeGroupEnum] /** * @export */ -export const LearningResourcesSimilarListSortbyEnum = { - Id: "-id", - LastModified: "-last_modified", - Mitcoursenumber: "-mitcoursenumber", - ReadableId: "-readable_id", - StartDate: "-start_date", - Views: "-views", - Id2: "id", - LastModified2: "last_modified", - Mitcoursenumber2: "mitcoursenumber", - New: "new", - ReadableId2: "readable_id", - StartDate2: "start_date", - Upcoming: "upcoming", - Views2: "views", -} as const -export type LearningResourcesSimilarListSortbyEnum = - (typeof LearningResourcesSimilarListSortbyEnum)[keyof typeof LearningResourcesSimilarListSortbyEnum] -/** - * @export - */ export const LearningResourcesSummaryListCertificationTypeEnum = { Completion: "completion", Micromasters: "micromasters", @@ -19489,10 +19394,10 @@ export type LearningResourcesSummaryListSortbyEnum = * @export */ export const LearningResourcesVectorSimilarListCertificationTypeEnum = { - Completion: "completion", Micromasters: "micromasters", - None: "none", Professional: "professional", + Completion: "completion", + None: "none", } as const export type LearningResourcesVectorSimilarListCertificationTypeEnum = (typeof LearningResourcesVectorSimilarListCertificationTypeEnum)[keyof typeof LearningResourcesVectorSimilarListCertificationTypeEnum] @@ -19512,6 +19417,14 @@ export type LearningResourcesVectorSimilarListDeliveryEnum = */ export const LearningResourcesVectorSimilarListDepartmentEnum = { _1: "1", + _2: "2", + _3: "3", + _4: "4", + _5: "5", + _6: "6", + _7: "7", + _8: "8", + _9: "9", _10: "10", _11: "11", _12: "12", @@ -19520,7 +19433,6 @@ export const LearningResourcesVectorSimilarListDepartmentEnum = { _16: "16", _17: "17", _18: "18", - _2: "2", _20: "20", _21A: "21A", _21G: "21G", @@ -19529,13 +19441,6 @@ export const LearningResourcesVectorSimilarListDepartmentEnum = { _21M: "21M", _22: "22", _24: "24", - _3: "3", - _4: "4", - _5: "5", - _6: "6", - _7: "7", - _8: "8", - _9: "9", Cc: "CC", CmsW: "CMS-W", Ec: "EC", @@ -19555,13 +19460,13 @@ export type LearningResourcesVectorSimilarListDepartmentEnum = * @export */ export const LearningResourcesVectorSimilarListLevelEnum = { - Advanced: "advanced", + Undergraduate: "undergraduate", Graduate: "graduate", HighSchool: "high_school", + Noncredit: "noncredit", + Advanced: "advanced", Intermediate: "intermediate", Introductory: "introductory", - Noncredit: "noncredit", - Undergraduate: "undergraduate", } as const export type LearningResourcesVectorSimilarListLevelEnum = (typeof LearningResourcesVectorSimilarListLevelEnum)[keyof typeof LearningResourcesVectorSimilarListLevelEnum] @@ -19569,13 +19474,13 @@ export type LearningResourcesVectorSimilarListLevelEnum = * @export */ export const LearningResourcesVectorSimilarListOfferedByEnum = { - Bootcamps: "bootcamps", - Climate: "climate", - Mitpe: "mitpe", Mitx: "mitx", Ocw: "ocw", - See: "see", + Bootcamps: "bootcamps", Xpro: "xpro", + Mitpe: "mitpe", + See: "see", + Climate: "climate", } as const export type LearningResourcesVectorSimilarListOfferedByEnum = (typeof LearningResourcesVectorSimilarListOfferedByEnum)[keyof typeof LearningResourcesVectorSimilarListOfferedByEnum] @@ -19583,27 +19488,27 @@ export type LearningResourcesVectorSimilarListOfferedByEnum = * @export */ export const LearningResourcesVectorSimilarListPlatformEnum = { - Bootcamps: "bootcamps", - Canvas: "canvas", - Climate: "climate", - Csail: "csail", - Ctl: "ctl", Edx: "edx", - Emeritus: "emeritus", - Globalalumni: "globalalumni", - Mitpe: "mitpe", - Mitxonline: "mitxonline", Ocw: "ocw", Oll: "oll", - Ovs: "ovs", - Podcast: "podcast", - Scc: "scc", + Mitxonline: "mitxonline", + Bootcamps: "bootcamps", + Xpro: "xpro", + Csail: "csail", + Mitpe: "mitpe", See: "see", - Simplilearn: "simplilearn", - Susskind: "susskind", + Scc: "scc", + Ctl: "ctl", Whu: "whu", - Xpro: "xpro", + Susskind: "susskind", + Globalalumni: "globalalumni", + Simplilearn: "simplilearn", + Emeritus: "emeritus", + Podcast: "podcast", Youtube: "youtube", + Canvas: "canvas", + Climate: "climate", + Ovs: "ovs", } as const export type LearningResourcesVectorSimilarListPlatformEnum = (typeof LearningResourcesVectorSimilarListPlatformEnum)[keyof typeof LearningResourcesVectorSimilarListPlatformEnum] @@ -19612,13 +19517,13 @@ export type LearningResourcesVectorSimilarListPlatformEnum = */ export const LearningResourcesVectorSimilarListResourceTypeEnum = { Course: "course", - Document: "document", + Program: "program", LearningPath: "learning_path", Podcast: "podcast", PodcastEpisode: "podcast_episode", - Program: "program", Video: "video", VideoPlaylist: "video_playlist", + Document: "document", } as const export type LearningResourcesVectorSimilarListResourceTypeEnum = (typeof LearningResourcesVectorSimilarListResourceTypeEnum)[keyof typeof LearningResourcesVectorSimilarListResourceTypeEnum] @@ -19627,32 +19532,11 @@ export type LearningResourcesVectorSimilarListResourceTypeEnum = */ export const LearningResourcesVectorSimilarListResourceTypeGroupEnum = { Course: "course", - LearningMaterial: "learning_material", Program: "program", + LearningMaterial: "learning_material", } as const export type LearningResourcesVectorSimilarListResourceTypeGroupEnum = (typeof LearningResourcesVectorSimilarListResourceTypeGroupEnum)[keyof typeof LearningResourcesVectorSimilarListResourceTypeGroupEnum] -/** - * @export - */ -export const LearningResourcesVectorSimilarListSortbyEnum = { - Id: "-id", - LastModified: "-last_modified", - Mitcoursenumber: "-mitcoursenumber", - ReadableId: "-readable_id", - StartDate: "-start_date", - Views: "-views", - Id2: "id", - LastModified2: "last_modified", - Mitcoursenumber2: "mitcoursenumber", - New: "new", - ReadableId2: "readable_id", - StartDate2: "start_date", - Upcoming: "upcoming", - Views2: "views", -} as const -export type LearningResourcesVectorSimilarListSortbyEnum = - (typeof LearningResourcesVectorSimilarListSortbyEnum)[keyof typeof LearningResourcesVectorSimilarListSortbyEnum] /** * LearningResourcesSearchApi - axios parameter creator diff --git a/learning_resources/views.py b/learning_resources/views.py index 9d43d1aeb0..e4e96dc12b 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -109,6 +109,7 @@ is_admin_user, ) from main.utils import cache_page_for_all_users, cache_page_for_anonymous_users, chunks +from vector_search.serializers import LearningResourcesSearchFiltersSerializer def show_content_file_content(user): @@ -130,6 +131,10 @@ def show_content_file_content(user): log = logging.getLogger(__name__) +def _clean_filter_params(params: dict) -> dict: + return {k: v for k, v in params.items() if v not in (None, [], "")} + + @extend_schema_view( list=extend_schema( summary="List", @@ -204,11 +209,18 @@ class LearningResourceViewSet( resource_type_name_plural = "Learning Resources" serializer_class = LearningResourceSerializer + @property + def filter_backends(self): + if self.action in ("similar", "vector_similar"): + return [] + return super().filter_backends + @extend_schema( summary="Get similar resources", parameters=[ OpenApiParameter(name="id", type=int, location=OpenApiParameter.PATH), OpenApiParameter(name="limit", type=int, location=OpenApiParameter.QUERY), + LearningResourcesSearchFiltersSerializer, ], responses=LearningResourceSerializer(many=True), ) @@ -225,7 +237,7 @@ class LearningResourceViewSet( ) def similar(self, request, *_, **kwargs): """ - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource @@ -235,13 +247,21 @@ def similar(self, request, *_, **kwargs): """ limit = int(request.GET.get("limit", 12)) pk = int(kwargs.get("id")) - learning_resource = get_object_or_404(LearningResource, id=pk) - learning_resource = LearningResource.objects.for_search_serialization().get( - id=pk + filter_serializer = LearningResourcesSearchFiltersSerializer(data=request.GET) + filter_serializer.is_valid(raise_exception=True) + filter_params = _clean_filter_params(filter_serializer.validated_data) + learning_resource = get_object_or_404( + LearningResource.objects.for_search_serialization(), + id=pk, ) resource_data = serialize_learning_resource_for_update(learning_resource) similar = get_similar_resources( - resource_data, limit, 2, 3, use_embeddings=False + resource_data, + limit, + 2, + 3, + use_embeddings=False, + filter_params=filter_params, ) return Response(LearningResourceSerializer(list(similar), many=True).data) @@ -250,6 +270,7 @@ def similar(self, request, *_, **kwargs): parameters=[ OpenApiParameter(name="id", type=int, location=OpenApiParameter.PATH), OpenApiParameter(name="limit", type=int, location=OpenApiParameter.QUERY), + LearningResourcesSearchFiltersSerializer, ], responses=LearningResourceSerializer(many=True), ) @@ -268,7 +289,7 @@ def similar(self, request, *_, **kwargs): ) def vector_similar(self, request, *_, **kwargs): """ - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource @@ -279,6 +300,10 @@ def vector_similar(self, request, *_, **kwargs): limit = int(request.GET.get("limit", 12)) pk = int(kwargs.get("id")) + filter_serializer = LearningResourcesSearchFiltersSerializer(data=request.GET) + filter_serializer.is_valid(raise_exception=True) + filter_params = _clean_filter_params(filter_serializer.validated_data) + try: learning_resource = LearningResource.objects.for_search_serialization().get( id=pk @@ -289,7 +314,12 @@ def vector_similar(self, request, *_, **kwargs): resource_data = serialize_learning_resource_for_update(learning_resource) try: similar = get_similar_resources( - resource_data, limit, 2, 3, use_embeddings=True + resource_data, + limit, + 2, + 3, + use_embeddings=True, + filter_params=filter_params, ) return Response(LearningResourceSerializer(list(similar), many=True).data) except _InactiveRpcError as ircp: diff --git a/learning_resources/views_test.py b/learning_resources/views_test.py index e145c54b8a..dcb750fa61 100644 --- a/learning_resources/views_test.py +++ b/learning_resources/views_test.py @@ -1318,6 +1318,130 @@ def test_vector_similar_resources_endpoint_only_returns_published(mocker, client assert len(response_ids) == 1 +def test_vector_similar_passes_resource_type_filter_to_qdrant(mocker, client): + """resource_type query param is translated to a Qdrant filter""" + from qdrant_client import models as qdrant_models + + from learning_resources.models import LearningResource + + resources = LearningResourceFactory.create_batch(3) + similar_for = resources[0].id + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[ + serialize_learning_resource_for_update(lr) + for lr in LearningResource.objects.for_search_serialization().filter( + id__in=[r.id for r in resources[1:]] + ) + ], + ) + + client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[similar_for]), + {"resource_type": "video_playlist"}, + ) + + query_filter = mock_similar.call_args.kwargs["query_filter"] + assert query_filter is not None + assert any( + isinstance(c, qdrant_models.FieldCondition) + and c.key == "resource_type" + and c.match.any == ["video_playlist"] + for c in query_filter.must + ) + + +def test_vector_similar_rejects_invalid_resource_type(mocker, client): + """Unknown resource_type value returns 400""" + resource = LearningResourceFactory.create() + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + resp = client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[resource.id]), + {"resource_type": "not_a_real_type"}, + ) + assert resp.status_code == 400 + + +def test_vector_similar_no_filter_passes_none(mocker, client): + """Absence of filter params yields query_filter=None""" + resource = LearningResourceFactory.create() + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[resource.id]) + ) + assert mock_similar.call_args.kwargs["query_filter"] is None + + +def test_similar_passes_filter_params_to_opensearch(mocker, client): + """resource_type query param is forwarded as filter_params to the OpenSearch path""" + resource = LearningResourceFactory.create() + mock_similar = mocker.patch( + "learning_resources_search.api.get_similar_resources_opensearch", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"resource_type": "video_playlist"}, + ) + assert mock_similar.call_args.kwargs["filter_params"] == { + "resource_type": ["video_playlist"] + } + + +def test_similar_rejects_invalid_resource_type(client): + """Unknown resource_type value returns 400 on the OpenSearch similarity endpoint""" + resource = LearningResourceFactory.create() + resp = client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"resource_type": "not_a_real_type"}, + ) + assert resp.status_code == 400 + + +@pytest.mark.parametrize(("free_value", "expected"), [("true", True), ("false", False)]) +def test_similar_boolean_filter_forwarded(mocker, client, free_value, expected): + """Boolean filter params (free=true/false) are forwarded correctly to the OpenSearch path""" + resource = LearningResourceFactory.create() + mock_similar = mocker.patch( + "learning_resources_search.api.get_similar_resources_opensearch", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"free": free_value}, + ) + assert mock_similar.call_args.kwargs["filter_params"] == {"free": expected} + + @pytest.mark.skip_nplusone_check def test_learning_resources_display_info_list_view(mocker, client): """Test learning_resources_display_info_list_view returns expected results""" diff --git a/learning_resources_search/api.py b/learning_resources_search/api.py index ad03163a35..d5e10ce417 100644 --- a/learning_resources_search/api.py +++ b/learning_resources_search/api.py @@ -6,6 +6,7 @@ from datetime import UTC, datetime from django.conf import settings +from django.db.models import QuerySet from opensearch_dsl import Search from opensearch_dsl.query import MoreLikeThis, Percolate from opensearchpy.exceptions import NotFoundError @@ -1079,20 +1080,21 @@ def get_similar_topics( return list(dict(counter.most_common(num_topics)).keys()) -def get_similar_resources( +def get_similar_resources( # noqa: PLR0913 value_doc: dict, num_resources: int, min_term_freq: int, min_doc_freq: int, use_embeddings, -) -> list[str]: + filter_params=None, +) -> QuerySet[LearningResource]: """ Get a list of similar resources based on another resource Args: value_doc (dict): a document representing the data fields we want to search with - num_topics (int): + num_resources (int): number of resources to return min_term_freq (int): minimum times a term needs to show up in input @@ -1100,15 +1102,24 @@ def get_similar_resources( minimum times a term needs to show up in docs use_embeddings (bool): use vector embeddings to retrieve results + filter_params (dict): + optional filter parameters (validated serializer data); each + backend translates these to its own filter format Returns: - list of str: - list of topic values + QuerySet[LearningResource]: + queryset of learning resources """ if use_embeddings: - return get_similar_resources_qdrant(value_doc, num_resources) + return get_similar_resources_qdrant( + value_doc, num_resources, filter_params=filter_params + ) return get_similar_resources_opensearch( - value_doc, num_resources, min_term_freq, min_doc_freq + value_doc, + num_resources, + min_term_freq, + min_doc_freq, + filter_params=filter_params, ) @@ -1117,15 +1128,23 @@ def _qdrant_similar_results( num_resources=6, collection_name=RESOURCES_COLLECTION_NAME, score_threshold=0, + query_filter=None, ): """ Get similar resources from qdrant Args: - doc (dict): - a document representing the data fields we want to search with + input_query: + query input for Qdrant similarity search; may be either a point id + or a raw embedding vector, depending on the caller num_resources (int): number of resources to return + collection_name (str): + qdrant collection name + score_threshold (float): + minimum similarity score + query_filter: + optional Qdrant filter to apply alongside the similarity search Returns: list of dict: @@ -1145,11 +1164,14 @@ def _qdrant_similar_results( limit=num_resources, using=encoder.model_short_name(), score_threshold=score_threshold, + query_filter=query_filter, ).points ] -def get_similar_resources_qdrant(value_doc: dict, num_resources: int): +def get_similar_resources_qdrant( + value_doc: dict, num_resources: int, filter_params=None +) -> QuerySet[LearningResource]: """ Get a list of similar resources from qdrant @@ -1158,16 +1180,24 @@ def get_similar_resources_qdrant(value_doc: dict, num_resources: int): a document representing the data fields we want to search with num_resources (int): number of resources to return + filter_params (dict): + optional filter parameters (validated serializer data) to narrow + the similarity search Returns: - list of str: - list of learning resources + list of learning resources """ - from vector_search.utils import vector_point_id, vector_point_key + from vector_search.utils import ( + qdrant_query_conditions, + vector_point_id, + vector_point_key, + ) + query_filter = qdrant_query_conditions(filter_params) if filter_params else None hits = _qdrant_similar_results( input_query=vector_point_id(vector_point_key(value_doc)), num_resources=num_resources, + query_filter=query_filter, ) return ( LearningResource.objects.for_search_serialization() @@ -1181,24 +1211,30 @@ def get_similar_resources_qdrant(value_doc: dict, num_resources: int): def get_similar_resources_opensearch( - value_doc: dict, num_resources: int, min_term_freq: int, min_doc_freq: int -) -> list[str]: + value_doc: dict, + num_resources: int, + min_term_freq: int, + min_doc_freq: int, + filter_params=None, +) -> QuerySet[LearningResource]: """ Get a list of similar resources from opensearch Args: value_doc (dict): a document representing the data fields we want to search with - num_topics (int): + num_resources (int): number of resources to return min_term_freq (int): minimum times a term needs to show up in input min_doc_freq (int): minimum times a term needs to show up in docs + filter_params (dict): + optional filter parameters (validated serializer data) to narrow + the similarity search Returns: - list of str: - list of learning resources + list of learning resources """ indexes = relevant_indexes( LEARNING_RESOURCE_TYPES, [], endpoint=LEARNING_RESOURCE, use_hybrid_search=False @@ -1218,10 +1254,21 @@ def get_similar_resources_opensearch( min_term_freq=min_term_freq, min_doc_freq=min_doc_freq, ) + # generate_filter_clauses expects list values; scalar booleans (from + # ArrayWrappedBoolean validated_data) must be wrapped before iterating. + normalized_params = { + k: [v] if isinstance(v, bool) else v for k, v in (filter_params or {}).items() + } + filter_clauses = generate_filter_clauses(normalized_params) + # Wrap all filters in a bool/must so opensearch-dsl serializes them as a + # single AND'd filter dict rather than a bare list (which it may not handle). + combined_filter = { + "bool": { + "must": [{"exists": {"field": "resource_type"}}, *filter_clauses.values()] + } + } # return only learning_resources - search = search.query( - "bool", must=[mlt_query], filter={"exists": {"field": "resource_type"}} - ) + search = search.query("bool", must=[mlt_query], filter=combined_filter) response = search.execute() return LearningResource.objects.for_search_serialization().filter( id__in=[ diff --git a/learning_resources_search/api_test.py b/learning_resources_search/api_test.py index 50f15d4520..68b129ebe5 100644 --- a/learning_resources_search/api_test.py +++ b/learning_resources_search/api_test.py @@ -4460,3 +4460,106 @@ def test_get_similar_topics_qdrant_uses_cached_embedding(mocker): encoder_instance.embed.assert_not_called() # Assert that the result is as expected assert result == ["topic1", "topic2"] + + +def test_get_similar_resources_qdrant_passes_filter_params(mocker): + """filter_params are translated to a Qdrant filter and forwarded to _qdrant_similar_results""" + from learning_resources_search.api import get_similar_resources_qdrant + + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[], + ) + sentinel_filter = object() + mocker.patch( + "vector_search.utils.qdrant_query_conditions", + return_value=sentinel_filter, + ) + get_similar_resources_qdrant( + value_doc={"id": 1, "readable_id": "abc", "platform": {"code": "ocw"}}, + num_resources=5, + filter_params={"resource_type": ["video_playlist"]}, + ) + assert mock_similar.call_args.kwargs["query_filter"] is sentinel_filter + + +def test_get_similar_resources_opensearch_passes_filter_params(mocker): + """filter_params are translated to OpenSearch clauses via generate_filter_clauses""" + from learning_resources_search.api import get_similar_resources_opensearch + + mock_search = mocker.MagicMock() + mock_search.extra.return_value = mock_search + mock_search.query.return_value = mock_search + mock_search.execute.return_value = mocker.MagicMock(hits=[]) + mocker.patch("learning_resources_search.api.Search", return_value=mock_search) + mocker.patch( + "learning_resources_search.api.relevant_indexes", return_value=["index"] + ) + + get_similar_resources_opensearch( + value_doc={"id": 1, "readable_id": "abc"}, + num_resources=5, + min_term_freq=2, + min_doc_freq=3, + filter_params={"resource_type": ["video_playlist"]}, + ) + + _, kwargs = mock_search.query.call_args + must_clauses = kwargs["filter"]["bool"]["must"] + assert any( + f + == { + "bool": { + "should": [ + { + "term": { + "resource_type": { + "value": "video_playlist", + "case_insensitive": True, + } + } + } + ] + } + } + for f in must_clauses + ) + + +@pytest.mark.parametrize("free_value", [True, False]) +def test_get_similar_resources_opensearch_boolean_filter(mocker, free_value): + """Scalar boolean filter_params are normalized to lists before generate_filter_clauses""" + from learning_resources_search.api import get_similar_resources_opensearch + + mock_search = mocker.MagicMock() + mock_search.extra.return_value = mock_search + mock_search.query.return_value = mock_search + mock_search.execute.return_value = mocker.MagicMock(hits=[]) + mocker.patch("learning_resources_search.api.Search", return_value=mock_search) + mocker.patch( + "learning_resources_search.api.relevant_indexes", return_value=["index"] + ) + + # Should not raise TypeError from iterating a scalar bool, + # and False must not be skipped by the truthiness check. + get_similar_resources_opensearch( + value_doc={"id": 1, "readable_id": "abc"}, + num_resources=5, + min_term_freq=2, + min_doc_freq=3, + filter_params={"free": free_value}, + ) + + _, kwargs = mock_search.query.call_args + must_clauses = kwargs["filter"]["bool"]["must"] + assert any( + f + == { + "bool": { + "should": [ + {"term": {"free": {"value": free_value, "case_insensitive": True}}} + ] + } + } + for f in must_clauses + ) diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index be7ebf931d..c29644cba6 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -2692,7 +2692,7 @@ paths: get: operationId: learning_resources_similar_list description: |- - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource @@ -2705,76 +2705,69 @@ paths: name: certification schema: type: boolean + nullable: true + description: True if the learning resource offers a certificate - in: query name: certification_type schema: type: array items: - type: string enum: - - completion - micromasters - - none - professional - description: |- - The type of certification offered - - * `micromasters` - MicroMasters Credential - * `professional` - Professional Certificate - * `completion` - Certificate of Completion - * `none` - No Certificate - explode: true - style: form + - completion + - none + type: string + description: |- + * `micromasters` - MicroMasters Credential + * `professional` - Professional Certificate + * `completion` - Certificate of Completion + * `none` - No Certificate + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ + \ Credential\n* `professional` - Professional Certificate\n* `completion`\ + \ - Certificate of Completion\n* `none` - No Certificate" - in: query name: course_feature schema: type: array items: type: string - description: Content feature for the resources. Load the 'api/v1/course_features' - endpoint for a list of course features - explode: true - style: form + minLength: 1 + description: The course feature. Possible options are at api/v1/course_features/ - in: query name: delivery schema: type: array items: - type: array - items: - enum: - - online - - hybrid - - in_person - - offline - type: string - description: |- - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline enum: + - online - hybrid - in_person - offline - - online - description: |- - The delivery of course/program resources - - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline - explode: true - style: form + type: string + description: |- + * `online` - Online + * `hybrid` - Hybrid + * `in_person` - In person + * `offline` - Offline + description: "The delivery options in which the learning resource is offered\ + \ \n\n* `online` - Online\n* `hybrid` - Hybrid\n* `in_person`\ + \ - In person\n* `offline` - Offline" - in: query name: department schema: type: array items: - type: string enum: - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + - '9' - '10' - '11' - '12' @@ -2783,7 +2776,6 @@ paths: - '16' - '17' - '18' - - '2' - '20' - 21A - 21G @@ -2792,13 +2784,6 @@ paths: - 21M - '22' - '24' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - CC - CMS-W - EC @@ -2811,53 +2796,68 @@ paths: - SP - STS - WGS - description: |- - The department that offers learning resources - - * `1` - Civil and Environmental Engineering - * `2` - Mechanical Engineering - * `3` - Materials Science and Engineering - * `4` - Architecture - * `5` - Chemistry - * `6` - Electrical Engineering and Computer Science - * `7` - Biology - * `8` - Physics - * `9` - Brain and Cognitive Sciences - * `10` - Chemical Engineering - * `11` - Urban Studies and Planning - * `12` - Earth, Atmospheric, and Planetary Sciences - * `14` - Economics - * `15` - Management - * `16` - Aeronautics and Astronautics - * `17` - Political Science - * `18` - Mathematics - * `20` - Biological Engineering - * `21A` - Anthropology - * `21G` - Global Languages - * `21H` - History - * `21L` - Literature - * `21M` - Music and Theater Arts - * `22` - Nuclear Science and Engineering - * `24` - Linguistics and Philosophy - * `CC` - Concourse - * `CMS-W` - Comparative Media Studies/Writing - * `EC` - Edgerton Center - * `ES` - Experimental Study Group - * `ESD` - Engineering Systems Division - * `HST` - Medical Engineering and Science - * `IDS` - Data, Systems, and Society - * `MAS` - Media Arts and Sciences - * `PE` - Athletics, Physical Education and Recreation - * `SP` - Special Programs - * `STS` - Science, Technology, and Society - * `WGS` - Women's and Gender Studies - explode: true - style: form + type: string + description: |- + * `1` - Civil and Environmental Engineering + * `2` - Mechanical Engineering + * `3` - Materials Science and Engineering + * `4` - Architecture + * `5` - Chemistry + * `6` - Electrical Engineering and Computer Science + * `7` - Biology + * `8` - Physics + * `9` - Brain and Cognitive Sciences + * `10` - Chemical Engineering + * `11` - Urban Studies and Planning + * `12` - Earth, Atmospheric, and Planetary Sciences + * `14` - Economics + * `15` - Management + * `16` - Aeronautics and Astronautics + * `17` - Political Science + * `18` - Mathematics + * `20` - Biological Engineering + * `21A` - Anthropology + * `21G` - Global Languages + * `21H` - History + * `21L` - Literature + * `21M` - Music and Theater Arts + * `22` - Nuclear Science and Engineering + * `24` - Linguistics and Philosophy + * `CC` - Concourse + * `CMS-W` - Comparative Media Studies/Writing + * `EC` - Edgerton Center + * `ES` - Experimental Study Group + * `ESD` - Engineering Systems Division + * `HST` - Medical Engineering and Science + * `IDS` - Data, Systems, and Society + * `MAS` - Media Arts and Sciences + * `PE` - Athletics, Physical Education and Recreation + * `SP` - Special Programs + * `STS` - Science, Technology, and Society + * `WGS` - Women's and Gender Studies + description: "The department that offers the learning resource \ + \ \n\n* `1` - Civil and Environmental Engineering\n* `2` - Mechanical Engineering\n\ + * `3` - Materials Science and Engineering\n* `4` - Architecture\n* `5` -\ + \ Chemistry\n* `6` - Electrical Engineering and Computer Science\n* `7`\ + \ - Biology\n* `8` - Physics\n* `9` - Brain and Cognitive Sciences\n* `10`\ + \ - Chemical Engineering\n* `11` - Urban Studies and Planning\n* `12` -\ + \ Earth, Atmospheric, and Planetary Sciences\n* `14` - Economics\n* `15`\ + \ - Management\n* `16` - Aeronautics and Astronautics\n* `17` - Political\ + \ Science\n* `18` - Mathematics\n* `20` - Biological Engineering\n* `21A`\ + \ - Anthropology\n* `21G` - Global Languages\n* `21H` - History\n* `21L`\ + \ - Literature\n* `21M` - Music and Theater Arts\n* `22` - Nuclear Science\ + \ and Engineering\n* `24` - Linguistics and Philosophy\n* `CC` - Concourse\n\ + * `CMS-W` - Comparative Media Studies/Writing\n* `EC` - Edgerton Center\n\ + * `ES` - Experimental Study Group\n* `ESD` - Engineering Systems Division\n\ + * `HST` - Medical Engineering and Science\n* `IDS` - Data, Systems, and\ + \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ + \ Education and Recreation\n* `SP` - Special Programs\n* `STS` - Science,\ + \ Technology, and Society\n* `WGS` - Women's and Gender Studies" - in: query name: free schema: type: boolean - description: The course/program is offered for free + nullable: true - in: path name: id schema: @@ -2868,225 +2868,178 @@ paths: schema: type: array items: - type: string enum: - - advanced + - undergraduate - graduate - high_school + - noncredit + - advanced - intermediate - introductory - - noncredit - - undergraduate - description: |- - The academic level of the resources - - * `undergraduate` - Undergraduate - * `graduate` - Graduate - * `high_school` - High School - * `noncredit` - Non-Credit - * `advanced` - Advanced - * `intermediate` - Intermediate - * `introductory` - Introductory - explode: true - style: form + type: string + description: |- + * `undergraduate` - Undergraduate + * `graduate` - Graduate + * `high_school` - High School + * `noncredit` - Non-Credit + * `advanced` - Advanced + * `intermediate` - Intermediate + * `introductory` - Introductory - in: query name: limit schema: type: integer - in: query - name: offered_by + name: ocw_topic schema: type: array items: type: string + minLength: 1 + description: The ocw topic name. + - in: query + name: offered_by + schema: + type: array + items: enum: - - bootcamps - - climate - - mitpe - mitx - ocw - - see + - bootcamps - xpro - description: |- - The organization that offers a learning resource - - * `mitx` - MITx - * `ocw` - MIT OpenCourseWare - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `climate` - MIT Climate - explode: true - style: form + - mitpe + - see + - climate + type: string + description: |- + * `mitx` - MITx + * `ocw` - MIT OpenCourseWare + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `climate` - MIT Climate + description: "The organization that offers the learning resource \ + \ \n\n* `mitx` - MITx\n* `ocw` - MIT OpenCourseWare\n* `bootcamps` -\ + \ Bootcamps\n* `xpro` - MIT xPRO\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `climate` - MIT Climate" - in: query name: platform schema: type: array items: - type: string enum: - - bootcamps - - canvas - - climate - - csail - - ctl - edx - - emeritus - - globalalumni - - mitpe - - mitxonline - ocw - oll - - ovs - - podcast - - scc + - mitxonline + - bootcamps + - xpro + - csail + - mitpe - see - - simplilearn - - susskind + - scc + - ctl - whu - - xpro + - susskind + - globalalumni + - simplilearn + - emeritus + - podcast - youtube - description: |- - The platform on which learning resources are offered - - * `edx` - edX - * `ocw` - MIT OpenCourseWare - * `oll` - Open Learning Library - * `mitxonline` - MITx Online - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `csail` - CSAIL - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `scc` - Schwarzman College of Computing - * `ctl` - Center for Transportation & Logistics - * `whu` - WHU - * `susskind` - Susskind - * `globalalumni` - Global Alumni - * `simplilearn` - Simplilearn - * `emeritus` - Emeritus - * `podcast` - Podcast - * `youtube` - YouTube - * `canvas` - Canvas - * `climate` - MIT Climate - * `ovs` - ODL Video Service - explode: true - style: form + - canvas + - climate + - ovs + type: string + description: |- + * `edx` - edX + * `ocw` - MIT OpenCourseWare + * `oll` - Open Learning Library + * `mitxonline` - MITx Online + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `csail` - CSAIL + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `scc` - Schwarzman College of Computing + * `ctl` - Center for Transportation & Logistics + * `whu` - WHU + * `susskind` - Susskind + * `globalalumni` - Global Alumni + * `simplilearn` - Simplilearn + * `emeritus` - Emeritus + * `podcast` - Podcast + * `youtube` - YouTube + * `canvas` - Canvas + * `climate` - MIT Climate + * `ovs` - ODL Video Service + description: "The platform on which the learning resource is offered \ + \ \n\n* `edx` - edX\n* `ocw` - MIT OpenCourseWare\n* `oll` - Open\ + \ Learning Library\n* `mitxonline` - MITx Online\n* `bootcamps` - Bootcamps\n\ + * `xpro` - MIT xPRO\n* `csail` - CSAIL\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `scc` - Schwarzman College of\ + \ Computing\n* `ctl` - Center for Transportation & Logistics\n* `whu` -\ + \ WHU\n* `susskind` - Susskind\n* `globalalumni` - Global Alumni\n* `simplilearn`\ + \ - Simplilearn\n* `emeritus` - Emeritus\n* `podcast` - Podcast\n* `youtube`\ + \ - YouTube\n* `canvas` - Canvas\n* `climate` - MIT Climate\n* `ovs` - ODL\ + \ Video Service" - in: query name: professional schema: type: boolean - - in: query - name: readable_id - schema: - type: array - items: - type: string - description: A unique text identifier for the resources - explode: true - style: form - - in: query - name: resource_id - schema: - type: array - items: - type: integer - description: Comma-separated list of learning resource IDs - explode: true - style: form + nullable: true - in: query name: resource_type schema: type: array items: - type: string enum: - course - - document + - program - learning_path - podcast - podcast_episode - - program - video - video_playlist - description: |- - The type of learning resource - - * `course` - Course - * `program` - Program - * `learning_path` - Learning Path - * `podcast` - Podcast - * `podcast_episode` - Podcast Episode - * `video` - Video - * `video_playlist` - Video Playlist - * `document` - Document - explode: true - style: form + - document + type: string + description: |- + * `course` - course + * `program` - program + * `learning_path` - learning path + * `podcast` - podcast + * `podcast_episode` - podcast episode + * `video` - video + * `video_playlist` - video playlist + * `document` - document + description: "The type of learning resource \n\n* `course` - course\n\ + * `program` - program\n* `learning_path` - learning path\n* `podcast` -\ + \ podcast\n* `podcast_episode` - podcast episode\n* `video` - video\n* `video_playlist`\ + \ - video playlist\n* `document` - document" - in: query name: resource_type_group schema: type: array items: - type: string enum: - course - - learning_material - program - description: |- - The resource type group of the learning resources - - * `course` - Course - * `program` - Program - * `learning_material` - Learning Material - explode: true - style: form - - in: query - name: sortby - schema: - type: string - enum: - - -id - - -last_modified - - -mitcoursenumber - - -readable_id - - -start_date - - -views - - id - - last_modified - - mitcoursenumber - - new - - readable_id - - start_date - - upcoming - - views - description: |- - Sort By - - * `id` - Object ID ascending - * `-id` - Object ID descending - * `readable_id` - Readable ID ascending - * `-readable_id` - Readable ID descending - * `last_modified` - Last Modified Date ascending - * `-last_modified` - Last Modified Date descending - * `new` - Newest resources first - * `start_date` - Start Date ascending - * `-start_date` - Start Date descending - * `mitcoursenumber` - MIT course number ascending - * `-mitcoursenumber` - MIT course number descending - * `views` - Popularity ascending - * `-views` - Popularity descending - * `upcoming` - Next start date ascending + - learning_material + type: string + description: |- + * `course` - Course + * `program` - Program + * `learning_material` - Learning Material + description: "The category of learning resource \n\n* `course`\ + \ - Course\n* `program` - Program\n* `learning_material` - Learning Material" - in: query name: topic schema: type: array items: type: string - description: Topics covered by the resources. Load the '/api/v1/topics' endpoint - for a list of topics - explode: true - style: form + minLength: 1 + description: The topic name. To see a list of options go to api/v1/topics/ tags: - learning_resources responses: @@ -3102,7 +3055,7 @@ paths: get: operationId: learning_resources_vector_similar_list description: |- - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource @@ -3115,76 +3068,69 @@ paths: name: certification schema: type: boolean + nullable: true + description: True if the learning resource offers a certificate - in: query name: certification_type schema: type: array items: - type: string enum: - - completion - micromasters - - none - professional - description: |- - The type of certification offered - - * `micromasters` - MicroMasters Credential - * `professional` - Professional Certificate - * `completion` - Certificate of Completion - * `none` - No Certificate - explode: true - style: form + - completion + - none + type: string + description: |- + * `micromasters` - MicroMasters Credential + * `professional` - Professional Certificate + * `completion` - Certificate of Completion + * `none` - No Certificate + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ + \ Credential\n* `professional` - Professional Certificate\n* `completion`\ + \ - Certificate of Completion\n* `none` - No Certificate" - in: query name: course_feature schema: type: array items: type: string - description: Content feature for the resources. Load the 'api/v1/course_features' - endpoint for a list of course features - explode: true - style: form + minLength: 1 + description: The course feature. Possible options are at api/v1/course_features/ - in: query name: delivery schema: type: array items: - type: array - items: - enum: - - online - - hybrid - - in_person - - offline - type: string - description: |- - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline enum: + - online - hybrid - in_person - offline - - online - description: |- - The delivery of course/program resources - - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline - explode: true - style: form - - in: query - name: department - schema: - type: array - items: type: string - enum: + description: |- + * `online` - Online + * `hybrid` - Hybrid + * `in_person` - In person + * `offline` - Offline + description: "The delivery options in which the learning resource is offered\ + \ \n\n* `online` - Online\n* `hybrid` - Hybrid\n* `in_person`\ + \ - In person\n* `offline` - Offline" + - in: query + name: department + schema: + type: array + items: + enum: - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + - '9' - '10' - '11' - '12' @@ -3193,7 +3139,6 @@ paths: - '16' - '17' - '18' - - '2' - '20' - 21A - 21G @@ -3202,13 +3147,6 @@ paths: - 21M - '22' - '24' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - CC - CMS-W - EC @@ -3221,53 +3159,68 @@ paths: - SP - STS - WGS - description: |- - The department that offers learning resources - - * `1` - Civil and Environmental Engineering - * `2` - Mechanical Engineering - * `3` - Materials Science and Engineering - * `4` - Architecture - * `5` - Chemistry - * `6` - Electrical Engineering and Computer Science - * `7` - Biology - * `8` - Physics - * `9` - Brain and Cognitive Sciences - * `10` - Chemical Engineering - * `11` - Urban Studies and Planning - * `12` - Earth, Atmospheric, and Planetary Sciences - * `14` - Economics - * `15` - Management - * `16` - Aeronautics and Astronautics - * `17` - Political Science - * `18` - Mathematics - * `20` - Biological Engineering - * `21A` - Anthropology - * `21G` - Global Languages - * `21H` - History - * `21L` - Literature - * `21M` - Music and Theater Arts - * `22` - Nuclear Science and Engineering - * `24` - Linguistics and Philosophy - * `CC` - Concourse - * `CMS-W` - Comparative Media Studies/Writing - * `EC` - Edgerton Center - * `ES` - Experimental Study Group - * `ESD` - Engineering Systems Division - * `HST` - Medical Engineering and Science - * `IDS` - Data, Systems, and Society - * `MAS` - Media Arts and Sciences - * `PE` - Athletics, Physical Education and Recreation - * `SP` - Special Programs - * `STS` - Science, Technology, and Society - * `WGS` - Women's and Gender Studies - explode: true - style: form + type: string + description: |- + * `1` - Civil and Environmental Engineering + * `2` - Mechanical Engineering + * `3` - Materials Science and Engineering + * `4` - Architecture + * `5` - Chemistry + * `6` - Electrical Engineering and Computer Science + * `7` - Biology + * `8` - Physics + * `9` - Brain and Cognitive Sciences + * `10` - Chemical Engineering + * `11` - Urban Studies and Planning + * `12` - Earth, Atmospheric, and Planetary Sciences + * `14` - Economics + * `15` - Management + * `16` - Aeronautics and Astronautics + * `17` - Political Science + * `18` - Mathematics + * `20` - Biological Engineering + * `21A` - Anthropology + * `21G` - Global Languages + * `21H` - History + * `21L` - Literature + * `21M` - Music and Theater Arts + * `22` - Nuclear Science and Engineering + * `24` - Linguistics and Philosophy + * `CC` - Concourse + * `CMS-W` - Comparative Media Studies/Writing + * `EC` - Edgerton Center + * `ES` - Experimental Study Group + * `ESD` - Engineering Systems Division + * `HST` - Medical Engineering and Science + * `IDS` - Data, Systems, and Society + * `MAS` - Media Arts and Sciences + * `PE` - Athletics, Physical Education and Recreation + * `SP` - Special Programs + * `STS` - Science, Technology, and Society + * `WGS` - Women's and Gender Studies + description: "The department that offers the learning resource \ + \ \n\n* `1` - Civil and Environmental Engineering\n* `2` - Mechanical Engineering\n\ + * `3` - Materials Science and Engineering\n* `4` - Architecture\n* `5` -\ + \ Chemistry\n* `6` - Electrical Engineering and Computer Science\n* `7`\ + \ - Biology\n* `8` - Physics\n* `9` - Brain and Cognitive Sciences\n* `10`\ + \ - Chemical Engineering\n* `11` - Urban Studies and Planning\n* `12` -\ + \ Earth, Atmospheric, and Planetary Sciences\n* `14` - Economics\n* `15`\ + \ - Management\n* `16` - Aeronautics and Astronautics\n* `17` - Political\ + \ Science\n* `18` - Mathematics\n* `20` - Biological Engineering\n* `21A`\ + \ - Anthropology\n* `21G` - Global Languages\n* `21H` - History\n* `21L`\ + \ - Literature\n* `21M` - Music and Theater Arts\n* `22` - Nuclear Science\ + \ and Engineering\n* `24` - Linguistics and Philosophy\n* `CC` - Concourse\n\ + * `CMS-W` - Comparative Media Studies/Writing\n* `EC` - Edgerton Center\n\ + * `ES` - Experimental Study Group\n* `ESD` - Engineering Systems Division\n\ + * `HST` - Medical Engineering and Science\n* `IDS` - Data, Systems, and\ + \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ + \ Education and Recreation\n* `SP` - Special Programs\n* `STS` - Science,\ + \ Technology, and Society\n* `WGS` - Women's and Gender Studies" - in: query name: free schema: type: boolean - description: The course/program is offered for free + nullable: true - in: path name: id schema: @@ -3278,225 +3231,178 @@ paths: schema: type: array items: - type: string enum: - - advanced + - undergraduate - graduate - high_school + - noncredit + - advanced - intermediate - introductory - - noncredit - - undergraduate - description: |- - The academic level of the resources - - * `undergraduate` - Undergraduate - * `graduate` - Graduate - * `high_school` - High School - * `noncredit` - Non-Credit - * `advanced` - Advanced - * `intermediate` - Intermediate - * `introductory` - Introductory - explode: true - style: form + type: string + description: |- + * `undergraduate` - Undergraduate + * `graduate` - Graduate + * `high_school` - High School + * `noncredit` - Non-Credit + * `advanced` - Advanced + * `intermediate` - Intermediate + * `introductory` - Introductory - in: query name: limit schema: type: integer - in: query - name: offered_by + name: ocw_topic schema: type: array items: type: string + minLength: 1 + description: The ocw topic name. + - in: query + name: offered_by + schema: + type: array + items: enum: - - bootcamps - - climate - - mitpe - mitx - ocw - - see + - bootcamps - xpro - description: |- - The organization that offers a learning resource - - * `mitx` - MITx - * `ocw` - MIT OpenCourseWare - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `climate` - MIT Climate - explode: true - style: form + - mitpe + - see + - climate + type: string + description: |- + * `mitx` - MITx + * `ocw` - MIT OpenCourseWare + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `climate` - MIT Climate + description: "The organization that offers the learning resource \ + \ \n\n* `mitx` - MITx\n* `ocw` - MIT OpenCourseWare\n* `bootcamps` -\ + \ Bootcamps\n* `xpro` - MIT xPRO\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `climate` - MIT Climate" - in: query name: platform schema: type: array items: - type: string enum: - - bootcamps - - canvas - - climate - - csail - - ctl - edx - - emeritus - - globalalumni - - mitpe - - mitxonline - ocw - oll - - ovs - - podcast - - scc + - mitxonline + - bootcamps + - xpro + - csail + - mitpe - see - - simplilearn - - susskind + - scc + - ctl - whu - - xpro + - susskind + - globalalumni + - simplilearn + - emeritus + - podcast - youtube - description: |- - The platform on which learning resources are offered - - * `edx` - edX - * `ocw` - MIT OpenCourseWare - * `oll` - Open Learning Library - * `mitxonline` - MITx Online - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `csail` - CSAIL - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `scc` - Schwarzman College of Computing - * `ctl` - Center for Transportation & Logistics - * `whu` - WHU - * `susskind` - Susskind - * `globalalumni` - Global Alumni - * `simplilearn` - Simplilearn - * `emeritus` - Emeritus - * `podcast` - Podcast - * `youtube` - YouTube - * `canvas` - Canvas - * `climate` - MIT Climate - * `ovs` - ODL Video Service - explode: true - style: form + - canvas + - climate + - ovs + type: string + description: |- + * `edx` - edX + * `ocw` - MIT OpenCourseWare + * `oll` - Open Learning Library + * `mitxonline` - MITx Online + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `csail` - CSAIL + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `scc` - Schwarzman College of Computing + * `ctl` - Center for Transportation & Logistics + * `whu` - WHU + * `susskind` - Susskind + * `globalalumni` - Global Alumni + * `simplilearn` - Simplilearn + * `emeritus` - Emeritus + * `podcast` - Podcast + * `youtube` - YouTube + * `canvas` - Canvas + * `climate` - MIT Climate + * `ovs` - ODL Video Service + description: "The platform on which the learning resource is offered \ + \ \n\n* `edx` - edX\n* `ocw` - MIT OpenCourseWare\n* `oll` - Open\ + \ Learning Library\n* `mitxonline` - MITx Online\n* `bootcamps` - Bootcamps\n\ + * `xpro` - MIT xPRO\n* `csail` - CSAIL\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `scc` - Schwarzman College of\ + \ Computing\n* `ctl` - Center for Transportation & Logistics\n* `whu` -\ + \ WHU\n* `susskind` - Susskind\n* `globalalumni` - Global Alumni\n* `simplilearn`\ + \ - Simplilearn\n* `emeritus` - Emeritus\n* `podcast` - Podcast\n* `youtube`\ + \ - YouTube\n* `canvas` - Canvas\n* `climate` - MIT Climate\n* `ovs` - ODL\ + \ Video Service" - in: query name: professional schema: type: boolean - - in: query - name: readable_id - schema: - type: array - items: - type: string - description: A unique text identifier for the resources - explode: true - style: form - - in: query - name: resource_id - schema: - type: array - items: - type: integer - description: Comma-separated list of learning resource IDs - explode: true - style: form + nullable: true - in: query name: resource_type schema: type: array items: - type: string enum: - course - - document + - program - learning_path - podcast - podcast_episode - - program - video - video_playlist - description: |- - The type of learning resource - - * `course` - Course - * `program` - Program - * `learning_path` - Learning Path - * `podcast` - Podcast - * `podcast_episode` - Podcast Episode - * `video` - Video - * `video_playlist` - Video Playlist - * `document` - Document - explode: true - style: form + - document + type: string + description: |- + * `course` - course + * `program` - program + * `learning_path` - learning path + * `podcast` - podcast + * `podcast_episode` - podcast episode + * `video` - video + * `video_playlist` - video playlist + * `document` - document + description: "The type of learning resource \n\n* `course` - course\n\ + * `program` - program\n* `learning_path` - learning path\n* `podcast` -\ + \ podcast\n* `podcast_episode` - podcast episode\n* `video` - video\n* `video_playlist`\ + \ - video playlist\n* `document` - document" - in: query name: resource_type_group schema: type: array items: - type: string enum: - course - - learning_material - program - description: |- - The resource type group of the learning resources - - * `course` - Course - * `program` - Program - * `learning_material` - Learning Material - explode: true - style: form - - in: query - name: sortby - schema: - type: string - enum: - - -id - - -last_modified - - -mitcoursenumber - - -readable_id - - -start_date - - -views - - id - - last_modified - - mitcoursenumber - - new - - readable_id - - start_date - - upcoming - - views - description: |- - Sort By - - * `id` - Object ID ascending - * `-id` - Object ID descending - * `readable_id` - Readable ID ascending - * `-readable_id` - Readable ID descending - * `last_modified` - Last Modified Date ascending - * `-last_modified` - Last Modified Date descending - * `new` - Newest resources first - * `start_date` - Start Date ascending - * `-start_date` - Start Date descending - * `mitcoursenumber` - MIT course number ascending - * `-mitcoursenumber` - MIT course number descending - * `views` - Popularity ascending - * `-views` - Popularity descending - * `upcoming` - Next start date ascending + - learning_material + type: string + description: |- + * `course` - Course + * `program` - Program + * `learning_material` - Learning Material + description: "The category of learning resource \n\n* `course`\ + \ - Course\n* `program` - Program\n* `learning_material` - Learning Material" - in: query name: topic schema: type: array items: type: string - description: Topics covered by the resources. Load the '/api/v1/topics' endpoint - for a list of topics - explode: true - style: form + minLength: 1 + description: The topic name. To see a list of options go to api/v1/topics/ tags: - learning_resources responses: diff --git a/vector_search/serializers.py b/vector_search/serializers.py index c4c2e7a56a..c1895e5dee 100644 --- a/vector_search/serializers.py +++ b/vector_search/serializers.py @@ -22,22 +22,15 @@ ) -class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): +class LearningResourcesSearchFiltersSerializer(serializers.Serializer): """ - Request serializer for vector based search - instead of id we use readable_id in case we upload qdrant snapshots + Shared filter fields for Qdrant-backed learning resource queries. + + Every field here must have a corresponding entry in + vector_search.constants.QDRANT_RESOURCE_PARAM_MAP so it can be translated + to a Qdrant filter by qdrant_query_conditions(). """ - q = serializers.CharField(required=False, help_text="The search text") - offset = serializers.IntegerField( - required=False, help_text="The initial index from which to return the results" - ) - limit = serializers.IntegerField( - required=False, help_text="Number of results to return per page" - ) - readable_id = serializers.CharField( - required=False, help_text="The readable id of the resource" - ) offered_by_choices = [(e.name.lower(), e.value) for e in OfferedBy] offered_by = serializers.ListField( required=False, @@ -148,6 +141,19 @@ class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): \n\n{build_choice_description_list(resource_type_group_choices)}" ), ) + + +class LearningResourcesVectorSearchRequestSerializer( + LearningResourcesSearchFiltersSerializer +): + """ + Request serializer for vector based search + instead of id we use readable_id in case we upload qdrant snapshots + """ + + readable_id = serializers.CharField( + required=False, help_text="The readable id of the resource" + ) url__isnull = serializers.BooleanField( required=False, default=None, @@ -160,6 +166,13 @@ class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): allow_null=True, help_text="Filter to learning resources where title is null/not null", ) + q = serializers.CharField(required=False, help_text="The search text") + offset = serializers.IntegerField( + required=False, help_text="The initial index from which to return the results" + ) + limit = serializers.IntegerField( + required=False, help_text="Number of results to return per page" + ) hybrid_search = serializers.BooleanField( required=False, default=False, diff --git a/vector_search/serializers_test.py b/vector_search/serializers_test.py index a7057c2cc3..c7e61bb9cd 100644 --- a/vector_search/serializers_test.py +++ b/vector_search/serializers_test.py @@ -1,3 +1,39 @@ -import pytest +from vector_search.serializers import ( + LearningResourcesSearchFiltersSerializer, + LearningResourcesVectorSearchRequestSerializer, +) -pytestmark = pytest.mark.django_db + +def test_filter_serializer_accepts_resource_type(): + s = LearningResourcesSearchFiltersSerializer( + data={"resource_type": ["video_playlist"]} + ) + assert s.is_valid(), s.errors + assert s.validated_data["resource_type"] == ["video_playlist"] + + +def test_filter_serializer_rejects_invalid_resource_type(): + s = LearningResourcesSearchFiltersSerializer(data={"resource_type": ["not_a_type"]}) + assert not s.is_valid() + assert "resource_type" in s.errors + + +def test_filter_serializer_has_no_search_fields(): + fields = LearningResourcesSearchFiltersSerializer().fields + assert "q" not in fields + assert "offset" not in fields + assert "limit" not in fields + assert "hybrid_search" not in fields + assert "readable_id" not in fields + # isnull filters are Qdrant-only and must not be in the shared base, + # since generate_filter_clauses (OpenSearch) doesn't support them + assert "url__isnull" not in fields + assert "title__isnull" not in fields + + +def test_vector_search_request_serializer_inherits_filter_fields(): + fields = LearningResourcesVectorSearchRequestSerializer().fields + assert "resource_type" in fields + assert "platform" in fields + assert "q" in fields + assert "hybrid_search" in fields From e69194a824e533ea530a9ea35417209fc6b48b6d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:48:30 -0400 Subject: [PATCH 4/9] Update dependency requests to v2.33.0 [SECURITY] (#3214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index f2d46e1925..1168ecea89 100644 --- a/uv.lock +++ b/uv.lock @@ -4340,7 +4340,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4348,9 +4348,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] From 6123ee772d6a12c84e570567dbcce491a815ca48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:48:58 -0400 Subject: [PATCH 5/9] Update dependency cryptography to v46.0.7 [SECURITY] (#3211) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- uv.lock | 62 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/uv.lock b/uv.lock index 1168ecea89..b5dd777f1e 100644 --- a/uv.lock +++ b/uv.lock @@ -546,41 +546,41 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] From 10ede08e074b7fb77fe88fbd5982ac1bcf5cc0d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:49:13 -0400 Subject: [PATCH 6/9] Update dependency Django to v4.2.30 [SECURITY] (#3212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 23dce1a7a0..d0ce1d10cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "Django==4.2.29", + "Django==4.2.30", "attrs>=25.0.0,<26", "base36>=0.1.1,<0.2", "beautifulsoup4>=4.8.2,<5", diff --git a/uv.lock b/uv.lock index b5dd777f1e..f3e976b943 100644 --- a/uv.lock +++ b/uv.lock @@ -748,16 +748,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/2b/8f/77a4b8ec50c821193 [[package]] name = "django" -version = "4.2.29" +version = "4.2.30" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/7306757cf2ac016d718d8a5dbae66de630addaa73dca2c341fc388458e71/django-4.2.29.tar.gz", hash = "sha256:86d91bc8086569c8d08f9c55888b583a921ac1f95ed3bdc7d5659d4709542014", size = 10438980, upload-time = "2026-03-03T13:56:42.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/b5/f1a53dc68da6429d6e0345bb848161e2381a2e9f02700148911e8582c2b3/django-4.2.30.tar.gz", hash = "sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c", size = 10468707, upload-time = "2026-04-07T14:05:45.57Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/2b/bd0a1d1846d5580e9f209b9e0128b4859e381ec1b39d6d175c61294bb530/django-4.2.29-py3-none-any.whl", hash = "sha256:074d7c4d2808050e528388bda442bd491f06def4df4fe863f27066851bba010c", size = 7996481, upload-time = "2026-03-03T13:56:36.495Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/a7c96f239cf91313a6589233fed55111c7063b26683b226802732c455dbc/django-4.2.30-py3-none-any.whl", hash = "sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65", size = 7997231, upload-time = "2026-04-07T14:05:38.241Z" }, ] [[package]] @@ -2663,7 +2663,7 @@ requires-dist = [ { name = "deepmerge", specifier = ">=2.0,<3" }, { name = "dj-database-url", specifier = ">=3.0.0,<4" }, { name = "dj-static", specifier = ">=0.0.6,<0.0.7" }, - { name = "django", specifier = "==4.2.29" }, + { name = "django", specifier = "==4.2.30" }, { name = "django-anymail", extras = ["mailgun"], specifier = ">=13.0,<14" }, { name = "django-bitfield", specifier = ">=2.2.0,<3" }, { name = "django-cache-memoize", specifier = ">=0.2.0,<0.3" }, From 08db6a18c3256d9efdb9e3247a52678ccd57408e Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Thu, 16 Apr 2026 09:53:05 -0400 Subject: [PATCH 7/9] Program Unenrollment (#3203) * feat: add program unenrollment via overflow menu on dashboard - Add useDestroyProgramEnrollment hook using v3ProgramEnrollmentsDestroy API - Add UnenrollProgramDialog confirmation dialog for program unenrollment - Add Unenroll menu item to program enrollment cards' overflow menu, visible only for free (non-verified) enrollments of regular programs (display_mode != 'course') - Add programEnrollment URL helper to test utils - Add tests covering visibility conditions and API call correctness Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add cancel and mobile coverage for UnenrollProgramDialog - Add test verifying Cancel button does not fire the DELETE API call - Add parameterized test covering both desktop and mobile overflow menus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: prevent loading flash in AllEnrollmentsDisplay after program unenrollment When program enrollments are invalidated after unenrolling, the dependent programsList and coursesList queries change keys (their id arrays change). Without keepPreviousData, the new key has no cache and isLoading fires, blanking the entire section. Use placeholderData: keepPreviousData on those two derived queries so stale data shows during the refetch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * prevent loading state after delete enrollment * remove unnecesary conditional --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/mitxonline/hooks/enrollment/index.ts | 22 +++ .../api/src/mitxonline/test-utils/urls.ts | 2 + .../CoursewareDisplay/DashboardCard.tsx | 19 ++ .../DashboardDialogs.test.tsx | 185 ++++++++++++++++++ .../CoursewareDisplay/DashboardDialogs.tsx | 76 ++++++- .../CoursewareDisplay/EnrollmentDisplay.tsx | 6 +- 6 files changed, 308 insertions(+), 2 deletions(-) diff --git a/frontends/api/src/mitxonline/hooks/enrollment/index.ts b/frontends/api/src/mitxonline/hooks/enrollment/index.ts index 97398dfef6..81fa057658 100644 --- a/frontends/api/src/mitxonline/hooks/enrollment/index.ts +++ b/frontends/api/src/mitxonline/hooks/enrollment/index.ts @@ -72,6 +72,27 @@ const useDestroyEnrollment = () => { }) } +const useDestroyProgramEnrollment = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (programId: number) => + programEnrollmentsApi.v3ProgramEnrollmentsDestroy({ + program_id: programId, + }), + onSuccess: (_data, vars) => { + queryClient.setQueryData( + enrollmentQueries.programEnrollmentsList().queryKey, + (data) => data?.filter((enrollment) => enrollment.program.id !== vars), + ) + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: enrollmentKeys.programEnrollmentsList(), + }) + }, + }) +} + const useCreateProgramEnrollment = () => { const queryClient = useQueryClient() return useMutation({ @@ -110,6 +131,7 @@ export { useCreateEnrollment, useUpdateEnrollment, useDestroyEnrollment, + useDestroyProgramEnrollment, useCreateProgramEnrollment, useCreateVerifiedProgramEnrollment, } diff --git a/frontends/api/src/mitxonline/test-utils/urls.ts b/frontends/api/src/mitxonline/test-utils/urls.ts index 598d86ca9d..06c09475c8 100644 --- a/frontends/api/src/mitxonline/test-utils/urls.ts +++ b/frontends/api/src/mitxonline/test-utils/urls.ts @@ -27,6 +27,8 @@ const enrollment = { const programEnrollments = { enrollmentsListV3: () => `${API_BASE_URL}/api/v3/program_enrollments/`, + programEnrollment: (programId: number) => + `${API_BASE_URL}/api/v3/program_enrollments/${programId}/`, } const b2b = { diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx index 84ca2d3d0b..e0c33932e8 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx @@ -25,6 +25,7 @@ import { EmailSettingsDialog, JustInTimeDialog, UnenrollDialog, + UnenrollProgramDialog, } from "./DashboardDialogs" import NiceModal from "@ebay/nice-modal-react" import { @@ -47,6 +48,7 @@ import { CourseRunEnrollmentV3, V3UserProgramEnrollment, CourseRunV2, + DisplayModeEnum, } from "@mitodl/mitxonline-api-axios/v2" import CourseEnrollmentDialog from "@/page-components/EnrollmentDialogs/CourseEnrollmentDialog" @@ -196,6 +198,23 @@ const getContextMenuItems = ( href: detailsUrl, }) } + + if ( + program.display_mode !== DisplayModeEnum.Course && + !isVerifiedEnrollmentMode(resource.data.enrollment_mode) + ) { + menuItems.push({ + className: "dashboard-card-menu-item", + key: "unenroll-program", + label: "Unenroll", + onClick: () => { + NiceModal.show(UnenrollProgramDialog, { + title, + programId: program.id, + }) + }, + }) + } } if (resource.type === DashboardType.CourseRunEnrollment) { const detailsUrl = useProductPages diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx index de52fcf48e..93687b1aff 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx @@ -147,6 +147,191 @@ describe("DashboardDialogs", () => { }) }) +describe("UnenrollProgramDialog", () => { + const setupProgramCard = ( + enrollmentMode: string | null = "audit", + displayMode: string | null = null, + ) => { + const mitxOnlineUser = mitxonline.factories.user.user() + setMockResponse.get(mitxonline.urls.userMe.get(), mitxOnlineUser) + + const programEnrollment = + mitxonline.factories.enrollment.programEnrollmentV3({ + enrollment_mode: enrollmentMode, + program: { display_mode: displayMode } as never, + }) + + return { programEnrollment } + } + + test("Shows unenroll option for free (audit) program enrollments", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + await screen.findByRole("menuitem", { name: "Unenroll" }), + ).toBeInTheDocument() + }) + + test("Does not show unenroll option for paid (verified) program enrollments", async () => { + const { programEnrollment } = setupProgramCard("verified", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + screen.queryByRole("menuitem", { name: "Unenroll" }), + ).not.toBeInTheDocument() + }) + + test("Does not show unenroll option for program-as-course display_mode programs", async () => { + const { programEnrollment } = setupProgramCard("audit", "course") + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + screen.queryByRole("menuitem", { name: "Unenroll" }), + ).not.toBeInTheDocument() + }) + + test("Confirming unenroll from a program fires the proper API call", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + setMockResponse.delete( + mitxonline.urls.programEnrollments.programEnrollment( + programEnrollment.program.id, + ), + null, + ) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + const unenrollMenuItem = await screen.findByRole("menuitem", { + name: "Unenroll", + }) + await user.click(unenrollMenuItem) + + const dialog = await screen.findByRole("dialog", { + name: `Unenroll from ${programEnrollment.program.title}`, + }) + expect(dialog).toBeInTheDocument() + expect( + within(dialog).getByText( + `Are you sure you want to unenroll from ${programEnrollment.program.title}?`, + ), + ).toBeInTheDocument() + + const confirmButton = within(dialog).getByRole("button", { + name: "Unenroll", + }) + await user.click(confirmButton) + + expect(mockAxiosInstance.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: "DELETE", + url: mitxonline.urls.programEnrollments.programEnrollment( + programEnrollment.program.id, + ), + }), + ) + }) + + test("Cancelling the dialog does not fire the API call", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + await user.click(await screen.findByRole("menuitem", { name: "Unenroll" })) + await screen.findByRole("dialog", { + name: `Unenroll from ${programEnrollment.program.title}`, + }) + + await user.click(screen.getByRole("button", { name: "Cancel" })) + + expect(mockAxiosInstance.request).not.toHaveBeenCalledWith( + expect.objectContaining({ method: "DELETE" }), + ) + }) + + test.each(["enrollment-card-desktop", "enrollment-card-mobile"] as const)( + "Unenroll option is accessible from the %s overflow menu", + async (cardTestId) => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const card = await screen.findByTestId(cardTestId) + await user.click(within(card).getByLabelText("More options")) + + expect( + await screen.findByRole("menuitem", { name: "Unenroll" }), + ).toBeInTheDocument() + }, + ) +}) + describe("JustInTimeDialog", () => { const getFields = (root: HTMLElement) => { return { diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx index 5d62d27526..5568084752 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx @@ -17,6 +17,7 @@ import { useFormik } from "formik" import { useCreateB2bEnrollment, useDestroyEnrollment, + useDestroyProgramEnrollment, useUpdateEnrollment, } from "api/mitxonline-hooks/enrollment" import { @@ -318,4 +319,77 @@ const EmailSettingsDialog = NiceModal.create(EmailSettingsDialogInner) const UnenrollDialog = NiceModal.create(UnenrollDialogInner) const JustInTimeDialog = NiceModal.create(JustInTimeDialogInner) -export { EmailSettingsDialog, UnenrollDialog, JustInTimeDialog } +type UnenrollProgramDialogProps = { + title: string + programId: number +} + +const UnenrollProgramDialogInner: React.FC = ({ + title, + programId, +}) => { + const modal = NiceModal.useModal() + const destroyProgramEnrollment = useDestroyProgramEnrollment() + const formik = useFormik({ + enableReinitialize: true, + validateOnChange: false, + validateOnBlur: false, + initialValues: {}, + onSubmit: async () => { + await destroyProgramEnrollment.mutateAsync(programId) + modal.hide() + }, + }) + return ( + + + + + } + > + + Are you sure you want to unenroll from {title}? + + {destroyProgramEnrollment.isError && ( + + There was a problem unenrolling you from this program. Please try + again later. + + )} + + ) +} + +const UnenrollProgramDialog = NiceModal.create(UnenrollProgramDialogInner) + +export { + EmailSettingsDialog, + UnenrollDialog, + UnenrollProgramDialog, + JustInTimeDialog, +} diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx index 14661062a5..c357d3f719 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx @@ -14,7 +14,7 @@ import { theme, } from "ol-components" import { Alert } from "@mitodl/smoot-design" -import { useQuery } from "@tanstack/react-query" +import { keepPreviousData, useQuery } from "@tanstack/react-query" import { EnrollmentStatus, getEnrollmentStatus, @@ -790,6 +790,9 @@ const AllEnrollmentsDisplay: React.FC = () => { page_size: enrolledProgramIds.length, }), enabled: enrolledProgramIds.length > 0, + // If the query key changes, show the old data while loading + // example: Deleting a program enrollment + placeholderData: keepPreviousData, }) const filteredProgramEnrollments = enrolledPrograms @@ -820,6 +823,7 @@ const AllEnrollmentsDisplay: React.FC = () => { page_size: homeCourseProgramModuleIds.length || undefined, }), enabled: homeCourseProgramModuleIds.length > 0, + placeholderData: keepPreviousData, }) const homeCourseProgramsById = new Map( From 041edafabcb0292f20f247642e8c5df8d3589359 Mon Sep 17 00:00:00 2001 From: Shankar Ambady Date: Thu, 16 Apr 2026 12:02:47 -0400 Subject: [PATCH 8/9] Facet counts and aggregations for Vector search (#3210) * adding aggregation generation method * adding aggregations to response * adding some optimizations and aggregations to response * regen spec * add published back to learning resources serializer * spec update * show facets on frontend * fixing aggregation counts * fix test * fix typechecks * remove unused test * adding tests for aggregations * Update vector_search/serializers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update vector_search/views.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixing 'with_payload' for group by * switch to safe getter * correct comment about dropping admin params * switching collection param map to constant * adding aggregation params for contentfiles * regenerate spec * adding fix for hybrid search offset * fix tests for new expected response * fix contentfile metadata * make hits and get_results same for both serializers * fixing skip with relation to offsets * tune prefetch multiplier * gather count with hits * adding fix for fields returned by contentfile endpoint * default hits to list * Update vector_search/utils.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * restore and update js test for vector hybrid search facet results * move published to resource specific serializer field * update spec --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontends/api/src/generated/v0/api.ts | 105 ++++++++ .../app-pages/SearchPage/SearchPage.test.tsx | 101 +++---- .../SearchDisplay/SearchDisplay.tsx | 18 +- main/settings.py | 4 +- openapi/specs/v0.yaml | 114 ++++++++ vector_search/constants.py | 14 + vector_search/serializers.py | 48 +++- vector_search/utils.py | 78 +++++- vector_search/utils_test.py | 246 ++++++++++++++++++ vector_search/views.py | 90 +++++-- vector_search/views_test.py | 12 +- 11 files changed, 731 insertions(+), 99 deletions(-) diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 368c911b52..8e0eb0472a 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -11532,6 +11532,7 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( /** * Vector Search for content * @summary Content File Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum * @param {string} [collection_name] Manually specify the name of the Qdrant collection to query * @param {Array} [file_extension] The extension of the content file. * @param {string} [group_by] The attribute to group results by @@ -11552,6 +11553,7 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( * @throws {RequiredError} */ vectorContentFilesSearchRetrieve: async ( + aggregations?: Array, collection_name?: string, file_extension?: Array, group_by?: string, @@ -11586,6 +11588,10 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( const localVarHeaderParameter = {} as any const localVarQueryParameter = {} as any + if (aggregations) { + localVarQueryParameter["aggregations"] = aggregations + } + if (collection_name !== undefined) { localVarQueryParameter["collection_name"] = collection_name } @@ -11680,6 +11686,7 @@ export const VectorContentFilesSearchApiFp = function ( /** * Vector Search for content * @summary Content File Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum * @param {string} [collection_name] Manually specify the name of the Qdrant collection to query * @param {Array} [file_extension] The extension of the content file. * @param {string} [group_by] The attribute to group results by @@ -11700,6 +11707,7 @@ export const VectorContentFilesSearchApiFp = function ( * @throws {RequiredError} */ async vectorContentFilesSearchRetrieve( + aggregations?: Array, collection_name?: string, file_extension?: Array, group_by?: string, @@ -11725,6 +11733,7 @@ export const VectorContentFilesSearchApiFp = function ( > { const localVarAxiosArgs = await localVarAxiosParamCreator.vectorContentFilesSearchRetrieve( + aggregations, collection_name, file_extension, group_by, @@ -11783,6 +11792,7 @@ export const VectorContentFilesSearchApiFactory = function ( ): AxiosPromise { return localVarFp .vectorContentFilesSearchRetrieve( + requestParameters.aggregations, requestParameters.collection_name, requestParameters.file_extension, requestParameters.group_by, @@ -11812,6 +11822,13 @@ export const VectorContentFilesSearchApiFactory = function ( * @interface VectorContentFilesSearchApiVectorContentFilesSearchRetrieveRequest */ export interface VectorContentFilesSearchApiVectorContentFilesSearchRetrieveRequest { + /** + * aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum + * @type {Array<'key' | 'course_number' | 'platform' | 'offered_by' | 'file_extension' | 'content_feature_type' | 'run_readable_id' | 'resource_readable_id' | 'run_title' | 'edx_module_id' | 'content_type' | 'description' | 'title' | 'url' | 'file_type' | 'summary' | 'flashcards' | 'checksum'>} + * @memberof VectorContentFilesSearchApiVectorContentFilesSearchRetrieve + */ + readonly aggregations?: Array + /** * Manually specify the name of the Qdrant collection to query * @type {string} @@ -11946,6 +11963,7 @@ export class VectorContentFilesSearchApi extends BaseAPI { ) { return VectorContentFilesSearchApiFp(this.configuration) .vectorContentFilesSearchRetrieve( + requestParameters.aggregations, requestParameters.collection_name, requestParameters.file_extension, requestParameters.group_by, @@ -11968,6 +11986,31 @@ export class VectorContentFilesSearchApi extends BaseAPI { } } +/** + * @export + */ +export const VectorContentFilesSearchRetrieveAggregationsEnum = { + Key: "key", + CourseNumber: "course_number", + Platform: "platform", + OfferedBy: "offered_by", + FileExtension: "file_extension", + ContentFeatureType: "content_feature_type", + RunReadableId: "run_readable_id", + ResourceReadableId: "resource_readable_id", + RunTitle: "run_title", + EdxModuleId: "edx_module_id", + ContentType: "content_type", + Description: "description", + Title: "title", + Url: "url", + FileType: "file_type", + Summary: "summary", + Flashcards: "flashcards", + Checksum: "checksum", +} as const +export type VectorContentFilesSearchRetrieveAggregationsEnum = + (typeof VectorContentFilesSearchRetrieveAggregationsEnum)[keyof typeof VectorContentFilesSearchRetrieveAggregationsEnum] /** * @export */ @@ -11991,6 +12034,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( /** * Vector Search for learning resources * @summary Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published * @param {boolean | null} [certification] True if the learning resource offers a certificate * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ @@ -12005,6 +12049,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service * @param {boolean | null} [professional] + * @param {boolean} [published] If the resource is published. We default to True unless passed in * @param {string} [q] The search text * @param {string} [readable_id] The readable id of the resource * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document @@ -12016,6 +12061,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( * @throws {RequiredError} */ vectorLearningResourcesSearchRetrieve: async ( + aggregations?: Array, certification?: boolean | null, certification_type?: Array, course_feature?: Array, @@ -12030,6 +12076,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( offset?: number, platform?: Array, professional?: boolean | null, + published?: boolean, q?: string, readable_id?: string, resource_type?: Array, @@ -12055,6 +12102,10 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( const localVarHeaderParameter = {} as any const localVarQueryParameter = {} as any + if (aggregations) { + localVarQueryParameter["aggregations"] = aggregations + } + if (certification !== undefined) { localVarQueryParameter["certification"] = certification } @@ -12111,6 +12162,10 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } + if (published !== undefined) { + localVarQueryParameter["published"] = published + } + if (q !== undefined) { localVarQueryParameter["q"] = q } @@ -12169,6 +12224,7 @@ export const VectorLearningResourcesSearchApiFp = function ( /** * Vector Search for learning resources * @summary Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published * @param {boolean | null} [certification] True if the learning resource offers a certificate * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ @@ -12183,6 +12239,7 @@ export const VectorLearningResourcesSearchApiFp = function ( * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service * @param {boolean | null} [professional] + * @param {boolean} [published] If the resource is published. We default to True unless passed in * @param {string} [q] The search text * @param {string} [readable_id] The readable id of the resource * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document @@ -12194,6 +12251,7 @@ export const VectorLearningResourcesSearchApiFp = function ( * @throws {RequiredError} */ async vectorLearningResourcesSearchRetrieve( + aggregations?: Array, certification?: boolean | null, certification_type?: Array, course_feature?: Array, @@ -12208,6 +12266,7 @@ export const VectorLearningResourcesSearchApiFp = function ( offset?: number, platform?: Array, professional?: boolean | null, + published?: boolean, q?: string, readable_id?: string, resource_type?: Array, @@ -12224,6 +12283,7 @@ export const VectorLearningResourcesSearchApiFp = function ( > { const localVarAxiosArgs = await localVarAxiosParamCreator.vectorLearningResourcesSearchRetrieve( + aggregations, certification, certification_type, course_feature, @@ -12238,6 +12298,7 @@ export const VectorLearningResourcesSearchApiFp = function ( offset, platform, professional, + published, q, readable_id, resource_type, @@ -12287,6 +12348,7 @@ export const VectorLearningResourcesSearchApiFactory = function ( ): AxiosPromise { return localVarFp .vectorLearningResourcesSearchRetrieve( + requestParameters.aggregations, requestParameters.certification, requestParameters.certification_type, requestParameters.course_feature, @@ -12301,6 +12363,7 @@ export const VectorLearningResourcesSearchApiFactory = function ( requestParameters.offset, requestParameters.platform, requestParameters.professional, + requestParameters.published, requestParameters.q, requestParameters.readable_id, requestParameters.resource_type, @@ -12321,6 +12384,13 @@ export const VectorLearningResourcesSearchApiFactory = function ( * @interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieveRequest */ export interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieveRequest { + /** + * aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published + * @type {Array<'readable_id' | 'resource_type' | 'certification' | 'certification_type' | 'professional' | 'free' | 'course_feature' | 'topic' | 'ocw_topic' | 'level' | 'department' | 'platform' | 'offered_by' | 'delivery' | 'title' | 'url' | 'resource_type_group' | 'resource_category' | 'published'>} + * @memberof VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieve + */ + readonly aggregations?: Array + /** * True if the learning resource offers a certificate * @type {boolean} @@ -12419,6 +12489,13 @@ export interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRe */ readonly professional?: boolean | null + /** + * If the resource is published. We default to True unless passed in + * @type {boolean} + * @memberof VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieve + */ + readonly published?: boolean + /** * The search text * @type {string} @@ -12490,6 +12567,7 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { ) { return VectorLearningResourcesSearchApiFp(this.configuration) .vectorLearningResourcesSearchRetrieve( + requestParameters.aggregations, requestParameters.certification, requestParameters.certification_type, requestParameters.course_feature, @@ -12504,6 +12582,7 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { requestParameters.offset, requestParameters.platform, requestParameters.professional, + requestParameters.published, requestParameters.q, requestParameters.readable_id, requestParameters.resource_type, @@ -12517,6 +12596,32 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { } } +/** + * @export + */ +export const VectorLearningResourcesSearchRetrieveAggregationsEnum = { + ReadableId: "readable_id", + ResourceType: "resource_type", + Certification: "certification", + CertificationType: "certification_type", + Professional: "professional", + Free: "free", + CourseFeature: "course_feature", + Topic: "topic", + OcwTopic: "ocw_topic", + Level: "level", + Department: "department", + Platform: "platform", + OfferedBy: "offered_by", + Delivery: "delivery", + Title: "title", + Url: "url", + ResourceTypeGroup: "resource_type_group", + ResourceCategory: "resource_category", + Published: "published", +} as const +export type VectorLearningResourcesSearchRetrieveAggregationsEnum = + (typeof VectorLearningResourcesSearchRetrieveAggregationsEnum)[keyof typeof VectorLearningResourcesSearchRetrieveAggregationsEnum] /** * @export */ diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index fe14c1c100..028bde95ed 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -152,50 +152,6 @@ describe("SearchPage", () => { }, ) - test("Vector Hybrid Search passes correct params and hides count", async () => { - setMockApiResponses({ - search: { - count: 700, - metadata: { - aggregations: { - resource_type_group: [{ key: "course", doc_count: 100 }], - }, - suggestions: [], - }, - results: factories.learningResources.resources({ count: 5 }).results, - }, - }) - - // Authenticate as path editor (admin) - setMockResponse.get(urls.userMe.get(), { - is_learning_path_editor: true, - is_authenticated: true, - }) - - renderWithProviders(, { url: "?vector_search=true&q=test" }) - - await waitFor(() => { - const call = makeRequest.mock.calls.find(([_method, url]) => { - return url.includes(urls.search.vectorResources()) - }) - expect(call).toBeDefined() - }) - - const call = makeRequest.mock.calls.find(([_method, url]) => - url.includes(urls.search.vectorResources()), - ) - invariant(call) - const fullUrl = new URL(call[1], "http://mit.edu") - const apiSearchParams = fullUrl.searchParams - - expect(apiSearchParams.get("hybrid_search")).toBe("true") - expect(apiSearchParams.get("q")).toBe("test") - - // Ensure count is hidden - const hideCountText = screen.queryByText("700 results") - expect(hideCountText).toBeNull() - }) - test("Toggling facets", async () => { setMockApiResponses({ search: { @@ -1060,4 +1016,61 @@ describe("UniversalAIBanner", () => { expect(screen.queryByText("Universal AI")).not.toBeInTheDocument() expect(screen.queryByText("New on MIT Learn")).not.toBeInTheDocument() }) + + test("Vector Hybrid Search passes correct params and renders expected count/facets", async () => { + setMockApiResponses({ + search: { + count: 700, + metadata: { + aggregations: { + resource_type_group: [{ key: "course", doc_count: 100 }], + }, + suggestions: [], + }, + results: factories.learningResources.resources({ count: 5 }).results, + }, + }) + + // Authenticate as path editor (admin) + setMockResponse.get(urls.userMe.get(), { + is_learning_path_editor: true, + is_authenticated: true, + }) + + renderWithProviders(, { url: "?vector_search=true&q=test" }) + + await waitFor(() => { + const call = makeRequest.mock.calls.find(([_method, url]) => { + return url.includes(urls.search.vectorResources()) + }) + expect(call).toBeDefined() + }) + + const call = makeRequest.mock.calls.find(([_method, url]) => + url.includes(urls.search.vectorResources()), + ) + invariant(call) + const fullUrl = new URL(call[1], "http://mit.edu") + const apiSearchParams = fullUrl.searchParams + + expect(apiSearchParams.get("hybrid_search")).toBe("true") + expect(apiSearchParams.get("q")).toBe("test") + + // Ensure count is visible + const countText = await screen.findByText("700 results") + expect(countText).toBeVisible() + + // Ensure facets are visible + await waitFor(() => { + const tabs = screen.getAllByRole("tab") + expect( + tabs.map((tab) => (tab.textContent || "").replace(/\s/g, "")), + ).toEqual([ + "All(100)", + "Courses(100)", + "Programs(0)", + "LearningMaterials(0)", + ]) + }) + }) }) diff --git a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx index 2cf28c753e..e0b53a8ee8 100644 --- a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -516,8 +516,8 @@ const searchModeDropdownOptions = Object.entries( /** * Extracts only the fields supported by the vector search API from a broader - * search params object, dropping admin-only params (e.g., aggregations, - * content_file_score_weight) that the vector endpoint does not accept. + * search params object, dropping admin-only params (e.g., content_file_score_weight) + * that the vector endpoint does not accept. * * The `as` casts for enum arrays are safe because the v0 and v1 generated * clients define separate (but structurally identical) enum types for the same @@ -526,6 +526,7 @@ const searchModeDropdownOptions = Object.entries( const toVectorSearchParams = ( params: ReturnType, ): VectorSearchRequest => ({ + aggregations: params.aggregations as VectorSearchRequest["aggregations"], certification: params.certification, certification_type: params.certification_type as VectorSearchRequest["certification_type"], @@ -625,10 +626,13 @@ const SearchDisplay: React.FC = ({ const wantsVectorSearch = searchParams.get("vector_search") === "true" const isVectorSearch = wantsVectorSearch && user?.is_learning_path_editor + const queryOptions = isVectorSearch + ? learningResourceQueries.vectorSearch(toVectorSearchParams(allParams)) + : learningResourceQueries.search(allParams as LRSearchRequest) + + // @ts-expect-error Typescript has trouble unifying the different query key types const { data, isLoading, isFetching } = useQuery({ - ...(isVectorSearch - ? learningResourceQueries.vectorSearch(toVectorSearchParams(allParams)) - : learningResourceQueries.search(allParams as LRSearchRequest)), + ...queryOptions, enabled: !wantsVectorSearch || !isUserLoading, placeholderData: keepPreviousData, select: (timedData: { @@ -985,9 +989,7 @@ const SearchDisplay: React.FC = ({ * the count when data is loaded even if count is same as previous * count. */} - {isFetching || isLoading || isVectorSearch - ? "" - : `${data?.count} results`} + {isFetching || isLoading ? "" : `${data?.count} results`} diff --git a/main/settings.py b/main/settings.py index 9f01c3535f..5233fa7607 100644 --- a/main/settings.py +++ b/main/settings.py @@ -822,10 +822,10 @@ def get_all_config_keys(): QDRANT_CLIENT_TIMEOUT = get_int(name="QDRANT_CLIENT_TIMEOUT", default=10) VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER = get_int( - name="VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER", default=20 + name="VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER", default=5 ) VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT = get_int( - name="VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT", default=10000 + name="VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT", default=500 ) # toggle to use requests (default for local) or webdriver which renders js elements EMBEDDINGS_EXTERNAL_FETCH_USE_WEBDRIVER = get_bool( diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 4bb97c19c2..85bfabe127 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -827,6 +827,58 @@ paths: description: Vector Search for content summary: Content File Vector Search parameters: + - in: query + name: aggregations + schema: + type: array + items: + enum: + - key + - course_number + - platform + - offered_by + - file_extension + - content_feature_type + - run_readable_id + - resource_readable_id + - run_title + - edx_module_id + - content_type + - description + - title + - url + - file_type + - summary + - flashcards + - checksum + type: string + description: |- + * `key` - Key + * `course_number` - Course Number + * `platform` - Platform + * `offered_by` - Offered By + * `file_extension` - File Extension + * `content_feature_type` - Content Feature Type + * `run_readable_id` - Run Readable Id + * `resource_readable_id` - Resource Readable Id + * `run_title` - Run Title + * `edx_module_id` - Edx Module Id + * `content_type` - Content Type + * `description` - Description + * `title` - Title + * `url` - Url + * `file_type` - File Type + * `summary` - Summary + * `flashcards` - Flashcards + * `checksum` - Checksum + description: "aggregations for facet counts \n\n* `key` - Key\n\ + * `course_number` - Course Number\n* `platform` - Platform\n* `offered_by`\ + \ - Offered By\n* `file_extension` - File Extension\n* `content_feature_type`\ + \ - Content Feature Type\n* `run_readable_id` - Run Readable Id\n* `resource_readable_id`\ + \ - Resource Readable Id\n* `run_title` - Run Title\n* `edx_module_id` -\ + \ Edx Module Id\n* `content_type` - Content Type\n* `description` - Description\n\ + * `title` - Title\n* `url` - Url\n* `file_type` - File Type\n* `summary`\ + \ - Summary\n* `flashcards` - Flashcards\n* `checksum` - Checksum" - in: query name: collection_name schema: @@ -961,6 +1013,61 @@ paths: description: Vector Search for learning resources summary: Vector Search parameters: + - in: query + name: aggregations + schema: + type: array + items: + enum: + - readable_id + - resource_type + - certification + - certification_type + - professional + - free + - course_feature + - topic + - ocw_topic + - level + - department + - platform + - offered_by + - delivery + - title + - url + - resource_type_group + - resource_category + - published + type: string + description: |- + * `readable_id` - Readable Id + * `resource_type` - Resource Type + * `certification` - Certification + * `certification_type` - Certification Type + * `professional` - Professional + * `free` - Free + * `course_feature` - Course Feature + * `topic` - Topic + * `ocw_topic` - Ocw Topic + * `level` - Level + * `department` - Department + * `platform` - Platform + * `offered_by` - Offered By + * `delivery` - Delivery + * `title` - Title + * `url` - Url + * `resource_type_group` - Resource Type Group + * `resource_category` - Resource Category + * `published` - Published + description: "aggregations for facet counts \n\n* `readable_id`\ + \ - Readable Id\n* `resource_type` - Resource Type\n* `certification` -\ + \ Certification\n* `certification_type` - Certification Type\n* `professional`\ + \ - Professional\n* `free` - Free\n* `course_feature` - Course Feature\n\ + * `topic` - Topic\n* `ocw_topic` - Ocw Topic\n* `level` - Level\n* `department`\ + \ - Department\n* `platform` - Platform\n* `offered_by` - Offered By\n*\ + \ `delivery` - Delivery\n* `title` - Title\n* `url` - Url\n* `resource_type_group`\ + \ - Resource Type Group\n* `resource_category` - Resource Category\n* `published`\ + \ - Published" - in: query name: certification schema: @@ -1255,6 +1362,13 @@ paths: schema: type: boolean nullable: true + - in: query + name: published + schema: + type: boolean + default: true + description: If the resource is published. We default to True unless passed + in - in: query name: q schema: diff --git a/vector_search/constants.py b/vector_search/constants.py index 0adc4f31a4..074fa67991 100644 --- a/vector_search/constants.py +++ b/vector_search/constants.py @@ -45,6 +45,8 @@ "title": "title", "url": "url", "resource_type_group": "resource_type_group", + "resource_category": "resource_category", + "published": "published", } @@ -71,6 +73,7 @@ "url": models.PayloadSchemaType.KEYWORD, "title": models.PayloadSchemaType.KEYWORD, "resource_type_group": models.PayloadSchemaType.KEYWORD, + "resource_category": models.PayloadSchemaType.KEYWORD, } """ @@ -92,3 +95,14 @@ QDRANT_TOPIC_INDEXES = { "name": models.PayloadSchemaType.KEYWORD, } + + +CONTENT_FILES_RETRIEVE_PAYLOAD = True +RESOURCES_RETRIEVE_PAYLOAD = ["readable_id"] + + +COLLECTION_PARAM_MAP = { + RESOURCES_COLLECTION_NAME: QDRANT_RESOURCE_PARAM_MAP, + TOPICS_COLLECTION_NAME: QDRANT_TOPICS_PARAM_MAP, + CONTENT_FILES_COLLECTION_NAME: QDRANT_CONTENT_FILE_PARAM_MAP, +} diff --git a/vector_search/serializers.py b/vector_search/serializers.py index c1895e5dee..dbd5614467 100644 --- a/vector_search/serializers.py +++ b/vector_search/serializers.py @@ -20,6 +20,10 @@ SearchResponseMetadata, SearchResponseSerializer, ) +from vector_search.constants import ( + QDRANT_CONTENT_FILE_PARAM_MAP, + QDRANT_RESOURCE_PARAM_MAP, +) class LearningResourcesSearchFiltersSerializer(serializers.Serializer): @@ -151,6 +155,23 @@ class LearningResourcesVectorSearchRequestSerializer( instead of id we use readable_id in case we upload qdrant snapshots """ + published = serializers.BooleanField( + required=False, + default=True, + help_text="If the resource is published. We default to True unless passed in", + ) + aggregation_choices = [ + (key, key.replace("_", " ").title()) for key in QDRANT_RESOURCE_PARAM_MAP + ] + aggregations = serializers.ListField( + required=False, + child=serializers.ChoiceField(choices=aggregation_choices), + help_text=( + f"aggregations for facet counts \ + \n\n{build_choice_description_list(aggregation_choices)}" + ), + ) + readable_id = serializers.CharField( required=False, help_text="The readable id of the resource" ) @@ -187,14 +208,14 @@ class LearningResourcesVectorSearchResponseSerializer(SearchResponseSerializer): @extend_schema_field(LearningResourceSerializer(many=True)) def get_results(self, instance): - return instance.get("hits", {}) + return instance.get("hits", []) def get_count(self, instance) -> int: - return instance.get("total", {}).get("value") + return instance.get("total", {}).get("value", 0) - def get_metadata(self, _) -> SearchResponseMetadata: + def get_metadata(self, instance) -> SearchResponseMetadata: return { - "aggregations": [], + "aggregations": instance.get("aggregations", {}), "suggest": [], } @@ -211,6 +232,17 @@ class ContentFileVectorSearchRequestSerializer(serializers.Serializer): limit = serializers.IntegerField( required=False, help_text="Number of results to return per page" ) + aggregation_choices = [ + (key, key.replace("_", " ").title()) for key in QDRANT_CONTENT_FILE_PARAM_MAP + ] + aggregations = serializers.ListField( + required=False, + child=serializers.ChoiceField(choices=aggregation_choices), + help_text=( + f"aggregations for facet counts \ + \n\n{build_choice_description_list(aggregation_choices)}" + ), + ) sortby = serializers.ChoiceField( required=False, choices=CONTENT_FILE_SORTBY_OPTIONS, @@ -288,14 +320,14 @@ class ContentFileVectorSearchResponseSerializer(SearchResponseSerializer): """ def get_count(self, instance) -> int: - return instance["total"]["value"] + return instance.get("total", {}).get("value", 0) @extend_schema_field(ContentFileSerializer(many=True)) def get_results(self, instance): - return instance["hits"] + return instance.get("hits", []) - def get_metadata(self, *_) -> SearchResponseMetadata: + def get_metadata(self, instance) -> SearchResponseMetadata: return { - "aggregations": [], + "aggregations": instance.get("aggregations", {}), "suggest": [], } diff --git a/vector_search/utils.py b/vector_search/utils.py index d5a50e05f0..89791ab06e 100644 --- a/vector_search/utils.py +++ b/vector_search/utils.py @@ -1,3 +1,4 @@ +import asyncio import gc import logging import uuid @@ -32,13 +33,13 @@ ) from main.utils import checksum_for_content from vector_search.constants import ( + COLLECTION_PARAM_MAP, CONTENT_FILES_COLLECTION_NAME, QDRANT_CONTENT_FILE_INDEXES, QDRANT_CONTENT_FILE_PARAM_MAP, QDRANT_LEARNING_RESOURCE_INDEXES, QDRANT_RESOURCE_PARAM_MAP, QDRANT_TOPIC_INDEXES, - QDRANT_TOPICS_PARAM_MAP, RESOURCES_COLLECTION_NAME, TOPICS_COLLECTION_NAME, ) @@ -871,7 +872,11 @@ def process_batch(docs_batch, summaries_list): def _resource_vector_hits(search_result): - hits = [hit.payload["readable_id"] for hit in search_result] + hits = [ + readable_id + for readable_id in (hit.payload.get("readable_id") for hit in search_result) + if readable_id + ] """ Always lookup learning resources by readable_id for portability in case we load points from external systems @@ -981,17 +986,74 @@ def document_exists(document, collection_name=RESOURCES_COLLECTION_NAME): return count_result.count > 0 +async def async_qdrant_aggregations( + aggregation_keys: list, + params: dict, + collection_name: str = RESOURCES_COLLECTION_NAME, +) -> dict: + """ + Compute facet aggregations from Qdrant for each requested field. + Issues one concurrent facet query per aggregation key and returns results + in the same shape used by the OpenSearch aggregation API: + ``{"delivery": [{"key": "online", "doc_count": 24}, ...], ...}`` + Args: + aggregation_keys: list of aggregation parameter names. + Must be valid keys in the collection's param map + (e.g. ``QDRANT_RESOURCE_PARAM_MAP``). + params: dict of all search parameters, which are used to construct + a Qdrant ``models.Filter`` for each facet query. + collection_name: name of the Qdrant collection to query. + Returns: + dict mapping each requested aggregation name to a list of + ``{"key": str, "doc_count": int}`` dicts sorted by + ``doc_count`` descending. + """ + if not aggregation_keys: + return {} + + param_map = COLLECTION_PARAM_MAP.get(collection_name, QDRANT_RESOURCE_PARAM_MAP) + client = async_qdrant_client() + + async def _get_facet(agg_key: str): + qdrant_field = param_map.get(agg_key) + if not qdrant_field: + return agg_key, [] + + filtered_params = { + k: v for k, v in params.items() if k.partition("__")[0] != agg_key + } + facet_filter = qdrant_query_conditions( + filtered_params, collection_name=collection_name + ) + + result = await client.facet( + collection_name=collection_name, + key=qdrant_field, + facet_filter=facet_filter, + limit=100, + ) + hits = [ + { + "key": str(hit.value).lower() + if isinstance(hit.value, bool) + else str(hit.value), + "doc_count": hit.count, + } + for hit in result.hits + ] + hits.sort(key=lambda x: x["doc_count"], reverse=True) + return agg_key, hits + + results = await asyncio.gather(*[_get_facet(key) for key in aggregation_keys]) + return dict(results) + + def qdrant_query_conditions(params, collection_name=RESOURCES_COLLECTION_NAME): """ Return a list of Qdrant FieldCondition objects based on params """ - collection_param_map = { - RESOURCES_COLLECTION_NAME: QDRANT_RESOURCE_PARAM_MAP, - TOPICS_COLLECTION_NAME: QDRANT_TOPICS_PARAM_MAP, - CONTENT_FILES_COLLECTION_NAME: QDRANT_CONTENT_FILE_PARAM_MAP, - } - qdrant_param_map = collection_param_map.get(collection_name) + qdrant_param_map = COLLECTION_PARAM_MAP.get(collection_name) if not params or not qdrant_param_map: return None must = [] diff --git a/vector_search/utils_test.py b/vector_search/utils_test.py index 8917834c5b..c9d2458a44 100644 --- a/vector_search/utils_test.py +++ b/vector_search/utils_test.py @@ -1,3 +1,4 @@ +import asyncio import random from decimal import Decimal from unittest.mock import MagicMock @@ -44,6 +45,7 @@ _get_text_splitter, _is_markdown_content, _resource_vector_hits, + async_qdrant_aggregations, create_qdrant_collections, embed_learning_resources, embed_topics, @@ -1519,3 +1521,247 @@ def test_resource_vector_hits_preserves_qdrant_score_order(): expected_readable_ids = [r.readable_id for r in shuffled] actual_readable_ids = [r["readable_id"] for r in result] assert actual_readable_ids == expected_readable_ids + + +def _make_facet_hit(count=0, value="test"): + """Build a minimal mock that looks like a Qdrant FacetHit.""" + hit = MagicMock() + hit.value = value + hit.count = count + return hit + + +def _make_facet_response(hits): + """Build a minimal mock that looks like a Qdrant FacetResponse.""" + resp = MagicMock() + resp.hits = hits + return resp + + +def test_async_qdrant_aggregations_empty_keys(mocker): + """Should return {} immediately without calling Qdrant when aggregation_keys is empty.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + result = asyncio.run(async_qdrant_aggregations([], {})) + assert result == {} + mock_client.facet.assert_not_called() + + +def test_async_qdrant_aggregations_unknown_key(mocker): + """An aggregation key not present in the param map should return an empty list.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + result = asyncio.run( + async_qdrant_aggregations( + ["nonexistent_field"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + assert result == {"nonexistent_field": []} + mock_client.facet.assert_not_called() + + +def test_async_qdrant_aggregations_single_key(mocker): + """A valid single aggregation key should query Qdrant and return correctly shaped data.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(42, value="course"), + _make_facet_hit(7, value="podcast"), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["resource_type"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + assert "resource_type" in result + hits = result["resource_type"] + # Should be sorted descending by doc_count + assert hits[0] == {"key": "course", "doc_count": 42} + assert hits[1] == {"key": "podcast", "doc_count": 7} + + mock_client.facet.assert_awaited_once() + call_kwargs = mock_client.facet.call_args.kwargs + assert call_kwargs["collection_name"] == RESOURCES_COLLECTION_NAME + assert call_kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["resource_type"] + assert call_kwargs["limit"] == 100 + + +def test_async_qdrant_aggregations_multiple_keys(mocker): + """Multiple valid keys should each issue a concurrent Qdrant facet call.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + # Return different data depending on the 'key' kwarg + def _facet_side_effect(**kwargs): + if kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["resource_type"]: + return _make_facet_response([_make_facet_hit(10, value="course")]) + if kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["platform"]: + return _make_facet_response( + [_make_facet_hit(30, value="ocw"), _make_facet_hit(20, value="edx")] + ) + return _make_facet_response([]) + + mock_client.facet.side_effect = _facet_side_effect + + result = asyncio.run( + async_qdrant_aggregations( + ["resource_type", "platform"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + assert set(result.keys()) == {"resource_type", "platform"} + assert result["resource_type"] == [{"key": "course", "doc_count": 10}] + # Descending sort + assert result["platform"][0] == {"key": "ocw", "doc_count": 30} + assert result["platform"][1] == {"key": "edx", "doc_count": 20} + assert mock_client.facet.await_count == 2 + + +def test_async_qdrant_aggregations_excludes_own_param_from_filter(mocker): + """ + When building the per-facet filter, the aggregation key's own param + must be excluded so that all values for that facet are counted. + """ + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + mock_client.facet.return_value = _make_facet_response([]) + + params = { + "resource_type": ["course"], + "platform": ["ocw"], + } + + asyncio.run( + async_qdrant_aggregations( + ["resource_type"], + params, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + mock_client.facet.assert_awaited_once() + call_kwargs = mock_client.facet.call_args.kwargs + + # The facet_filter should NOT contain a condition for resource_type + # (it was stripped out so we get all resource_type facet values), + # but it SHOULD still filter by platform. + facet_filter = call_kwargs.get("facet_filter") + # facet_filter is a qdrant models.Filter with must conditions + assert facet_filter is not None + condition_keys = [c.key for c in facet_filter.must if hasattr(c, "key")] + assert QDRANT_RESOURCE_PARAM_MAP["platform"] in condition_keys + assert QDRANT_RESOURCE_PARAM_MAP["resource_type"] not in condition_keys + + +def test_async_qdrant_aggregations_bool_values_lowercased(mocker): + """Boolean hit values must be returned as lowercase strings ('true'/'false').""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(5, value=True), + _make_facet_hit(3, value=False), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["free"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + keys = {hit["key"] for hit in result["free"]} + assert "true" in keys + assert "false" in keys + # Verify no raw booleans slipped through + assert True not in keys + assert False not in keys + + +def test_async_qdrant_aggregations_sorted_by_doc_count_desc(mocker): + """Results must be sorted by doc_count in descending order.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(5, value="edx"), + _make_facet_hit(100, value="ocw"), + _make_facet_hit(20, value="xpro"), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["platform"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + counts = [hit["doc_count"] for hit in result["platform"]] + assert counts == sorted(counts, reverse=True) + + +def test_async_qdrant_aggregations_uses_content_file_param_map(mocker): + """ + When collection_name is CONTENT_FILES_COLLECTION_NAME the function must + use QDRANT_CONTENT_FILE_PARAM_MAP to resolve the Qdrant field name. + """ + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + mock_client.facet.return_value = _make_facet_response( + [_make_facet_hit(8, value=".pdf")] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["file_extension"], + {}, + collection_name=CONTENT_FILES_COLLECTION_NAME, + ) + ) + + assert "file_extension" in result + call_kwargs = mock_client.facet.call_args.kwargs + assert call_kwargs["collection_name"] == CONTENT_FILES_COLLECTION_NAME + # The Qdrant field for 'file_extension' should come from the content-file map + assert call_kwargs["key"] == QDRANT_CONTENT_FILE_PARAM_MAP["file_extension"] diff --git a/vector_search/views.py b/vector_search/views.py index 4466edd129..5335351c46 100644 --- a/vector_search/views.py +++ b/vector_search/views.py @@ -17,6 +17,12 @@ from authentication.decorators import blocked_ip_exempt from learning_resources.constants import GROUP_CONTENT_FILE_CONTENT_VIEWERS from main.utils import cache_page_for_anonymous_users +from vector_search.constants import ( + CONTENT_FILES_COLLECTION_NAME, + CONTENT_FILES_RETRIEVE_PAYLOAD, + RESOURCES_COLLECTION_NAME, + RESOURCES_RETRIEVE_PAYLOAD, +) from vector_search.serializers import ( ContentFileVectorSearchRequestSerializer, ContentFileVectorSearchResponseSerializer, @@ -24,11 +30,10 @@ LearningResourcesVectorSearchResponseSerializer, ) from vector_search.utils import ( - CONTENT_FILES_COLLECTION_NAME, - RESOURCES_COLLECTION_NAME, _content_file_vector_hits, _merge_dicts, _resource_vector_hits, + async_qdrant_aggregations, async_qdrant_client, dense_encoder, qdrant_query_conditions, @@ -82,12 +87,12 @@ async def dispatch(self, request, *args, **kwargs): self.response = self.finalize_response(request, response, *args, **kwargs) return self.response - async def async_vector_search( # noqa: PLR0913 + async def async_vector_search( # noqa: PLR0913, PLR0915 self, query_string: str, params: dict, limit: int = 10, - offset: int = 10, + offset: int = 0, search_collection=RESOURCES_COLLECTION_NAME, *, hybrid_search: bool = False, @@ -113,8 +118,19 @@ async def async_vector_search( # noqa: PLR0913 "collection_name": search_collection, "query_filter": search_filter, "with_vectors": False, - "with_payload": True, - "search_params": models.SearchParams(indexed_only=True, exact=False), + "with_payload": RESOURCES_RETRIEVE_PAYLOAD + if search_collection == RESOURCES_COLLECTION_NAME + else CONTENT_FILES_RETRIEVE_PAYLOAD, + "search_params": models.SearchParams( + quantization=models.QuantizationSearchParams( + ignore=False, + rescore=True, + oversampling=1, + ), + hnsw_ef=64, + indexed_only=True, + exact=False, + ), "limit": limit, } @@ -151,6 +167,7 @@ async def async_vector_search( # noqa: PLR0913 search_params.pop("search_params", None) search_params["group_by"] = params.get("group_by") search_params["group_size"] = params.get("group_size", 1) + search_params["with_payload"] = True group_result = await client.query_points_groups(**search_params) search_result = [] for group in group_result.groups: @@ -171,29 +188,56 @@ async def async_vector_search( # noqa: PLR0913 result_obj = await client.query_points(**search_params) search_result = result_obj.points else: - scroll_res = await client.scroll( - collection_name=search_collection, - scroll_filter=search_filter, - limit=limit, - offset=offset, - with_vectors=False, - ) - search_result = scroll_res[0] - - if search_collection == RESOURCES_COLLECTION_NAME: - hits = await sync_to_async(_resource_vector_hits)(search_result) - else: - hits = await sync_to_async(_content_file_vector_hits)(search_result) + # Qdrant's scroll API uses a point-ID cursor for `offset`, not a + # numeric skip count. We implement integer offset by consuming + # scroll pages until the desired number of records are skipped. + remaining_to_skip = offset + next_page_offset = None + search_result = [] + while True: + fetch_size = min(max(remaining_to_skip, limit), 1000) + scroll_res = await client.scroll( + collection_name=search_collection, + scroll_filter=search_filter, + limit=fetch_size, + offset=next_page_offset, + with_vectors=False, + ) + page_points, next_page_offset = scroll_res + if remaining_to_skip > 0: + skipped = min(remaining_to_skip, len(page_points)) + page_points = page_points[skipped:] + remaining_to_skip -= skipped + search_result.extend(page_points) + if len(search_result) >= limit or not next_page_offset: + break + search_result = search_result[:limit] + + hits_coroutine = ( + sync_to_async(_resource_vector_hits)(search_result) + if search_collection == RESOURCES_COLLECTION_NAME + else sync_to_async(_content_file_vector_hits)(search_result) + ) - count_result = await client.count( - collection_name=search_collection, - count_filter=search_filter, - exact=False, + aggregation_keys = params.get("aggregations") or [] + hits, count_result, aggregations = await asyncio.gather( + hits_coroutine, + client.count( + collection_name=search_collection, + count_filter=search_filter, + exact=False, + ), + async_qdrant_aggregations( + aggregation_keys, + params, + collection_name=search_collection, + ), ) return { "hits": hits, "total": {"value": count_result.count}, + "aggregations": aggregations or {}, } def handle_exception(self, exc): diff --git a/vector_search/views_test.py b/vector_search/views_test.py index 774ca25436..981fc4371b 100644 --- a/vector_search/views_test.py +++ b/vector_search/views_test.py @@ -14,7 +14,7 @@ def test_vector_search_filters(mocker, client): mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -63,7 +63,7 @@ def test_vector_search_filters_empty_query(mocker, client): mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mock_qdrant.count = mocker.AsyncMock(return_value=CountResult(count=10)) @@ -124,7 +124,7 @@ def test_content_file_vector_search_filters( mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -201,7 +201,7 @@ def test_content_file_vector_search_filters_empty_query( mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -255,7 +255,7 @@ def test_content_file_vector_search_filters_custom_collection( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() custom_collection_name = "foo_bar_collection" - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -300,7 +300,7 @@ def test_content_file_vector_search_group_parameters(mocker, client, django_user )() custom_collection_name = "foo_bar_collection" - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( From 7c076c40b35568fe69f39470ccf6b2db319debf5 Mon Sep 17 00:00:00 2001 From: Doof Date: Thu, 16 Apr 2026 17:12:49 +0000 Subject: [PATCH 9/9] Release 0.63.6 --- RELEASE.rst | 12 ++++++++++++ main/settings.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 81c08e184d..e938bf123a 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,18 @@ Release Notes ============= +Version 0.63.6 +-------------- + +- Facet counts and aggregations for Vector search (#3210) +- Program Unenrollment (#3203) +- Update dependency Django to v4.2.30 [SECURITY] (#3212) +- Update dependency cryptography to v46.0.7 [SECURITY] (#3211) +- Update dependency requests to v2.33.0 [SECURITY] (#3214) +- Filtering for similarity endpoints (#3204) +- fix: minor improvements on video collection pages (#3205) +- Update dependency litellm to v1.83.0 [SECURITY] (#3213) + Version 0.63.5 (Released April 16, 2026) -------------- diff --git a/main/settings.py b/main/settings.py index 5233fa7607..766e05645c 100644 --- a/main/settings.py +++ b/main/settings.py @@ -34,7 +34,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.63.5" +VERSION = "0.63.6" log = logging.getLogger()