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"