diff --git a/package.json b/package.json index a5d15d1..52b0bd9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ }, "homepage": "https://github.com/mitodl/course-search-utils#readme", "dependencies": { - "bodybuilder": "^2.5.0", "query-string": "^6.13.1", "ramda": "^0.27.1" }, diff --git a/src/constants.ts b/src/constants.ts index 96453a7..94e2b2b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,28 +15,20 @@ export enum LearningResourceType { Profile = "profile" } -export const LR_TYPE_ALL = [ - LearningResourceType.Course, - LearningResourceType.Program, - LearningResourceType.Userlist, - LearningResourceType.LearningPath, - LearningResourceType.Video, - LearningResourceType.Podcast, - LearningResourceType.PodcastEpisode -] - export const INITIAL_FACET_STATE: Facets = { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] } +export const LEARNING_RESOURCE_ENDPOINT = "resource" +export const CONTENT_FILE_ENDPOINT = "content_file" + export const COURSENUM_SORT_FIELD = "department_course_numbers.sort_coursenum" -export type Level = "Graduate" | "Undergraduate" | null + diff --git a/src/index.test.tsx b/src/index.test.tsx index 5dbf241..56f5d87 100644 --- a/src/index.test.tsx +++ b/src/index.test.tsx @@ -10,13 +10,12 @@ import { import { LearningResourceType, - INITIAL_FACET_STATE, - LR_TYPE_ALL + INITIAL_FACET_STATE } from "./constants" import { useCourseSearch, useSearchInputs, useSyncUrlAndSearch } from "./index" import { facetMap, wait } from "./test_util" -import { serializeSort, serializeSearchParams } from "./url_utils" +import { serializeSearchParams } from "./url_utils" function FacetTestComponent(props: any) { const { @@ -57,7 +56,8 @@ function TestComponent(props: any) { onSubmit, sort, updateSort, - updateUI + updateUI, + updateEndpoint } = useCourseSearch( runSearch, clearSearch, @@ -79,12 +79,13 @@ function TestComponent(props: any) { +
{ expect(wrapper.find("input").prop("value")).toBe("") checkSearchCall(runSearch, [ "", // empty search text - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 0, null, + null, null ]) }) @@ -183,16 +182,11 @@ describe("useCourseSearch", () => { expect(wrapper.find("select").prop("value")).toBe("coursenum") checkSearchCall(runSearch, [ "", // empty search text - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 0, - { - field: "coursenum", - option: "asc" - }, - null + "coursenum", + null, + null, ]) }) @@ -201,13 +195,24 @@ describe("useCourseSearch", () => { wrapper.find(".ui").simulate("click") checkSearchCall(runSearch, [ "", // empty search text - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, + 0, + null, + "list", + null + ]) + }) + + it("should let you update the endpoint param", async () => { + const { wrapper, runSearch } = render() + wrapper.find(".endpoint").simulate("click") + checkSearchCall(runSearch, [ + "", // empty search text + INITIAL_FACET_STATE, 0, null, - "list" + null, + 'content_file' ]) }) @@ -218,12 +223,10 @@ describe("useCourseSearch", () => { wrapper.find(".submit").simulate("click") checkSearchCall(runSearch, [ "My New Search Text", // empty search text - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 0, null, + null, null ]) wrapper.update() @@ -239,7 +242,7 @@ describe("useCourseSearch", () => { wrapper.find(".onUpdateFacets").prop("onClick")?.({ target: { // @ts-expect-error - name: "topics", + name: "topic", value: "Mathematics", checked: true } @@ -248,7 +251,7 @@ describe("useCourseSearch", () => { wrapper.update() expect(wrapper.find("FacetTestComponent").prop("activeFacets")).toEqual({ ...INITIAL_FACET_STATE, - topics: ["Mathematics"] + topic: ["Mathematics"] }) await wait(1) @@ -257,7 +260,7 @@ describe("useCourseSearch", () => { text: "", activeFacets: { ...INITIAL_FACET_STATE, - topics: ["Mathematics"] + topic: ["Mathematics"] } })}` ) @@ -266,22 +269,20 @@ describe("useCourseSearch", () => { "", { ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL, - topics: ["Mathematics"] + topic: ["Mathematics"] }, 0, null, + null, null ]) wrapper.find(".clearAllFilters").simulate("click") checkSearchCall(runSearch, [ "", - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 0, null, + null, null ]) }) @@ -294,12 +295,10 @@ describe("useCourseSearch", () => { }) checkSearchCall(runSearch, [ "my suggestion", - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 0, null, + null, null ]) }) @@ -309,7 +308,7 @@ describe("useCourseSearch", () => { act(() => { // @ts-expect-error wrapper.find(".toggleFacet").prop("onClick")( - "topics", + "topic", "mathematics", true ) @@ -318,11 +317,11 @@ describe("useCourseSearch", () => { "", { ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL, - topics: ["mathematics"] + topic: ["mathematics"] }, 0, null, + null, null ]) }) @@ -332,20 +331,21 @@ describe("useCourseSearch", () => { act(() => { // @ts-expect-error wrapper.find(".toggleFacets").prop("onClick")([ - ["topics", "mathematics", true], - ["type", LearningResourceType.Course, false], - ["type", LearningResourceType.ResourceFile, true] + ["topic", "mathematics", true], + ["resource_type", LearningResourceType.Course, false], + ["resource_type", LearningResourceType.Program, true] ]) }) checkSearchCall(runSearch, [ "", { ...INITIAL_FACET_STATE, - type: [LearningResourceType.ResourceFile], - topics: ["mathematics"] + resource_type: [LearningResourceType.Program], + topic: ["mathematics"] }, 0, null, + null, null ]) }) @@ -357,12 +357,10 @@ describe("useCourseSearch", () => { }) checkSearchCall(runSearch, [ "", - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 10, // from value has been incremented null, + null, null ]) }) @@ -371,41 +369,13 @@ describe("useCourseSearch", () => { const { wrapper } = render() const facetOptions = wrapper.find(".facet-options").prop("onClick") // @ts-expect-error - expect(facetOptions("type")).toEqual({ - buckets: [ - { key: LearningResourceType.Video, doc_count: 8156 }, - { key: LearningResourceType.Course, doc_count: 2508 }, - { key: LearningResourceType.Podcast, doc_count: 1180 } - ] - }) - // @ts-expect-error - expect(facetOptions("topics").buckets.length).toEqual(137) - }) - - it("should merge an empty active facet into the ones from the search backend", async () => { - const { wrapper } = render() - act(() => { - // @ts-expect-error - wrapper.find(".onUpdateFacets").prop("onClick")({ - target: { - // @ts-expect-error - name: "type", - value: "Obstacle Course", - checked: true - } - }) - }) - wrapper.update() - const facetOptions = wrapper.find(".facet-options").prop("onClick") + expect(facetOptions("resource_type")).toEqual([ + { key: LearningResourceType.Video, doc_count: 8156 }, + { key: LearningResourceType.Course, doc_count: 2508 }, + { key: LearningResourceType.Podcast, doc_count: 1180 } + ]) // @ts-expect-error - expect(facetOptions("type")).toEqual({ - buckets: [ - { key: LearningResourceType.Video, doc_count: 8156 }, - { key: LearningResourceType.Course, doc_count: 2508 }, - { key: LearningResourceType.Podcast, doc_count: 1180 }, - { key: "Obstacle Course", doc_count: 0 } - ] - }) + expect(facetOptions("topic").length).toEqual(137) }) it("should update the state when the back button is pressed", async () => { @@ -439,15 +409,14 @@ describe("useCourseSearch", () => { const facets = wrapper.find(FacetTestComponent).prop("activeFacets") expect(facets).toStrictEqual({ - audience: [], - certification: [], - type: [], - offered_by: [], - topics: ["Science"], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: ["Science"], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }) const text = wrapper.find("input").prop("value") expect(text).toBe("sometext") @@ -488,12 +457,10 @@ describe("useCourseSearch", () => { }) checkSearchCall(runSearch, [ "", - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 50, null, + null, null ]) }) @@ -512,12 +479,10 @@ describe("useCourseSearch", () => { }) checkSearchCall(runSearch, [ "", - { - ...INITIAL_FACET_STATE, - type: LR_TYPE_ALL - }, + INITIAL_FACET_STATE, 50, null, + null, null ]) }) @@ -532,31 +497,38 @@ describe("useSearchInputs", () => { text: "", activeFacets: INITIAL_FACET_STATE, sort: null, - ui: null + ui: null, + endpoint: null } }, { descrip: "initial: text, facets, ui", initial: { text: "cat", - activeFacets: { topics: ["math", "bio"] }, - ui: "list" + activeFacets: { topic: ["math", "bio"] }, + ui: "list", + endpoint: null + }, expected: { text: "cat", - activeFacets: { ...INITIAL_FACET_STATE, topics: ["math", "bio"] }, + activeFacets: { ...INITIAL_FACET_STATE, topic: ["math", "bio"] }, ui: "list", - sort: null + sort: null, + endpoint: null + } }, { descrip: "initial: sort", - initial: { sort: { field: "coursenum", option: "asc" } }, + initial: { sort: "coursenum" }, expected: { text: "", activeFacets: INITIAL_FACET_STATE, - sort: { field: "coursenum", option: "asc" }, - ui: null + sort: "coursenum", + ui: null, + endpoint: null + } } ])( @@ -586,14 +558,14 @@ describe("useSearchInputs", () => { act(() => { result.current.setSearchParams({ text: "cat", - activeFacets: { topics: ["math", "bio"] }, + activeFacets: { topic: ["math", "bio"] }, ui: null, sort: null }) }) expect(result.current.searchParams).toEqual({ text: "cat", - activeFacets: { topics: ["math", "bio"] }, + activeFacets: { topic: ["math", "bio"] }, sort: null, ui: null }) @@ -609,7 +581,8 @@ describe("useSearchInputs", () => { text: "cat", activeFacets: INITIAL_FACET_STATE, sort: null, - ui: null + ui: null, + endpoint: null }) }) @@ -625,13 +598,14 @@ describe("useSearchInputs", () => { expect(result.current.text).toEqual("cat") }) - test("clearAllFilters clears text and searchParams", () => { + test("clearAllFilters clears text and searchParams but not ui or endpoint", () => { const initialEntries = [ `?${serializeSearchParams({ text: "cat", - activeFacets: { topics: ["math", "bio"] }, + activeFacets: { topic: ["math", "bio"] }, ui: "list", - sort: { field: "coursenum", option: "asc" } + sort: "coursenum", + endpoint: "endpoint" })}` ] const history = createMemoryHistory({ initialEntries }) @@ -649,7 +623,8 @@ describe("useSearchInputs", () => { text: "", activeFacets: INITIAL_FACET_STATE, sort: null, - ui: null + ui: 'list', + endpoint: 'endpoint' }) expect(result.current.text).toEqual("") }) @@ -658,19 +633,19 @@ describe("useSearchInputs", () => { const initialEntries = [ `?${serializeSearchParams({ text: "cat", - activeFacets: { topics: ["math", "bio"] } + activeFacets: { topic: ["math", "bio"] } })}` ] const history = createMemoryHistory({ initialEntries }) const { result } = renderHook(() => useSearchInputs(history)) act(() => { - result.current.toggleFacet("topics", "math", false) + result.current.toggleFacet("topic", "math", false) }) - expect(result.current.searchParams.activeFacets.topics).toEqual(["bio"]) + expect(result.current.searchParams.activeFacets.topic).toEqual(["bio"]) act(() => { - result.current.toggleFacet("topics", "math", true) + result.current.toggleFacet("topic", "math", true) }) - expect(result.current.searchParams.activeFacets.topics).toEqual([ + expect(result.current.searchParams.activeFacets.topic).toEqual([ "bio", "math" ]) @@ -681,9 +656,9 @@ describe("useSearchInputs", () => { `?${serializeSearchParams({ text: "cat", activeFacets: { - topics: ["math", "bio"], + topic: ["math", "bio"], level: ["beginner"], - department_name: ["physics", "biology"] + department: ["7", "8"] } })}` ] @@ -691,21 +666,21 @@ describe("useSearchInputs", () => { const { result } = renderHook(() => useSearchInputs(history)) act(() => { result.current.toggleFacets([ - ["topics", "chem", true], + ["topic", "chem", true], ["level", "beginner", false], - ["department_name", "chemistry", true] + ["department", "5", true] ]) }) - expect(result.current.searchParams.activeFacets.topics).toEqual([ + expect(result.current.searchParams.activeFacets.topic).toEqual([ "math", "bio", "chem" ]) expect(result.current.searchParams.activeFacets.level).toEqual([]) - expect(result.current.searchParams.activeFacets.department_name).toEqual([ - "physics", - "biology", - "chemistry" + expect(result.current.searchParams.activeFacets.department).toEqual([ + "7", + "8", + "5" ]) }) @@ -713,23 +688,23 @@ describe("useSearchInputs", () => { const initialEntries = [ `?${serializeSearchParams({ text: "cat", - activeFacets: { topics: ["math", "bio"] } + activeFacets: { topic: ["math", "bio"] } })}` ] const history = createMemoryHistory({ initialEntries }) const { result } = renderHook(() => useSearchInputs(history)) act(() => { result.current.onUpdateFacet({ - target: { name: "topics", value: "math", checked: false } + target: { name: "topic", value: "math", checked: false } }) }) - expect(result.current.searchParams.activeFacets.topics).toEqual(["bio"]) + expect(result.current.searchParams.activeFacets.topic).toEqual(["bio"]) act(() => { result.current.onUpdateFacet({ - target: { name: "topics", value: "math", checked: true } + target: { name: "topic", value: "math", checked: true } }) }) - expect(result.current.searchParams.activeFacets.topics).toEqual([ + expect(result.current.searchParams.activeFacets.topic).toEqual([ "bio", "math" ]) @@ -743,7 +718,7 @@ describe("useSearchInputs", () => { expect(result.current.searchParams.text).toEqual("") act(() => { - result.current.toggleFacet("topics", "math", true) + result.current.toggleFacet("topic", "math", true) }) expect(result.current.searchParams.text).toEqual("algebra") }) @@ -756,7 +731,7 @@ describe("useSearchInputs", () => { expect(result.current.searchParams.text).toEqual("") act(() => { - result.current.toggleFacets([["topics", "math", true]]) + result.current.toggleFacets([["topic", "math", true]]) }) expect(result.current.searchParams.text).toEqual("algebra") }) @@ -770,7 +745,7 @@ describe("useSearchInputs", () => { expect(result.current.searchParams.text).toEqual("") act(() => { result.current.onUpdateFacet({ - target: { name: "topics", value: "math", checked: true } + target: { name: "topic", value: "math", checked: true } }) }) expect(result.current.searchParams.text).toEqual("algebra") @@ -814,10 +789,7 @@ describe("useSearchInputs", () => { act(() => { result.current.updateSort({ target: { value: "-title" } }) }) - expect(result.current.searchParams.sort).toEqual({ - field: "title", - option: "desc" - }) + expect(result.current.searchParams.sort).toEqual("-title") expect(result.current.searchParams.text).toBe("algebra") }) @@ -855,7 +827,7 @@ describe("useSyncUrlAndSearch", () => { expect(history.index).toBe(0) act(() => { result.current.setText("algebra") - result.current.toggleFacet("topics", "math", true) + result.current.toggleFacet("topic", "math", true) result.current.submitText() }) @@ -892,7 +864,7 @@ describe("useSyncUrlAndSearch", () => { const [{ result }, history] = setupHook([ `?${serializeSearchParams({ text: "algebra", - activeFacets: { topics: ["math"] } + activeFacets: { topic: ["math"] } })}` ]) @@ -907,6 +879,6 @@ describe("useSyncUrlAndSearch", () => { expect(history.location.search).toBe("?q=algebra&t=math") expect(result.current.searchParams.text).toBe("algebra") - expect(result.current.searchParams.activeFacets.topics).toEqual(["math"]) + expect(result.current.searchParams.activeFacets.topic).toEqual(["math"]) }) }) diff --git a/src/index.ts b/src/index.ts index 8d6e4b2..36be1c8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,22 +6,16 @@ import React, { useMemo, useRef } from "react" -import { unionWith, eqBy, prop, clone } from "ramda" +import { clone } from "ramda" import _ from "lodash" import type { History as HHistory } from "history" -import { - LearningResourceType, - INITIAL_FACET_STATE, - LR_TYPE_ALL -} from "./constants" +import { INITIAL_FACET_STATE } from "./constants" import { FacetsAndSort, Facets, deserializeSearchParams, - deserializeSort, serializeSearchParams, - SortParam, SearchParams } from "./url_utils" import { useEffectAfterMount } from "./hooks" @@ -30,30 +24,19 @@ export * from "./constants" export * from "./url_utils" -export { buildSearchQuery, SearchQueryParams } from "./search" +export { buildSearchUrl } from "./search" export interface Bucket { key: string doc_count: number // eslint-disable-line camelcase } -export type Aggregation = { - doc_count_error_upper_bound?: number // eslint-disable-line camelcase - sum_other_doc_count?: number // eslint-disable-line camelcase - buckets: Bucket[] -} +export type Aggregation = Bucket[] -export type Aggregations = Map +export type Aggregations = Map export type GetSearchPageSize = (ui: string | null) => number -export const mergeFacetResults = (...args: Aggregation[]): Aggregation => ({ - buckets: args - .map(prop("buckets")) - // @ts-ignore - .reduce((buckets, acc) => unionWith(eqBy(prop("key")), buckets, acc)) -}) - /** * Accounts for a difference in the listener API for v4 and v5. * See https://github.com/remix-run/history/issues/811 @@ -78,22 +61,18 @@ export const useFacetOptions = ( ): ((group: string) => Aggregation | null) => { return useCallback( (group: string) => { - const emptyFacet = { buckets: [] } - const emptyActiveFacets = { - buckets: (activeFacets[group] || []).map((facet: string) => ({ + const emptyActiveFacets = (activeFacets[group] || []).map( + (facet: string) => ({ key: facet, doc_count: 0 - })) - } + }) + ) if (!aggregations) { return null } - return mergeFacetResults( - aggregations.get(group) || emptyFacet, - emptyActiveFacets - ) + return aggregations.get(group) || emptyActiveFacets }, [aggregations, activeFacets] ) @@ -157,6 +136,7 @@ type UseSearchInputsResult = { * Set `searchParams.text` to the current value of `text`. */ submitText: () => void + updateEndpoint: (newEndpoint: string | null) => void } /** @@ -199,11 +179,14 @@ export const useSearchInputs = (history: HHistory): UseSearchInputsResult => { }, []) const clearAllFilters = useCallback(() => { - setSearchParams({ - text: "", - sort: null, - ui: null, - activeFacets: INITIAL_FACET_STATE + setSearchParams(current => { + return { + text: "", + sort: null, + ui: current.ui, + activeFacets: INITIAL_FACET_STATE, + endpoint: current.endpoint + } }) setText("") }, [setText]) @@ -271,8 +254,7 @@ export const useSearchInputs = (history: HHistory): UseSearchInputsResult => { const updateSort: UseSearchInputsResult["updateSort"] = useCallback( (event): void => { - const param = event ? (event.target as HTMLSelectElement).value : "" - const newSort = deserializeSort(param) + const newSort = event ? (event.target as HTMLSelectElement).value : "" setSearchParams(current => ({ ...current, sort: newSort, @@ -290,6 +272,14 @@ export const useSearchInputs = (history: HHistory): UseSearchInputsResult => { })) }, []) + const updateEndpoint = useCallback((newEndpoint: string | null): void => { + setSearchParams(current => ({ + ...current, + endpoint: newEndpoint, + text: textRef.current + })) + }, []) + const clearText = useCallback(() => { setText("") setSearchParams(current => ({ ...current, text: "" })) @@ -315,6 +305,7 @@ export const useSearchInputs = (history: HHistory): UseSearchInputsResult => { updateSort, clearText, updateUI, + updateEndpoint, submitText } } @@ -323,12 +314,13 @@ const setLocation = (history: HHistory, searchParams: SearchParams) => { const currentSearch = serializeSearchParams( deserializeSearchParams(history.location) ) - const { activeFacets, sort, ui, text } = searchParams + const { activeFacets, sort, ui, text, endpoint } = searchParams const newSearch = serializeSearchParams({ text, activeFacets, sort, - ui + ui, + endpoint }) if (currentSearch !== newSearch) { const prefix = newSearch ? "?" : "" @@ -354,8 +346,9 @@ export const useSyncUrlAndSearch = ( // sync URL to search useEffect(() => { const unlisten = history4or5Listen(history, location => { - const { activeFacets, sort, ui, text } = deserializeSearchParams(location) - setSearchParams({ activeFacets, sort, ui, text }) + const { activeFacets, sort, ui, text, endpoint } = + deserializeSearchParams(location) + setSearchParams({ activeFacets, sort, ui, text, endpoint }) setText(text) }) return unlisten @@ -383,7 +376,7 @@ interface CourseSearchResult { loadMore: () => void incremental: boolean text: string - sort: SortParam | null + sort: string | null activeFacets: Facets /** * Callback that handles search submission. Pass this to your search input @@ -397,6 +390,8 @@ interface CourseSearchResult { from: number updateUI: (newUI: string | null) => void ui: string | null + updateEndpoint: (newEndpoint: string | null) => void + endpoint: string | null } export const useCourseSearch = ( @@ -404,8 +399,9 @@ export const useCourseSearch = ( text: string, searchFacets: Facets, nextFrom: number, - sort?: SortParam | null, - ui?: string | null + sort?: string | null, + ui?: string | null, + endpoint?: string | null ) => Promise, clearSearch: () => void, aggregations: Aggregations, @@ -428,12 +424,13 @@ export const useCourseSearch = ( onUpdateFacet: onUpdateFacets, updateText, updateSort, - updateUI + updateUI, + updateEndpoint } = seachUI - const { activeFacets, sort, ui } = searchParams + const { activeFacets, sort, ui, endpoint } = searchParams const activeFacetsAndSort = useMemo( - () => ({ activeFacets, sort, ui }), - [activeFacets, sort, ui] + () => ({ activeFacets, sort, ui, endpoint }), + [activeFacets, sort, ui, endpoint] ) const facetOptions = useFacetOptions(aggregations, activeFacets) @@ -443,7 +440,7 @@ export const useCourseSearch = ( activeFacetsAndSort: FacetsAndSort, incremental = false ) => { - const { activeFacets, sort, ui } = activeFacetsAndSort + const { activeFacets, sort, ui, endpoint } = activeFacetsAndSort const currentPageSize = typeof searchPageSize === "number" ? searchPageSize : searchPageSize(ui) @@ -459,21 +456,9 @@ export const useCourseSearch = ( const searchFacets = clone(activeFacets) - if (searchFacets.type !== undefined && searchFacets.type.length > 0) { - if (searchFacets.type.includes(LearningResourceType.Podcast)) { - searchFacets.type.push(LearningResourceType.PodcastEpisode) - } + await runSearch(text, searchFacets, nextFrom, sort, ui, endpoint) - if (searchFacets.type.includes(LearningResourceType.Userlist)) { - searchFacets.type.push(LearningResourceType.LearningPath) - } - } else { - searchFacets.type = LR_TYPE_ALL - } - - await runSearch(text, searchFacets, nextFrom, sort, ui) - - setLocation(history, { text, activeFacets, sort, ui }) + setLocation(history, { text, activeFacets, sort, ui, endpoint }) }, [ from, @@ -488,10 +473,17 @@ export const useCourseSearch = ( const initSearch = useCallback( (location: { search: string }) => { - const { text, activeFacets, sort, ui } = deserializeSearchParams(location) + const { text, activeFacets, sort, ui, endpoint } = + deserializeSearchParams(location) clearSearch() setText(text) - setSearchParams(current => ({ ...current, activeFacets, sort, ui })) + setSearchParams(current => ({ + ...current, + activeFacets, + sort, + ui, + endpoint + })) }, [clearSearch, setText, setSearchParams] ) @@ -588,6 +580,8 @@ export const useCourseSearch = ( onSubmit, from, updateUI, - ui + ui, + endpoint, + updateEndpoint } } diff --git a/src/search.test.ts b/src/search.test.ts index fcfc00b..b030650 100644 --- a/src/search.test.ts +++ b/src/search.test.ts @@ -1,973 +1,22 @@ -import bodybuilder from "bodybuilder" - -import { - INITIAL_FACET_STATE, - LearningResourceType, - LR_TYPE_ALL -} from "./constants" - -import { - LEARN_SUGGEST_FIELDS, - RESOURCE_QUERY_NESTED_FIELDS, - RESOURCEFILE_QUERY_FIELDS, - searchFields, - buildSuggestQuery, - buildSearchQuery, - buildDefaultSort, - buildLearnQuery, - channelField -} from "./search" - -import { Facets } from "./url_utils" - -describe("search library", () => { - let activeFacets: Facets - - beforeEach(() => { - activeFacets = { - ...INITIAL_FACET_STATE, - type: [LearningResourceType.Course] - } - }) - - it("builds a search query with empty values", () => { - expect(buildSearchQuery({})).toStrictEqual({ - query: { - bool: { - should: ["comment", "post", "profile"].map(objectType => ({ - bool: { - filter: { - bool: { - must: [{ term: { object_type: objectType } }] - } - } - } - })) - } - } - }) - }) - - it("form a basic text query", () => { - // @ts-ignore - const { query } = buildSearchQuery({ - text: "Dogs are the best", - activeFacets - }) - const repeatedPart = { - should: [ - { - multi_match: { - query: "Dogs are the best", - fields: searchFields(LearningResourceType.Course) - } - }, - { - wildcard: { - coursenum: { - boost: 100, - rewrite: "constant_score", - value: "DOGS ARE THE BEST*" - } - } - }, - { - nested: { - path: "runs", - query: { - multi_match: { - query: "Dogs are the best", - fields: RESOURCE_QUERY_NESTED_FIELDS - } - } - } - }, - { - has_child: { - type: "resourcefile", - query: { - multi_match: { - query: "Dogs are the best", - fields: RESOURCEFILE_QUERY_FIELDS - } - }, - score_mode: "avg" - } - } - ] - } - - expect(query).toStrictEqual({ - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: LearningResourceType.Course - } - }, - { - bool: repeatedPart - } - ] - } - }, - ...repeatedPart - } - } - ] - } - }) - }) - - it(`only performs aggregations for the facets selected in the aggregations array`, () => { - const text = "" - const facets = { - offered_by: ["MITx"], - topics: ["Engineering", "Science"], - type: [LearningResourceType.Course] - } - - expect( - buildSearchQuery({ - text: text, - activeFacets: facets, - aggregations: ["offered_by"] - }) - ).toStrictEqual({ - aggs: { - agg_filter_offered_by: { - aggs: { - offered_by: { - terms: { - field: "offered_by", - size: 10000 - } - } - }, - filter: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - topics: "Engineering" - } - }, - { - term: { - topics: "Science" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - } - } - } - ] - } - } - } - }, - post_filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - offered_by: "MITx" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - topics: "Engineering" - } - }, - { - term: { - topics: "Science" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - }, - query: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: "course" - } - } - ] - } - } - } - } - ] - } - } - }) - }) - - it("filters on object type", () => { - expect( - buildSearchQuery({ - activeFacets: { type: ["course"] }, - aggregations: ["type"] - }) - ).toStrictEqual({ - aggs: { - type: { - terms: { - field: "object_type.keyword", - size: 10000 - } - } - }, - post_filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - }, - query: { - bool: { - should: ["course"].map(objectType => ({ - bool: { - filter: { - bool: { - must: [{ term: { object_type: objectType } }] - } - } - } - })) - } - } - }) - }) - - it("filters on object type but does not exclude other types from aggregations when resourceTypes is set", () => { - expect( - buildSearchQuery({ - activeFacets: { type: ["course"] }, - resourceTypes: ["course", "program"], - aggregations: ["type"] - }) - ).toStrictEqual({ - aggs: { - type: { - terms: { - field: "object_type.keyword", - size: 10000 - } - } - }, - post_filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - }, - query: { - bool: { - should: ["course", "program"].map(objectType => ({ - bool: { - filter: { - bool: { - must: [{ term: { object_type: objectType } }] - } - } - } - })) - } - } - }) - }) - - it(`filters by platform and topics`, () => { - const text = "" - const facets = { - offered_by: ["MITx"], - topics: ["Engineering", "Science"], - type: [LearningResourceType.Course] - } - - expect( - // @ts-ignore - buildSearchQuery({ - text: text, - activeFacets: facets, - aggregations: ["topics", "offered_by"] - }) - ).toStrictEqual({ - aggs: { - agg_filter_offered_by: { - aggs: { - offered_by: { - terms: { - field: "offered_by", - size: 10000 - } - } - }, - filter: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - topics: "Engineering" - } - }, - { - term: { - topics: "Science" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - } - } - } - ] - } - } - }, - agg_filter_topics: { - aggs: { - topics: { - terms: { - field: "topics", - size: 10000 - } - } - }, - filter: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - offered_by: "MITx" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - } - } - } - ] - } - } - } - }, - post_filter: { - bool: { - must: [ - { - bool: { - should: [ - { - term: { - offered_by: "MITx" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - topics: "Engineering" - } - }, - { - term: { - topics: "Science" - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - } - ] - } - }, - query: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: "course" - } - } - ] - } - } - } - } - ] - } - } - }) - }) - - it("should do a nested query for level", () => { - activeFacets["level"] = ["Undergraduate"] - //eslint-disable-next-line camelcase - const { query, post_filter, aggs } = buildSearchQuery({ - text: "", - activeFacets, - aggregations: ["level"] - }) - expect(query).toStrictEqual({ - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: LearningResourceType.Course - } - } - ] - } - } - } - } - ] - } - }) - - // this is the part of aggregation specific to the nesting - expect(aggs.agg_filter_level.aggs).toStrictEqual({ - level: { - aggs: { - level: { - aggs: { - courses: { - reverse_nested: {} - } - }, - terms: { - field: "runs.level", - size: 10000 - } - } - }, - nested: { - path: "runs" - } - } - }) - - expect(post_filter).toStrictEqual({ - bool: { - must: [ - { - bool: { - should: [ - { - term: { - "object_type.keyword": "course" - } - } - ] - } - }, - { - bool: { - should: [ - { - nested: { - path: "runs", - query: { - match: { - "runs.level": "Undergraduate" - } - } - } - } - ] - } - } - ] - } - }) - }) - - it("should include an appropriate resource query and aggregation for resource_type ", () => { - //eslint-disable-next-line camelcase - const { query, post_filter, aggs } = buildSearchQuery({ - text: "", +import { buildSearchUrl } from "./search" + +describe("buildSearchUrl", () => { + it("should build a GET request url from the parameters and base url", () => { + const params = { + text: "The Best Course", + from: 10, + size: 20, + sort: 'sort', activeFacets: { - ...INITIAL_FACET_STATE, - resource_type: ["Exams"], - type: [LearningResourceType.ResourceFile], - offered_by: ["OCW"] + platform: ['mitx', 'ocw'], + department: ['2'] }, - aggregations: ["resource_type", "type", "offered_by"] - }) - - expect(query).toStrictEqual({ - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: LearningResourceType.ResourceFile - } - } - ] - } - } - } - } - ] - } - }) - - expect(aggs.agg_filter_resource_type.aggs).toStrictEqual({ - resource_type: { - terms: { - field: "resource_type", - size: 10000 - } - } - }) - - expect(post_filter).toStrictEqual({ - bool: { - must: [ - { - bool: { - should: [ - { - has_parent: { - parent_type: "resource", - query: { - bool: { - should: [ - { - term: { - offered_by: "OCW" - } - } - ] - } - } - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - "object_type.keyword": LearningResourceType.ResourceFile - } - } - ] - } - }, - { - bool: { - should: [ - { - term: { - resource_type: "Exams" - } - } - ] - } - } - ] - } - }) - }) - - it("should include suggest query, if text", () => { - expect( - // @ts-ignore - buildSearchQuery({ text: "text!", activeFacets }).suggest - ).toStrictEqual(buildSuggestQuery("text!", LEARN_SUGGEST_FIELDS)) - // @ts-ignore - expect(buildSearchQuery({ activeFacets }).suggest).toBeUndefined() - }) - - it("should set from, size values", () => { - // @ts-ignore - const query = buildSearchQuery({ from: 10, size: 100, activeFacets }) - // @ts-ignore - expect(query.from).toBe(10) - // @ts-ignore - expect(query.size).toBe(100) - }) - - // - ;[ - [null, LearningResourceType.Course, undefined, []], - [undefined, LearningResourceType.Course, undefined, []], - [ - { field: "nested.field", option: "desc" }, - LearningResourceType.ResourceFile, - undefined, - [] - ], - [ - { field: "nested.field", option: "desc" }, - LearningResourceType.Course, - [{ "nested.field": { order: "desc", nested: { path: "nested" } } }], - [] - ], - [ - { field: "department_course_numbers.sort_coursenum", option: "asc" }, - LearningResourceType.Course, - [ - { - "department_course_numbers.sort_coursenum": { - nested: { - filter: { - term: { - "department_course_numbers.primary": true - } - }, - path: "department_course_numbers" - }, - order: "asc" - } - } - ], - [] - ], - [ - { field: "department_course_numbers.sort_coursenum", option: "asc" }, - LearningResourceType.Course, - [ - { - "department_course_numbers.sort_coursenum": { - nested: { - filter: { - bool: { - should: [ - { - term: { - "department_course_numbers.department": "Physics" - } - } - ] - } - }, - path: "department_course_numbers" - }, - order: "asc" - } - } - ], - ["Physics"] - ], - [ - { field: "department_course_numbers.sort_coursenum", option: "asc" }, - LearningResourceType.ResourceFile, - undefined, - [] - ] - ].forEach(([sortField, type, expectedSort, departmentFilter]) => { - it(`should add a sort option if field is ${JSON.stringify( - sortField - )} and type is ${type}`, () => { - // @ts-ignore - activeFacets["type"] = [type] - // @ts-ignore - activeFacets["department_name"] = departmentFilter - // @ts-ignore - const query = buildSearchQuery({ sort: sortField, activeFacets }) - // @ts-ignore - expect(query.sort).toStrictEqual(expectedSort) - }) - }) - - it(`sorts the search results when there are no filters or text`, () => { - const query = buildLearnQuery(bodybuilder(), null, LR_TYPE_ALL, {}) - expect(query.sort).toStrictEqual(buildDefaultSort()) - }) - - it("filters by channelName", () => { - const type = LearningResourceType.Comment - const channelName = "a_channel" - expect( - buildSearchQuery({ channelName, activeFacets: { type: [type] } }) - ).toStrictEqual({ - query: { - bool: { - should: [ - { - bool: { - filter: { - bool: { - must: [ - { - term: { - object_type: type - } - }, - { - term: { - ["channel_name"]: channelName - } - } - ] - } - } - } - } - ] - } - } - }) - }) - - describe("channelField", () => { - [ - [LearningResourceType.Post, "channel_name"], - [LearningResourceType.Comment, "channel_name"], - [LearningResourceType.Profile, "author_channel_membership"] - ].forEach(([type, field]) => { - it(`has the right channelField for ${type}`, () => { - expect(channelField(type as LearningResourceType)).toStrictEqual(field) - }) - }) - }) - - describe("searchFields", () => { - [ - ["post", ["text.english", "post_title.english", "plain_text.english"]], - ["comment", ["text.english"]], - [ - "profile", - ["author_headline.english", "author_bio.english", "author_name.english"] - ], - [ - "course", - [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "topics", - "platform", - "course_id", - "offered_by", - "department_name", - "course_feature_tags" - ] - ], - ["program", ["title.english^3", "short_description.english^2", "topics"]], - [ - "userlist", - ["title.english^3", "short_description.english^2", "topics"] - ], - [ - "video", - [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "transcript.english^2", - "topics", - "platform", - "video_id", - "offered_by" - ] - ] - ].forEach(([type, fields]) => { - it(`has the right searchFields for ${type}`, () => { - expect(searchFields(type as LearningResourceType)).toStrictEqual(fields) - }) - }) - }) - ;[ - ['"mechanical engineering"', "query_string"], - ["'mechanical engineering\"", "multi_match"], - ["'mechanical engineering'", "multi_match"], - ["mechanical engineering", "multi_match"] - ].forEach(([text, queryType]) => { - it(`constructs a ${queryType} query on text ${text}`, () => { - const esQuery = buildLearnQuery(bodybuilder(), text, [ - LearningResourceType.Course - ]) - expect( - Object.keys(esQuery.query.bool.should[0].bool.should[0]) - ).toStrictEqual([queryType]) - }) - }) + aggregations: ['platform'] + } - it("should include a childQuery for course type", () => { - const text = "search query" - const esQuery = buildLearnQuery(bodybuilder(), text, [ - LearningResourceType.Course - ]) - expect(esQuery.query.bool.should[0].bool.should.length).toStrictEqual(4) - expect(esQuery.query.bool.should[0].bool.should[3]).toStrictEqual({ - has_child: { - type: "resourcefile", - query: { - multi_match: { - query: text, - fields: [ - "content", - "title.english^3", - "short_description.english^2", - "department_name", - "resource_type" - ] - } - }, - score_mode: "avg" - } - }) + expect(buildSearchUrl('http://www.base.edu/', params)).toEqual( + "http://www.base.edu/?q=The+Best+Course&offset=10&limit=20&sortby=sort&aggregations=platform&platform=mitx%2Cocw&department=2" + ) }) }) diff --git a/src/search.ts b/src/search.ts index 6d4cf79..0e1fe78 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,626 +1,46 @@ -import bodybuilder, { Bodybuilder } from "bodybuilder" -import { either, isEmpty, isNil, uniq, intersection, equals } from "ramda" +import { Facets } from "./url_utils" -import { - LearningResourceType, - COURSENUM_SORT_FIELD, - Level, - LR_TYPE_ALL -} from "./constants" -import { SortParam, Facets } from "./url_utils" -const PODCAST_QUERY_FIELDS = [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "topics" -] - -export const LEARN_SUGGEST_FIELDS = [ - "title.trigram", - "short_description.trigram" -] - -const CHANNEL_SUGGEST_FIELDS = ["suggest_field1", "suggest_field2"] - -const PODCAST_EPISODE_QUERY_FIELDS = [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "topics", - "series_title^2" -] - -const COURSE_QUERY_FIELDS = [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "topics", - "platform", - "course_id", - "offered_by", - "department_name", - "course_feature_tags" -] -const VIDEO_QUERY_FIELDS = [ - "title.english^3", - "short_description.english^2", - "full_description.english", - "transcript.english^2", - "topics", - "platform", - "video_id", - "offered_by" -] - -const LIST_QUERY_FIELDS = [ - "title.english^3", - "short_description.english^2", - "topics" -] - -const POST_QUERY_FIELDS = [ - "text.english", - "post_title.english", - "plain_text.english" -] -const COMMENT_QUERY_FIELDS = ["text.english"] -const PROFILE_QUERY_FIELDS = [ - "author_headline.english", - "author_bio.english", - "author_name.english" -] - -export const RESOURCE_QUERY_NESTED_FIELDS = [ - "runs.year", - "runs.semester", - "runs.level", - "runs.instructors^5", - "department_name" -] - -export const RESOURCEFILE_QUERY_FIELDS = [ - "content", - "title.english^3", - "short_description.english^2", - "department_name", - "resource_type" -] - -const OBJECT_TYPE = "type" -const POST_CHANNEL_FIELD = "channel_name" -const COMMENT_CHANNEL_FIELD = "channel_name" -const PROFILE_CHANNEL_FIELD = "author_channel_membership" - -export const SEARCH_FILTER_POST = "post" -export const SEARCH_FILTER_COMMENT = "comment" -export const SEARCH_FILTER_PROFILE = "profile" - -export const channelField = (type: LearningResourceType): string => { - if (type === LearningResourceType.Post) { - return POST_CHANNEL_FIELD - } else if (type === LearningResourceType.Comment) { - return COMMENT_CHANNEL_FIELD - } else if (type === LearningResourceType.Profile) { - return PROFILE_CHANNEL_FIELD - } else { - throw new Error("Missing type") - } -} - -export const searchFields = (type: LearningResourceType): string[] => { - switch (type) { - case LearningResourceType.Course: - return COURSE_QUERY_FIELDS - case LearningResourceType.Video: - return VIDEO_QUERY_FIELDS - case LearningResourceType.Podcast: - return PODCAST_QUERY_FIELDS - case LearningResourceType.PodcastEpisode: - return PODCAST_EPISODE_QUERY_FIELDS - case LearningResourceType.ResourceFile: - return RESOURCEFILE_QUERY_FIELDS - case LearningResourceType.Comment: - return COMMENT_QUERY_FIELDS - case LearningResourceType.Post: - return POST_QUERY_FIELDS - case LearningResourceType.Profile: - return PROFILE_QUERY_FIELDS - case LearningResourceType.Program: - case LearningResourceType.Userlist: - case LearningResourceType.LearningPath: - return LIST_QUERY_FIELDS - default: - return uniq([ - ...POST_QUERY_FIELDS, - ...COMMENT_QUERY_FIELDS, - ...PROFILE_QUERY_FIELDS - ]) - } -} - -export const isDoubleQuoted = (str: string | null | undefined): boolean => - /^".+"$/.test(normalizeDoubleQuotes(str) || "") - -export const normalizeDoubleQuotes = ( - text: string | null | undefined -): string => (text || "").replace(/[\u201C\u201D]/g, '"') - -export const emptyOrNil = either(isEmpty, isNil) - -/** - Interface for parameters for generating a search query. Supported fields are text, from, size, sort, channelName - and activeFacets. activeFacets supports audience, certification, type, offered_by, topics, department_name, level, - course_feature_tags and resource_type as nested params -*/ export interface SearchQueryParams { text?: string from?: number size?: number - sort?: SortParam + sort?: string activeFacets?: Facets - channelName?: string - resourceTypes?: string[] aggregations?: string[] } -const getTypes = (activeFacets: Facets | undefined) => { - if (activeFacets?.type) { - return activeFacets.type - } else { - return [SEARCH_FILTER_COMMENT, SEARCH_FILTER_POST, SEARCH_FILTER_PROFILE] - } -} - -/** - Generates an elasticsearch query object with nested string parameters from inputs of type SearchQueryParams. -*/ -export const buildSearchQuery = ({ - text, - from, - size, - sort, - activeFacets, - channelName, - resourceTypes, - aggregations -}: SearchQueryParams): Record => { - let builder = bodybuilder() - - if (!isNil(from)) { - builder = builder.from(from) - } - if (!isNil(size)) { - builder = builder.size(size) - } - if ( - sort && - activeFacets && - !(activeFacets.type ?? []).includes(LearningResourceType.ResourceFile) - ) { - const { field, option } = sort - const fieldPieces = field.split(".") - - const sortQuery = { - order: option, - nested: { - path: fieldPieces[0] - } - } - - if (field === COURSENUM_SORT_FIELD) { - if ((activeFacets.department_name ?? []).length === 0) { - sortQuery["nested"]["filter"] = { - term: { - "department_course_numbers.primary": true - } - } - } else { - const filterClause: any[] = [] - addFacetClauseToArray( - filterClause, - "department_course_numbers.department", - activeFacets.department_name || [], - LearningResourceType.Course - ) - sortQuery["nested"]["filter"] = filterClause[0] - } - } - - builder.sort(field, sortQuery) - } - - const types = resourceTypes ?? getTypes(activeFacets) - const searchText = normalizeDoubleQuotes(text) - return emptyOrNil( - intersection([...LR_TYPE_ALL, LearningResourceType.ResourceFile], types) - ) ? - buildChannelQuery(builder, searchText, types, channelName) : - buildLearnQuery(builder, searchText, types, activeFacets, aggregations) -} - -export const buildChannelQuery = ( - builder: Bodybuilder, - text: string | null, - types: Array, - channelName: string | undefined -): Record => { - for (const type of types) { - const textQuery = emptyOrNil(text) ? - {} : - { - should: [ - { - multi_match: { - query: text, - fields: searchFields(type as LearningResourceType) - } - } - ].filter(clause => clause !== null) - } - - // If channelName is present add a filter for the type - const channelClauses = channelName ? - [ - { - term: { - [channelField(type as LearningResourceType)]: channelName - } - } - ] : - [] - - builder = buildOrQuery(builder, type, textQuery, channelClauses) - } - - if (!emptyOrNil(text)) { - builder = builder.rawOption( - "suggest", - // @ts-expect-error - buildSuggestQuery(text, CHANNEL_SUGGEST_FIELDS) - ) - } - - return builder.build() -} - -export const buildLearnQuery = ( - builder: Bodybuilder, - text: string | null, - types: Array, - facets?: Facets, - aggregations?: Array -): Record => { - for (const type of types) { - const queryType = isDoubleQuoted(text) ? "query_string" : "multi_match" - const textQuery = emptyOrNil(text) ? - {} : - { - should: [ - { - [queryType]: { - query: text, - fields: searchFields(type as LearningResourceType) - } - }, - { - wildcard: { - coursenum: { - value: `${(text || "").toUpperCase()}*`, - boost: 100.0, - rewrite: "constant_score" - } - } - }, - [ - LearningResourceType.Course, - LearningResourceType.Program - ].includes(type as LearningResourceType) ? - { - nested: { - path: "runs", - query: { - [queryType]: { - query: text, - fields: RESOURCE_QUERY_NESTED_FIELDS - } - } - } - } : - null, - type === LearningResourceType.Course ? - { - has_child: { - type: "resourcefile", - query: { - [queryType]: { - query: text, - fields: RESOURCEFILE_QUERY_FIELDS - } - }, - score_mode: "avg" - } - } : - null - ] - .flat() - .filter(clause => clause !== null) - } +export const buildSearchUrl = ( + baseUrl: string, + { text, from, size, sort, activeFacets, aggregations }: SearchQueryParams +): string => { + const url = new URL(baseUrl) - // Add filters for facets if necessary - const facetClauses = buildFacetSubQuery(facets, builder, type, aggregations) - builder = buildOrQuery(builder, type, textQuery, []) - builder = builder.rawOption("post_filter", { - bool: { - must: [...facetClauses] - } - }) - - // Include suggest if search test is not null/empty - if (!emptyOrNil(text)) { - builder = builder.rawOption( - "suggest", - // @ts-expect-error - buildSuggestQuery(text, LEARN_SUGGEST_FIELDS) - ) - } else if (facetClauses.length === 0 && equals(types, LR_TYPE_ALL)) { - builder = builder.rawOption("sort", buildDefaultSort()) - } + if (text) { + url.searchParams.append("q", text) } - return builder.build() -} - -interface LevelFilter { - nested: { - path: "runs" - query: { - match: { - "runs.level": Level - } - } + if (from) { + url.searchParams.append("offset", from.toString()) } -} -const buildLevelQuery = ( - _builder: Bodybuilder, - values: Level[], - facetClauses: any -) => { - if (values && values.length > 0) { - const facetFilter: LevelFilter[] = values.map(value => ({ - nested: { - path: "runs", - query: { - match: { - "runs.level": value - } - } - } - })) - facetClauses.push({ - bool: { - should: facetFilter - } - }) + if (size) { + url.searchParams.append("limit", size.toString()) } -} - -export const buildFacetSubQuery = ( - facets: Facets | undefined, - builder: Bodybuilder, - objectType?: string, - aggregations?: string[] -): any[] => { - const facetClauses: any[] = [] - if (facets) { - Object.entries(facets).forEach(([key, values]) => { - const facetClausesForFacet: any[] = [] - - if (values && values.length > 0) { - if (key === "level") { - buildLevelQuery(builder, values, facetClauses) - } else { - addFacetClauseToArray(facetClauses, key, values, objectType) - } - } - if (aggregations && aggregations.includes(key)) { - // $FlowFixMe: we check for null facets earlier - Object.entries(facets).forEach(([otherKey, otherValues]) => { - if (otherKey !== key && otherValues && otherValues.length > 0) { - if (otherKey === "level") { - buildLevelQuery(builder, otherValues, facetClausesForFacet) - } else { - addFacetClauseToArray( - facetClausesForFacet, - otherKey, - otherValues, - objectType - ) - } - } - }) - - if (facetClausesForFacet.length > 0) { - const filter = { - filter: { - bool: { - must: [...facetClausesForFacet] - } - } - } - - if (key === "level") { - // this is done seperately b/c it's a nested field - builder.agg("filter", key, aggr => - aggr - .orFilter("bool", filter) - .agg("nested", { path: "runs" }, "level", aggr => - aggr.agg( - "terms", - "runs.level", - { size: 10000 }, - "level", - aggr => - aggr.agg("reverse_nested", null as any, {}, "courses") - ) - ) - ) - } else { - builder.agg("filter", key, aggregation => - aggregation - .orFilter("bool", filter) - .agg( - "terms", - key === OBJECT_TYPE ? "object_type.keyword" : key, - { size: 10000 }, - key - ) - ) - } - } else { - if (key === "level") { - // this is done seperately b/c it's a nested field - builder.agg("nested", { path: "runs" }, "level", aggr => - aggr.agg( - "terms", - "runs.level", - { - size: 10000 - }, - "level", - aggr => aggr.agg("reverse_nested", null as any, {}, "courses") - ) - ) - } else { - builder.agg( - "terms", - key === OBJECT_TYPE ? "object_type.keyword" : key, - { size: 10000 }, - key - ) - } - } - } - }) + if (sort) { + url.searchParams.append("sortby", sort) } - return facetClauses -} - -export const buildOrQuery = ( - builder: Bodybuilder, - searchType: string, - textQuery: Record | undefined, - extraClauses: any[] -): Bodybuilder => { - const textFilter = emptyOrNil(textQuery) ? [] : [{ bool: textQuery }] - builder = builder.orQuery("bool", { - filter: { - bool: { - must: [ - { - term: { - object_type: searchType - } - }, - ...extraClauses, - // Add multimatch text query here to filter out non-matching results - ...textFilter - ] - } - }, - // Add multimatch text query here again to score results based on match - ...textQuery - }) - return builder -} - -const addFacetClauseToArray = ( - facetClauses: any[], - facet: string, - values: string[], - type?: string -) => { - if ( - facet === OBJECT_TYPE && - values.toString() === buildSearchQuery.toString() - ) { - return + if (aggregations && aggregations.length > 0) { + url.searchParams.append("aggregations", aggregations.join(",")) } - const filterKey = facet === OBJECT_TYPE ? "object_type.keyword" : facet - let valueClauses - // Apply standard facet clause unless this is an offered_by facet for resources. - if (facet !== "offered_by" || type !== LearningResourceType.ResourceFile) { - valueClauses = values.map(value => ({ - term: { - [filterKey]: value + if (activeFacets) { + for (const [key, value] of Object.entries(activeFacets)) { + if (value && value.length > 0) { + url.searchParams.append(key, value.join(",")) } - })) - } else { - // offered_by facet should apply to parent doc of resource - valueClauses = [ - { - has_parent: { - parent_type: "resource", - query: { - bool: { - should: values.map(value => ({ - term: { - [filterKey]: value - } - })) - } - } - } - } - ] - } - - facetClauses.push({ - bool: { - should: valueClauses } - }) -} - -export const buildSuggestQuery = ( - text: string, - suggestFields: string[] -): Record => { - const suggest: any = { - text } - suggestFields.forEach( - field => - (suggest[field] = { - phrase: { - field: `${field}`, - size: 5, - gram_size: 1, - confidence: 0.0001, - max_errors: 3, - collate: { - query: { - source: { - match_phrase: { - "{{field_name}}": "{{suggestion}}" - } - } - }, - params: { field_name: `${field}` }, - prune: true - } - } - }) - ) - return suggest -} -export const buildDefaultSort = (): Array => { - return [ - { minimum_price: { order: "asc" } }, - { default_search_priority: { order: "desc" } }, - { created: { order: "desc" } } - ] + return url.toString() } diff --git a/src/test_util.ts b/src/test_util.ts index 1e412fa..0aefe16 100644 --- a/src/test_util.ts +++ b/src/test_util.ts @@ -1,439 +1,183 @@ export const wait = (millis: number): Promise => new Promise(resolve => setTimeout(resolve, millis)) -export const facetMap = new Map([ +export const facetMap = new Map( [ - "agg_filter_topics", - { - doc_count: 11844, - topics: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "Physics", doc_count: 6568 }, - { key: "Engineering", doc_count: 5600 }, - { key: "Computer Science", doc_count: 5325 }, - { key: "Science", doc_count: 2006 }, - { key: "Economics", doc_count: 1957 }, - { key: "Probability and Statistics", doc_count: 1764 }, - { key: "Media Studies", doc_count: 1607 }, - { key: "Teaching and Education", doc_count: 1170 }, - { key: "Business", doc_count: 939 }, - { key: "Social Science", doc_count: 669 }, - { key: "Humanities", doc_count: 566 }, - { key: "Electrical Engineering", doc_count: 524 }, - { key: "Communication", doc_count: 433 }, - { key: "Mathematics", doc_count: 291 }, - { key: "Fine Arts", doc_count: 285 }, - { key: "Biology", doc_count: 239 }, - { key: "History", doc_count: 213 }, - { key: "Literature", doc_count: 199 }, - { key: "Health and Medicine", doc_count: 184 }, - { key: "Public Administration", doc_count: 173 }, - { key: "Society", doc_count: 166 }, - { key: "Political Science", doc_count: 162 }, - { key: "Systems Engineering", doc_count: 159 }, - { key: "Mechanical Engineering", doc_count: 144 }, - { key: "Anthropology", doc_count: 143 }, - { key: "Philosophy", doc_count: 133 }, - { key: "Earth Science", doc_count: 129 }, - { key: "Chemistry", doc_count: 127 }, - { key: "Visual Arts", doc_count: 125 }, - { key: "Urban Studies", doc_count: 111 }, - { key: "Physical Education and Recreation", doc_count: 106 }, - { key: "Architecture", doc_count: 101 }, - { key: "Management", doc_count: 96 }, - { key: "Sociology", doc_count: 87 }, - { key: "Cognitive Science", doc_count: 83 }, - { key: "Business & Management", doc_count: 81 }, - { key: "Language", doc_count: 75 }, - { key: "Applied Mathematics", doc_count: 74 }, - { key: "Materials Science and Engineering", doc_count: 67 }, - { key: "Data Analysis & Statistics", doc_count: 65 }, - { key: "Biological Engineering", doc_count: 62 }, - { key: "Operations Management", doc_count: 62 }, - { key: "Differential Equations", doc_count: 54 }, - { key: "Aerospace Engineering", doc_count: 53 }, - { key: "Chemical Engineering", doc_count: 52 }, - { key: "Electronics", doc_count: 51 }, - { key: "Civil Engineering", doc_count: 49 }, - { key: "Music", doc_count: 48 }, - { key: "Entrepreneurship", doc_count: 47 }, - { key: "Organizational Behavior", doc_count: 47 }, - { key: "Environmental Engineering", doc_count: 45 }, - { key: "Gender Studies", doc_count: 45 }, - { key: "Energy", doc_count: 44 }, - { key: "Ocean Engineering", doc_count: 42 }, - { key: "Linear Algebra", doc_count: 41 }, - { key: "Linguistics", doc_count: 41 }, - { key: "Psychology", doc_count: 41 }, - { key: "Information Technology", doc_count: 40 }, - { key: "Leadership", doc_count: 40 }, - { key: "The Developing World", doc_count: 40 }, - { key: "Globalization", doc_count: 39 }, - { key: "Social Sciences", doc_count: 38 }, - { key: "Innovation", doc_count: 35 }, - { key: "Algebra and Number Theory", doc_count: 34 }, - { key: "Computation", doc_count: 34 }, - { key: "Finance", doc_count: 34 }, - { key: "Topology and Geometry", doc_count: 34 }, - { key: "Asian Studies", doc_count: 31 }, - { key: "Economics & Finance", doc_count: 31 }, - { key: "Discrete Mathematics", doc_count: 30 }, - { key: "Mathematical Analysis", doc_count: 29 }, - { key: "Calculus", doc_count: 28 }, - { key: "Nuclear Engineering", doc_count: 28 }, - { key: "Public Health", doc_count: 28 }, - { key: "Project Management", doc_count: 27 }, - { key: "Performance Arts", doc_count: 26 }, - { key: "Nanotechnology", doc_count: 25 }, - { key: "Women's Studies", doc_count: 24 }, - { key: "European and Russian Studies", doc_count: 22 }, - { key: "Game Theory", doc_count: 21 }, - { key: "Curriculum and Teaching", doc_count: 20 }, - { key: "Global Poverty", doc_count: 20 }, - { key: "Legal Studies", doc_count: 20 }, - { key: "Anatomy and Physiology", doc_count: 19 }, - { key: "Educational Technology", doc_count: 19 }, - { key: "Supply Chain Management", doc_count: 19 }, - { key: "Biomedicine", doc_count: 16 }, - { key: "Game Design", doc_count: 16 }, - { key: "Sensory-Neural Systems", doc_count: 16 }, - { key: "Biomedical Instrumentation", doc_count: 14 }, - { key: "Marketing", doc_count: 14 }, - { key: "Technology", doc_count: 14 }, - { key: "Health Care Management", doc_count: 13 }, - { key: "Nuclear", doc_count: 13 }, - { key: "Art History", doc_count: 12 }, - { key: "Business Ethics", doc_count: 12 }, - { - key: "Industrial Relations and Human Resource Management", - doc_count: 12 - }, - { key: "Latin and Caribbean Studies", doc_count: 12 }, - { key: "African American Studies", doc_count: 11 }, - { key: "Electricity", doc_count: 11 }, - { key: "Fossil Fuels", doc_count: 10 }, - { key: "Medical Imaging", doc_count: 10 }, - { key: "Pathology and Pathophysiology", doc_count: 10 }, - { key: "Religion", doc_count: 10 }, - { key: "Biomedical Signal and Image Processing", doc_count: 9 }, - { key: "Climate", doc_count: 9 }, - { key: "Real Estate", doc_count: 9 }, - { key: "Accounting", doc_count: 8 }, - { key: "Pharmacology and Toxicology", doc_count: 8 }, - { key: "Spectroscopy", doc_count: 8 }, - { key: "Biomedical Enterprise", doc_count: 7 }, - { key: "Education Policy", doc_count: 7 }, - { key: "Functional Genomics", doc_count: 7 }, - { key: "Geography", doc_count: 7 }, - { key: "Middle Eastern Studies", doc_count: 7 }, - { key: "Renewables", doc_count: 7 }, - { key: "Buildings", doc_count: 6 }, - { key: "Cancer", doc_count: 6 }, - { key: "Mathematical Logic", doc_count: 6 }, - { key: "Combustion", doc_count: 5 }, - { key: "Education & Teacher Training", doc_count: 5 }, - { key: "Higher Education", doc_count: 5 }, - { key: "Archaeology", doc_count: 4 }, - { key: "Health and Exercise Science", doc_count: 4 }, - { key: "Mental Health", doc_count: 4 }, - { key: "Biology & Life Sciences", doc_count: 3 }, - { key: "Cellular and Molecular Medicine", doc_count: 3 }, - { key: "Design", doc_count: 3 }, - { key: "Epidemiology", doc_count: 3 }, - { key: "Fuel Cells", doc_count: 3 }, - { key: "Hydrogen and Alternatives", doc_count: 3 }, - { key: "Speech Pathology", doc_count: 3 }, - { key: "Transportation", doc_count: 3 }, - { key: "Immunology", doc_count: 2 }, - { key: "Indigenous Studies", doc_count: 1 }, - { key: "Math", doc_count: 1 }, - { key: "Social Medicine", doc_count: 1 } - ] - } - } - ], - [ - "agg_filter_certification", - { - doc_count: 11844, - certification: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: "Certificates", doc_count: 132 }] - } - } - ], - [ - "agg_filter_audience", - { - doc_count: 11844, - audience: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "Open Content", doc_count: 11838 }, - { key: "Professional Offerings", doc_count: 6 } - ] - } - } - ], - [ - "agg_filter_offered_by", - { - doc_count: 11844, - offered_by: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "OCW", doc_count: 8298 }, - { key: "CBMM", doc_count: 620 }, - { key: "MITx", doc_count: 411 }, - { - key: "School of Humanities, Arts, and Social Sciences", - doc_count: 351 - }, - { key: "Video Productions", doc_count: 318 }, - { key: "Center for International Studies", doc_count: 234 }, - { key: "Open Learning", doc_count: 211 }, - { key: "CSAIL", doc_count: 174 }, - { key: "Arts at MIT", doc_count: 131 }, - { key: "Sloan School of Management", doc_count: 129 }, - { key: "Bootcamps", doc_count: 114 }, - { key: "MIT Press", doc_count: 101 }, - { key: "Physics Technical Services Group", doc_count: 96 }, - { key: "MIT BLOSSOMS", doc_count: 85 }, - { key: "MIT Alumni", doc_count: 79 }, - { key: "MIT Medical", doc_count: 44 }, - { key: "Energy Initiative", doc_count: 33 }, - { key: "MIT Technology Review", doc_count: 31 }, - { key: "Open Learning Library", doc_count: 30 }, - { key: "MIT News", doc_count: 26 }, - { key: "Department of Urban Studies and Planning", doc_count: 24 }, - { key: "Broad Inistitute", doc_count: 10 }, - { key: "MIT LGO", doc_count: 10 }, - { key: "Department of Biology", doc_count: 8 }, - { key: "xPRO", doc_count: 4 } - ] - } - } - ], - [ - "type", - { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "video", doc_count: 8156 }, - { key: "course", doc_count: 2508 }, - { key: "podcast", doc_count: 1180 } - ] - } - ], - [ - "topics", - { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "Physics", doc_count: 6568 }, - { key: "Engineering", doc_count: 5600 }, - { key: "Computer Science", doc_count: 5325 }, - { key: "Science", doc_count: 2006 }, - { key: "Economics", doc_count: 1957 }, - { key: "Probability and Statistics", doc_count: 1764 }, - { key: "Media Studies", doc_count: 1607 }, - { key: "Teaching and Education", doc_count: 1170 }, - { key: "Business", doc_count: 939 }, - { key: "Social Science", doc_count: 669 }, - { key: "Humanities", doc_count: 566 }, - { key: "Electrical Engineering", doc_count: 524 }, - { key: "Communication", doc_count: 433 }, - { key: "Mathematics", doc_count: 291 }, - { key: "Fine Arts", doc_count: 285 }, - { key: "Biology", doc_count: 239 }, - { key: "History", doc_count: 213 }, - { key: "Literature", doc_count: 199 }, - { key: "Health and Medicine", doc_count: 184 }, - { key: "Public Administration", doc_count: 173 }, - { key: "Society", doc_count: 166 }, - { key: "Political Science", doc_count: 162 }, - { key: "Systems Engineering", doc_count: 159 }, - { key: "Mechanical Engineering", doc_count: 144 }, - { key: "Anthropology", doc_count: 143 }, - { key: "Philosophy", doc_count: 133 }, - { key: "Earth Science", doc_count: 129 }, - { key: "Chemistry", doc_count: 127 }, - { key: "Visual Arts", doc_count: 125 }, - { key: "Urban Studies", doc_count: 111 }, - { key: "Physical Education and Recreation", doc_count: 106 }, - { key: "Architecture", doc_count: 101 }, - { key: "Management", doc_count: 96 }, - { key: "Sociology", doc_count: 87 }, - { key: "Cognitive Science", doc_count: 83 }, - { key: "Business & Management", doc_count: 81 }, - { key: "Language", doc_count: 75 }, - { key: "Applied Mathematics", doc_count: 74 }, - { key: "Materials Science and Engineering", doc_count: 67 }, - { key: "Data Analysis & Statistics", doc_count: 65 }, - { key: "Biological Engineering", doc_count: 62 }, - { key: "Operations Management", doc_count: 62 }, - { key: "Differential Equations", doc_count: 54 }, - { key: "Aerospace Engineering", doc_count: 53 }, - { key: "Chemical Engineering", doc_count: 52 }, - { key: "Electronics", doc_count: 51 }, - { key: "Civil Engineering", doc_count: 49 }, - { key: "Music", doc_count: 48 }, - { key: "Entrepreneurship", doc_count: 47 }, - { key: "Organizational Behavior", doc_count: 47 }, - { key: "Environmental Engineering", doc_count: 45 }, - { key: "Gender Studies", doc_count: 45 }, - { key: "Energy", doc_count: 44 }, - { key: "Ocean Engineering", doc_count: 42 }, - { key: "Linear Algebra", doc_count: 41 }, - { key: "Linguistics", doc_count: 41 }, - { key: "Psychology", doc_count: 41 }, - { key: "Information Technology", doc_count: 40 }, - { key: "Leadership", doc_count: 40 }, - { key: "The Developing World", doc_count: 40 }, - { key: "Globalization", doc_count: 39 }, - { key: "Social Sciences", doc_count: 38 }, - { key: "Innovation", doc_count: 35 }, - { key: "Algebra and Number Theory", doc_count: 34 }, - { key: "Computation", doc_count: 34 }, - { key: "Finance", doc_count: 34 }, - { key: "Topology and Geometry", doc_count: 34 }, - { key: "Asian Studies", doc_count: 31 }, - { key: "Economics & Finance", doc_count: 31 }, - { key: "Discrete Mathematics", doc_count: 30 }, - { key: "Mathematical Analysis", doc_count: 29 }, - { key: "Calculus", doc_count: 28 }, - { key: "Nuclear Engineering", doc_count: 28 }, - { key: "Public Health", doc_count: 28 }, - { key: "Project Management", doc_count: 27 }, - { key: "Performance Arts", doc_count: 26 }, - { key: "Nanotechnology", doc_count: 25 }, - { key: "Women's Studies", doc_count: 24 }, - { key: "European and Russian Studies", doc_count: 22 }, - { key: "Game Theory", doc_count: 21 }, - { key: "Curriculum and Teaching", doc_count: 20 }, - { key: "Global Poverty", doc_count: 20 }, - { key: "Legal Studies", doc_count: 20 }, - { key: "Anatomy and Physiology", doc_count: 19 }, - { key: "Educational Technology", doc_count: 19 }, - { key: "Supply Chain Management", doc_count: 19 }, - { key: "Biomedicine", doc_count: 16 }, - { key: "Game Design", doc_count: 16 }, - { key: "Sensory-Neural Systems", doc_count: 16 }, - { key: "Biomedical Instrumentation", doc_count: 14 }, - { key: "Marketing", doc_count: 14 }, - { key: "Technology", doc_count: 14 }, - { key: "Health Care Management", doc_count: 13 }, - { key: "Nuclear", doc_count: 13 }, - { key: "Art History", doc_count: 12 }, - { key: "Business Ethics", doc_count: 12 }, - { - key: "Industrial Relations and Human Resource Management", - doc_count: 12 - }, - { key: "Latin and Caribbean Studies", doc_count: 12 }, - { key: "African American Studies", doc_count: 11 }, - { key: "Electricity", doc_count: 11 }, - { key: "Fossil Fuels", doc_count: 10 }, - { key: "Medical Imaging", doc_count: 10 }, - { key: "Pathology and Pathophysiology", doc_count: 10 }, - { key: "Religion", doc_count: 10 }, - { key: "Biomedical Signal and Image Processing", doc_count: 9 }, - { key: "Climate", doc_count: 9 }, - { key: "Real Estate", doc_count: 9 }, - { key: "Accounting", doc_count: 8 }, - { key: "Pharmacology and Toxicology", doc_count: 8 }, - { key: "Spectroscopy", doc_count: 8 }, - { key: "Biomedical Enterprise", doc_count: 7 }, - { key: "Education Policy", doc_count: 7 }, - { key: "Functional Genomics", doc_count: 7 }, - { key: "Geography", doc_count: 7 }, - { key: "Middle Eastern Studies", doc_count: 7 }, - { key: "Renewables", doc_count: 7 }, - { key: "Buildings", doc_count: 6 }, - { key: "Cancer", doc_count: 6 }, - { key: "Mathematical Logic", doc_count: 6 }, - { key: "Combustion", doc_count: 5 }, - { key: "Education & Teacher Training", doc_count: 5 }, - { key: "Higher Education", doc_count: 5 }, - { key: "Archaeology", doc_count: 4 }, - { key: "Health and Exercise Science", doc_count: 4 }, - { key: "Mental Health", doc_count: 4 }, - { key: "Biology & Life Sciences", doc_count: 3 }, - { key: "Cellular and Molecular Medicine", doc_count: 3 }, - { key: "Design", doc_count: 3 }, - { key: "Epidemiology", doc_count: 3 }, - { key: "Fuel Cells", doc_count: 3 }, - { key: "Hydrogen and Alternatives", doc_count: 3 }, - { key: "Speech Pathology", doc_count: 3 }, - { key: "Transportation", doc_count: 3 }, - { key: "Immunology", doc_count: 2 }, - { key: "Indigenous Studies", doc_count: 1 }, - { key: "Math", doc_count: 1 }, - { key: "Social Medicine", doc_count: 1 } - ] - } - ], - [ - "offered_by", - { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "OCW", doc_count: 8298 }, - { key: "CBMM", doc_count: 620 }, - { key: "MITx", doc_count: 411 }, - { - key: "School of Humanities, Arts, and Social Sciences", - doc_count: 351 - }, - { key: "Video Productions", doc_count: 318 }, - { key: "Center for International Studies", doc_count: 234 }, - { key: "Open Learning", doc_count: 211 }, - { key: "CSAIL", doc_count: 174 }, - { key: "Arts at MIT", doc_count: 131 }, - { key: "Sloan School of Management", doc_count: 129 }, - { key: "Bootcamps", doc_count: 114 }, - { key: "MIT Press", doc_count: 101 }, - { key: "Physics Technical Services Group", doc_count: 96 }, - { key: "MIT BLOSSOMS", doc_count: 85 }, - { key: "MIT Alumni", doc_count: 79 }, - { key: "MIT Medical", doc_count: 44 }, - { key: "Energy Initiative", doc_count: 33 }, - { key: "MIT Technology Review", doc_count: 31 }, - { key: "Open Learning Library", doc_count: 30 }, - { key: "MIT News", doc_count: 26 }, - { key: "Department of Urban Studies and Planning", doc_count: 24 }, - { key: "Broad Inistitute", doc_count: 10 }, - { key: "MIT LGO", doc_count: 10 }, - { key: "Department of Biology", doc_count: 8 }, - { key: "xPRO", doc_count: 4 } - ] - } - ], - [ - "audience", - { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: "Open Content", doc_count: 11838 }, - { key: "Professional Offerings", doc_count: 6 } - ] - } - ], - [ - "certification", - { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: "Certificates", doc_count: 132 }] - } - ] -]) + ['topic', [ + { key: "Physics", doc_count: 6568 }, + { key: "Engineering", doc_count: 5600 }, + { key: "Computer Science", doc_count: 5325 }, + { key: "Science", doc_count: 2006 }, + { key: "Economics", doc_count: 1957 }, + { key: "Probability and Statistics", doc_count: 1764 }, + { key: "Media Studies", doc_count: 1607 }, + { key: "Teaching and Education", doc_count: 1170 }, + { key: "Business", doc_count: 939 }, + { key: "Social Science", doc_count: 669 }, + { key: "Humanities", doc_count: 566 }, + { key: "Electrical Engineering", doc_count: 524 }, + { key: "Communication", doc_count: 433 }, + { key: "Mathematics", doc_count: 291 }, + { key: "Fine Arts", doc_count: 285 }, + { key: "Biology", doc_count: 239 }, + { key: "History", doc_count: 213 }, + { key: "Literature", doc_count: 199 }, + { key: "Health and Medicine", doc_count: 184 }, + { key: "Public Administration", doc_count: 173 }, + { key: "Society", doc_count: 166 }, + { key: "Political Science", doc_count: 162 }, + { key: "Systems Engineering", doc_count: 159 }, + { key: "Mechanical Engineering", doc_count: 144 }, + { key: "Anthropology", doc_count: 143 }, + { key: "Philosophy", doc_count: 133 }, + { key: "Earth Science", doc_count: 129 }, + { key: "Chemistry", doc_count: 127 }, + { key: "Visual Arts", doc_count: 125 }, + { key: "Urban Studies", doc_count: 111 }, + { key: "Physical Education and Recreation", doc_count: 106 }, + { key: "Architecture", doc_count: 101 }, + { key: "Management", doc_count: 96 }, + { key: "Sociology", doc_count: 87 }, + { key: "Cognitive Science", doc_count: 83 }, + { key: "Business & Management", doc_count: 81 }, + { key: "Language", doc_count: 75 }, + { key: "Applied Mathematics", doc_count: 74 }, + { key: "Materials Science and Engineering", doc_count: 67 }, + { key: "Data Analysis & Statistics", doc_count: 65 }, + { key: "Biological Engineering", doc_count: 62 }, + { key: "Operations Management", doc_count: 62 }, + { key: "Differential Equations", doc_count: 54 }, + { key: "Aerospace Engineering", doc_count: 53 }, + { key: "Chemical Engineering", doc_count: 52 }, + { key: "Electronics", doc_count: 51 }, + { key: "Civil Engineering", doc_count: 49 }, + { key: "Music", doc_count: 48 }, + { key: "Entrepreneurship", doc_count: 47 }, + { key: "Organizational Behavior", doc_count: 47 }, + { key: "Environmental Engineering", doc_count: 45 }, + { key: "Gender Studies", doc_count: 45 }, + { key: "Energy", doc_count: 44 }, + { key: "Ocean Engineering", doc_count: 42 }, + { key: "Linear Algebra", doc_count: 41 }, + { key: "Linguistics", doc_count: 41 }, + { key: "Psychology", doc_count: 41 }, + { key: "Information Technology", doc_count: 40 }, + { key: "Leadership", doc_count: 40 }, + { key: "The Developing World", doc_count: 40 }, + { key: "Globalization", doc_count: 39 }, + { key: "Social Sciences", doc_count: 38 }, + { key: "Innovation", doc_count: 35 }, + { key: "Algebra and Number Theory", doc_count: 34 }, + { key: "Computation", doc_count: 34 }, + { key: "Finance", doc_count: 34 }, + { key: "Topology and Geometry", doc_count: 34 }, + { key: "Asian Studies", doc_count: 31 }, + { key: "Economics & Finance", doc_count: 31 }, + { key: "Discrete Mathematics", doc_count: 30 }, + { key: "Mathematical Analysis", doc_count: 29 }, + { key: "Calculus", doc_count: 28 }, + { key: "Nuclear Engineering", doc_count: 28 }, + { key: "Public Health", doc_count: 28 }, + { key: "Project Management", doc_count: 27 }, + { key: "Performance Arts", doc_count: 26 }, + { key: "Nanotechnology", doc_count: 25 }, + { key: "Women's Studies", doc_count: 24 }, + { key: "European and Russian Studies", doc_count: 22 }, + { key: "Game Theory", doc_count: 21 }, + { key: "Curriculum and Teaching", doc_count: 20 }, + { key: "Global Poverty", doc_count: 20 }, + { key: "Legal Studies", doc_count: 20 }, + { key: "Anatomy and Physiology", doc_count: 19 }, + { key: "Educational Technology", doc_count: 19 }, + { key: "Supply Chain Management", doc_count: 19 }, + { key: "Biomedicine", doc_count: 16 }, + { key: "Game Design", doc_count: 16 }, + { key: "Sensory-Neural Systems", doc_count: 16 }, + { key: "Biomedical Instrumentation", doc_count: 14 }, + { key: "Marketing", doc_count: 14 }, + { key: "Technology", doc_count: 14 }, + { key: "Health Care Management", doc_count: 13 }, + { key: "Nuclear", doc_count: 13 }, + { key: "Art History", doc_count: 12 }, + { key: "Business Ethics", doc_count: 12 }, + { + key: "Industrial Relations and Human Resource Management", + doc_count: 12 + }, + { key: "Latin and Caribbean Studies", doc_count: 12 }, + { key: "African American Studies", doc_count: 11 }, + { key: "Electricity", doc_count: 11 }, + { key: "Fossil Fuels", doc_count: 10 }, + { key: "Medical Imaging", doc_count: 10 }, + { key: "Pathology and Pathophysiology", doc_count: 10 }, + { key: "Religion", doc_count: 10 }, + { key: "Biomedical Signal and Image Processing", doc_count: 9 }, + { key: "Climate", doc_count: 9 }, + { key: "Real Estate", doc_count: 9 }, + { key: "Accounting", doc_count: 8 }, + { key: "Pharmacology and Toxicology", doc_count: 8 }, + { key: "Spectroscopy", doc_count: 8 }, + { key: "Biomedical Enterprise", doc_count: 7 }, + { key: "Education Policy", doc_count: 7 }, + { key: "Functional Genomics", doc_count: 7 }, + { key: "Geography", doc_count: 7 }, + { key: "Middle Eastern Studies", doc_count: 7 }, + { key: "Renewables", doc_count: 7 }, + { key: "Buildings", doc_count: 6 }, + { key: "Cancer", doc_count: 6 }, + { key: "Mathematical Logic", doc_count: 6 }, + { key: "Combustion", doc_count: 5 }, + { key: "Education & Teacher Training", doc_count: 5 }, + { key: "Higher Education", doc_count: 5 }, + { key: "Archaeology", doc_count: 4 }, + { key: "Health and Exercise Science", doc_count: 4 }, + { key: "Mental Health", doc_count: 4 }, + { key: "Biology & Life Sciences", doc_count: 3 }, + { key: "Cellular and Molecular Medicine", doc_count: 3 }, + { key: "Design", doc_count: 3 }, + { key: "Epidemiology", doc_count: 3 }, + { key: "Fuel Cells", doc_count: 3 }, + { key: "Hydrogen and Alternatives", doc_count: 3 }, + { key: "Speech Pathology", doc_count: 3 }, + { key: "Transportation", doc_count: 3 }, + { key: "Immunology", doc_count: 2 }, + { key: "Indigenous Studies", doc_count: 1 }, + { key: "Math", doc_count: 1 }, + { key: "Social Medicine", doc_count: 1 } + ]], + ['offered_by', [ + { key: "OCW", doc_count: 8298 }, + { key: "CBMM", doc_count: 620 }, + { key: "MITx", doc_count: 411 }, + { + key: "School of Humanities, Arts, and Social Sciences", + doc_count: 351 + }, + { key: "Video Productions", doc_count: 318 }, + { key: "Center for International Studies", doc_count: 234 }, + { key: "Open Learning", doc_count: 211 }, + { key: "CSAIL", doc_count: 174 }, + { key: "Arts at MIT", doc_count: 131 }, + { key: "Sloan School of Management", doc_count: 129 }, + { key: "Bootcamps", doc_count: 114 }, + { key: "MIT Press", doc_count: 101 }, + { key: "Physics Technical Services Group", doc_count: 96 }, + { key: "MIT BLOSSOMS", doc_count: 85 }, + { key: "MIT Alumni", doc_count: 79 }, + { key: "MIT Medical", doc_count: 44 }, + { key: "Energy Initiative", doc_count: 33 }, + { key: "MIT Technology Review", doc_count: 31 }, + { key: "Open Learning Library", doc_count: 30 }, + { key: "MIT News", doc_count: 26 }, + { key: "Department of Urban Studies and Planning", doc_count: 24 }, + { key: "Broad Inistitute", doc_count: 10 }, + { key: "MIT LGO", doc_count: 10 }, + { key: "Department of Biology", doc_count: 8 }, + { key: "xPRO", doc_count: 4 } + ]], + ['resource_type', [ + { key: "video", doc_count: 8156 }, + { key: "course", doc_count: 2508 }, + { key: "podcast", doc_count: 1180 } + ]] + ]) diff --git a/src/url_utils.test.ts b/src/url_utils.test.ts index d93c098..8a2ad7f 100644 --- a/src/url_utils.test.ts +++ b/src/url_utils.test.ts @@ -11,57 +11,57 @@ describe("course search library", () => { it("should deserialize text from the URL", () => { expect(deserializeSearchParams({ search: "q=The Best Course" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "The Best Course", - sort: null, - ui: null + text: "The Best Course", + sort: null, + ui: null, + endpoint: null }) }) it("should deserialize offered by", () => { - expect(deserializeSearchParams({ search: "o=MITx" })).toEqual({ + expect(deserializeSearchParams({ search: "o=mitx" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: ["MITx"], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: ["mitx"], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) - it("should deserialize department_name", () => { - expect(deserializeSearchParams({ search: "d=Philosophy" })).toEqual({ + it("should deserialize departmen", () => { + expect(deserializeSearchParams({ search: "d=2" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: ["Philosophy"], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: ["2"], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) @@ -73,235 +73,188 @@ describe("course search library", () => { }) ).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [ + platform: [], + offered_by: [], + topic: [ "Science", "Physics", "Chemistry", "Computer Science", "Electronics" ], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) it("should deserialize type from the URL", () => { - expect(deserializeSearchParams({ search: "type=course" })).toEqual({ + expect(deserializeSearchParams({ search: "r=course" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: ["course"], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: ['course'], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) - it("should deserialize audience from the URL", () => { - expect(deserializeSearchParams({ search: "a=Open%20Content" })).toEqual({ - activeFacets: { - audience: ["Open Content"], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] - }, - text: "", - sort: null, - ui: null - }) - }) - - it("should deserialize certification from the URL", () => { - expect(deserializeSearchParams({ search: "c=Certification" })).toEqual({ - activeFacets: { - audience: [], - certification: ["Certification"], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] - }, - text: "", - sort: null, - ui: null - }) - }) it("should deserialize level from the URL", () => { - expect(deserializeSearchParams({ search: "l=Graduate" })).toEqual({ + expect(deserializeSearchParams({ search: "l=graduate" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: ["Graduate"], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: ['graduate'], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) - it("should deserialize course feature tags from the URL", () => { + it("should deserialize content featrue tags from the URL", () => { expect( deserializeSearchParams({ search: - "f=Exams%20with%20Solutions&f=Exams&f=Media%20Assignments&f=Media%20Assignments%20with%20Examples" + "cf=Exams%20with%20Solutions&cf=Exams&cf=Media%20Assignments%20with%20Examples" }) ).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - resource_type: [], - course_feature_tags: [ - "Exams with Solutions", - "Exams", - "Media Assignments", - "Media Assignments with Examples" - ] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: ["Exams with Solutions", "Exams", "Media Assignments with Examples"] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) - it("should deserialize resource type from the URL", () => { + it("should deserialize course_feature from the URL", () => { expect( deserializeSearchParams({ - search: "r=Assignments&r=Exams&r=Lecture%20Notes" + search: "f=Assignments&f=Exams&f=Lecture%20Notes" }) ).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: ["Assignments", "Exams", "Lecture Notes"] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: ["Assignments", "Exams", "Lecture Notes"], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) - it("should deserialize an ascending sort param", () => { - expect(deserializeSearchParams({ search: "s=coursenum" })).toEqual({ + it("should deserialize a sort param", () => { + expect(deserializeSearchParams({ search: "s=-coursenum" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] - }, - text: "", - sort: { - field: "coursenum", - option: "asc" + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - ui: null + text: "", + sort: '-coursenum', + ui: null, + endpoint: null }) }) - it("should deserialize a descending sort param", () => { - expect(deserializeSearchParams({ search: "s=-coursenum" })).toEqual({ + it("should deserialize the ui param", () => { + expect(deserializeSearchParams({ search: "u=list" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: { - field: "coursenum", - option: "desc" - }, - ui: null + text: "", + sort: null, + ui: "list", + endpoint: null }) }) - it("should deserialize the ui param", () => { - expect(deserializeSearchParams({ search: "u=list" })).toEqual({ + + it("should deserialize the endpoint param", () => { + expect(deserializeSearchParams({ search: "e=endpoint" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: "list" + text: "", + sort: null, + ui: null, + endpoint: 'endpoint' }) }) it("should ignore unknown params", () => { expect(deserializeSearchParams({ search: "eeee=beeeeeep" })).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - text: "", - sort: null, - ui: null + text: "", + sort: null, + ui: null, + endpoint: null }) }) @@ -312,22 +265,19 @@ describe("course search library", () => { }) ).toEqual({ activeFacets: { - audience: [], - certification: [], - offered_by: [], - topics: [], - type: [], - department_name: [], - level: [], - course_feature_tags: [], - resource_type: [] - }, - text: "", - sort: { - field: "coursenum", - option: "desc" + platform: [], + offered_by: [], + topic: [], + department: [], + level: [], + course_feature: [], + resource_type: [], + content_feature_type: [] }, - ui: null + text: "", + sort: "-coursenum", + ui: null, + endpoint: null }) }) }) @@ -359,7 +309,7 @@ describe("course search library", () => { expect( serializeSearchParams({ activeFacets: { - topics: [ + topic: [ "Science", "Physics", "Chemistry", @@ -377,10 +327,10 @@ describe("course search library", () => { expect( serializeSearchParams({ activeFacets: { - level: ["Graduate"] + level: ["graduate"] } }) - ).toEqual("l=Graduate") + ).toEqual("l=graduate") }) it("should serialize offered by", () => { @@ -393,41 +343,39 @@ describe("course search library", () => { ).toEqual("o=MITx") }) - it("should serialize type to the URL", () => { + it("should serialize resource_type to the URL", () => { expect( serializeSearchParams({ activeFacets: { - type: ["course"] + resource_type: ["course"] } }) - ).toEqual("type=course") + ).toEqual("r=course") }) - it("should serialize audience", () => { - expect( - serializeSearchParams({ - activeFacets: { - audience: ["Open Content"] - } - }) - ).toEqual("a=Open%20Content") - }) - it("should serialize certification", () => { + it("should serialize course_feature", () => { expect( serializeSearchParams({ activeFacets: { - certification: ["Certificate"] + course_feature: [ + "Exams with Solutions", + "Exams", + "Media Assignments", + "Media Assignments with Examples" + ] } }) - ).toEqual("c=Certificate") + ).toEqual( + "f=Exams%20with%20Solutions&f=Exams&f=Media%20Assignments&f=Media%20Assignments%20with%20Examples" + ) }) - it("should serialize course_feature_tags", () => { + it("should serialize content_feature_type", () => { expect( serializeSearchParams({ activeFacets: { - course_feature_tags: [ + content_feature_type: [ "Exams with Solutions", "Exams", "Media Assignments", @@ -436,7 +384,7 @@ describe("course search library", () => { } }) ).toEqual( - "f=Exams%20with%20Solutions&f=Exams&f=Media%20Assignments&f=Media%20Assignments%20with%20Examples" + "cf=Exams%20with%20Solutions&cf=Exams&cf=Media%20Assignments&cf=Media%20Assignments%20with%20Examples" ) }) @@ -450,34 +398,20 @@ describe("course search library", () => { ).toEqual("r=Assignments&r=Exams&r=Lecture%20Notes") }) - it("should serialize sort for ascending params", () => { - expect( - serializeSearchParams({ - sort: { - field: "coursenum", - option: "asc" - } - }) - ).toEqual("s=coursenum") - }) - - it("should serialize sort for descending params", () => { + it("should serialize sort", () => { expect( serializeSearchParams({ - sort: { - field: "coursenum", - option: "desc" - } + sort: 'sort' }) - ).toEqual("s=-coursenum") + ).toEqual("s=sort") }) - it("should the serialize ui param", () => { + it("should serialize endpoint", () => { expect( serializeSearchParams({ - ui: "list" + endpoint: 'endpoint' }) - ).toEqual("u=list") + ).toEqual("e=endpoint") }) }) diff --git a/src/url_utils.ts b/src/url_utils.ts index 4c00e08..c71b9ae 100644 --- a/src/url_utils.ts +++ b/src/url_utils.ts @@ -12,33 +12,29 @@ const urlParamToArray = (param: ParsedParam): string[] => _.union(toArray(param) || []) export interface Facets { - audience?: string[] - certification?: string[] - type?: string[] + platform?: string[] offered_by?: string[] - topics?: string[] - department_name?: string[] + topic?: string[] + department?: string[] level?: string[] - course_feature_tags?: string[] + course_feature?: string[] resource_type?: string[] -} - -export interface SortParam { - field: string - option: string + content_feature_type?: string[] } export interface FacetsAndSort { activeFacets: Facets - sort: SortParam | null + sort: string | null ui: string | null + endpoint: string | null } export type SearchParams = { text: string activeFacets: Facets - sort: SortParam | null + sort: string | null ui: string | null + endpoint: string | null } const handleText = (q: ParsedParam): string => { @@ -52,30 +48,12 @@ const handleText = (q: ParsedParam): string => { return q } -export const deserializeSort = (sortParam: string): SortParam | null => { - if (!sortParam) { - return null - } - - if (sortParam.startsWith("-")) { - return { - field: sortParam.slice(1), - option: "desc" - } - } else { - return { - field: sortParam, - option: "asc" - } - } -} - -export const deserializeUI = (uiParam: string): string | null => { - if (!uiParam) { +export const deserializeTextParam = (param: string): string | null => { + if (!param) { return null } - return uiParam + return param } export const deserializeSearchParams = ({ @@ -85,64 +63,53 @@ export const deserializeSearchParams = ({ }): SearchParams => { const searchUrlParams = search.replace(/^\?/, "").split("?", 1)[0] - const { type, o, t, q, a, c, d, l, f, r, s, u } = qs.parse(searchUrlParams) + const { p, o, t, q, d, l, f, cf, r, s, u, e } = qs.parse(searchUrlParams) return { text: handleText(q), activeFacets: { - audience: urlParamToArray(a), - certification: urlParamToArray(c), - type: urlParamToArray(type), - offered_by: urlParamToArray(o), - topics: urlParamToArray(t), - department_name: urlParamToArray(d), - level: urlParamToArray(l), - course_feature_tags: urlParamToArray(f), - resource_type: urlParamToArray(r) + platform: urlParamToArray(p), + offered_by: urlParamToArray(o), + topic: urlParamToArray(t), + department: urlParamToArray(d), + level: urlParamToArray(l), + course_feature: urlParamToArray(f), + resource_type: urlParamToArray(r), + content_feature_type: urlParamToArray(cf) }, - sort: deserializeSort(handleText(s)), - ui: deserializeUI(handleText(u)) - } -} - -export const serializeSort = (sort: SortParam | null): string | undefined => { - if (sort === null) { - // leave it off the params if set to default - return undefined - } - - if (sort.option === "desc") { - return `-${sort.field}` - } else { - return sort.field + sort: deserializeTextParam(handleText(s)), + ui: deserializeTextParam(handleText(u)), + endpoint: deserializeTextParam(handleText(e)) } } -export const serializeUI = (ui: string | null): string | undefined => { - if (ui === null) { +export const serializeTextParam = ( + param: string | null +): string | undefined => { + if (param === null) { // leave it off the params if set to default return undefined } - return ui + return param } export const serializeSearchParams = ({ text, activeFacets, sort, - ui + ui, + endpoint }: Partial): string => { const { - type, + platform, offered_by, - topics, - audience, - certification, - department_name, + topic, + department, level, - course_feature_tags, + course_feature, resource_type, + content_feature_type, ...others } = activeFacets ?? {} if (Object.keys(others).length > 0) { @@ -150,17 +117,16 @@ export const serializeSearchParams = ({ } return qs.stringify({ - q: text || undefined, - type, - a: audience, - c: certification, - o: offered_by, - t: topics, - d: department_name, - l: level, - f: course_feature_tags, - r: resource_type, - s: serializeSort(sort || null), - u: serializeUI(ui || null) + q: text || undefined, + o: offered_by, + t: topic, + d: department, + l: level, + f: course_feature, + r: resource_type, + cf: content_feature_type, + s: serializeTextParam(sort || null), + u: serializeTextParam(ui || null), + e: serializeTextParam(endpoint || null) }) } diff --git a/yarn.lock b/yarn.lock index c60ce3d..92240d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1655,18 +1655,6 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" integrity "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==" -bodybuilder@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/bodybuilder/-/bodybuilder-2.5.0.tgz" - integrity sha512-n9Cpi/qKH7FfFe3WHRC/jYBQt/vtkPmCuDdIM53lARoWHVDGEc5d55erKZE/dxzv0dY+ioKcd+C8EY+AytBlSw== - dependencies: - lodash.clonedeep "4.5.0" - lodash.isobject "3.0.2" - lodash.isplainobject "4.0.6" - lodash.merge "4.6.2" - lodash.set "4.3.2" - lodash.unset "4.5.2" - boolbase@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" @@ -3857,11 +3845,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.clonedeep@4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" - integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== - lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz" @@ -3877,41 +3860,21 @@ lodash.isequal@^4.5.0: resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" integrity "sha1-QVxEePK8wwEgwizhDtMib30+GOA= sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" -lodash.isobject@3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz" - integrity sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA== - -lodash.isplainobject@4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -lodash.merge@4.6.2, lodash.merge@^4.6.0, lodash.merge@^4.6.2: +lodash.merge@^4.6.0, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.set@4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz" - integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== - lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash.unset@4.5.2: - version "4.5.2" - resolved "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz" - integrity sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg== - lodash@^4.15.0, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"