Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions frontends/api/src/generated/v1/api.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ const SearchDisplay: React.FC<SearchDisplayProps> = ({
search_mode: searchParams.get("search_mode"),
slop: searchParams.get("slop"),
min_score: searchParams.get("min_score"),
max_incompleteness_penalty: searchParams.get(
"max_incompleteness_penalty",
),
...requestParams,
aggregations: (facetNames || []).concat([
"resource_category",
Expand Down Expand Up @@ -703,6 +706,28 @@ const SearchDisplay: React.FC<SearchDisplayProps> = ({
Minimum relevance score for a search result to be displayed. Only
affects results if there is a search term.
</ExplanationContainer>
<AdminTitleContainer>
Maximum Incompleteness Penalty
</AdminTitleContainer>
<SliderInput
currentValue={
searchParams.get("max_incompleteness_penalty")
? Number(searchParams.get("max_incompleteness_penalty"))
: 0
}
setSearchParams={setSearchParams}
urlParam="max_incompleteness_penalty"
min={0}
max={100}
step={1}
/>
<ExplanationContainer>
Maximum score penalty for incomplete OCW courses in percent. An
OCW course with completeness = 0 will have this score penalty.
Partially complete courses have a linear penalty proportional to
the degree of incompleteness. Only affects results if there is a
search term.
</ExplanationContainer>
</div>
) : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ describe("SearchPage", () => {
await waitFor(() => {
screen.getByTestId("yearly_decay_percent-slider")
screen.getByTestId("min_score-slider")
screen.getByTestId("max_incompleteness_penalty-slider")
})
})
})
Expand Down
52 changes: 34 additions & 18 deletions learning_resources_search/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,32 +544,48 @@ def add_text_query_to_search(search, text, search_params, query_type_query):

yearly_decay_percent = search_params.get("yearly_decay_percent")
min_score = search_params.get("min_score")
max_incompleteness_penalty = (
search_params.get("max_incompleteness_penalty", 0) / 100
)

if (yearly_decay_percent and yearly_decay_percent > 0) or (
min_score and min_score > 0
):
if yearly_decay_percent or min_score or max_incompleteness_penalty:
script_query = {
"script_score": {
"query": {"bool": {"must": [text_query], "filter": query_type_query}}
}
}

if yearly_decay_percent and yearly_decay_percent > 0:
script_query["script_score"]["script"] = {
"source": (
"doc['resource_age_date'].size() == 0 ? _score : "
"_score * decayDateLinear(params.origin, params.scale, "
"params.offset, params.decay, doc['resource_age_date'].value)"
),
"params": {
"origin": datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"offset": "0",
"scale": "354d",
"decay": 1 - (yearly_decay_percent / 100),
},
}
completeness_term = (
"(doc['completeness'].value * params.max_incompleteness_penalty + "
"(1-params.max_incompleteness_penalty))"
)

staleness_term = (
"(doc['resource_age_date'].size() == 0 ? 1 : "
"decayDateLinear(params.origin, params.scale, params.offset, params.decay, "
"doc['resource_age_date'].value))"
)

source = "_score"
params = {}

if max_incompleteness_penalty:
source = f"{source} * {completeness_term}"
params["max_incompleteness_penalty"] = max_incompleteness_penalty

if yearly_decay_percent:
source = f"{source} * {staleness_term}"
params["decay"] = 1 - (yearly_decay_percent / 100)
params["offset"] = "0"
params["scale"] = "365d"
params["origin"] = datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ")

script_query["script_score"]["script"] = {
"source": source,
"params": params,
}

if min_score and min_score > 0:
if min_score:
script_query["script_score"]["min_score"] = min_score

search = search.query(script_query)
Expand Down
59 changes: 50 additions & 9 deletions learning_resources_search/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,11 +1440,55 @@ def test_execute_learn_search_for_learning_resource_query(opensearch):


@freeze_time("2024-07-20")
def test_execute_learn_search_with_yearly_decay_percent(mocker, opensearch):
@pytest.mark.parametrize(
("yearly_decay_percent", "max_incompleteness_penalty"),
[
(5, 25),
(0, 25),
(5, 0),
],
)
def test_execute_learn_search_with_script_score(
mocker, opensearch, yearly_decay_percent, max_incompleteness_penalty
):
opensearch.conn.search.return_value = {
"hits": {"total": {"value": 10, "relation": "eq"}}
}

if yearly_decay_percent > 0 and max_incompleteness_penalty > 0:
source = (
"_score * (doc['completeness'].value * params.max_incompleteness_penalty + "
"(1-params.max_incompleteness_penalty)) * (doc['resource_age_date'].size() == 0 ? "
"1 : decayDateLinear(params.origin, params.scale, params.offset, params.decay, "
"doc['resource_age_date'].value))"
)
params = {
"origin": "2024-07-20T00:00:00.000000Z",
"offset": "0",
"scale": "365d",
"decay": 0.95,
"max_incompleteness_penalty": 0.25,
}
elif yearly_decay_percent > 0:
source = (
"_score * (doc['resource_age_date'].size() == 0 ? "
"1 : decayDateLinear(params.origin, params.scale, params.offset, params.decay, "
"doc['resource_age_date'].value))"
)

params = {
"origin": "2024-07-20T00:00:00.000000Z",
"offset": "0",
"scale": "365d",
"decay": 0.95,
}
else:
source = (
"_score * (doc['completeness'].value * params.max_incompleteness_penalty +"
" (1-params.max_incompleteness_penalty))"
)
params = {"max_incompleteness_penalty": 0.25}

search_params = {
"aggregations": ["offered_by"],
"q": "math",
Expand All @@ -1454,7 +1498,8 @@ def test_execute_learn_search_with_yearly_decay_percent(mocker, opensearch):
"offset": 1,
"sortby": "-readable_id",
"endpoint": LEARNING_RESOURCE,
"yearly_decay_percent": 5,
"yearly_decay_percent": yearly_decay_percent,
"max_incompleteness_penalty": max_incompleteness_penalty,
}

query = {
Expand Down Expand Up @@ -1700,13 +1745,8 @@ def test_execute_learn_search_with_yearly_decay_percent(mocker, opensearch):
}
},
"script": {
"source": "doc['resource_age_date'].size() == 0 ? _score : _score * decayDateLinear(params.origin, params.scale, params.offset, params.decay, doc['resource_age_date'].value)",
"params": {
"origin": "2024-07-20T00:00:00.000000Z",
"offset": "0",
"scale": "354d",
"decay": 0.95,
},
"source": source,
"params": params,
},
}
},
Expand Down Expand Up @@ -2115,6 +2155,7 @@ def test_execute_learn_search_with_min_score(mocker, opensearch):
"filter": [{"exists": {"field": "resource_type"}}],
}
},
"script": {"params": {}, "source": "_score"},
"min_score": 5,
}
},
Expand Down
1 change: 1 addition & 0 deletions learning_resources_search/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ class FilterConfig:
"next_start_date": {"type": "date"},
"resource_age_date": {"type": "date"},
"featured_rank": {"type": "float"},
"completeness": {"type": "float"},
}


Expand Down
14 changes: 14 additions & 0 deletions learning_resources_search/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,20 @@ class LearningResourcesSearchRequestSerializer(SearchRequestSerializer):
"Minimum score value a text query result needs to have to be displayed"
),
)
max_incompleteness_penalty = serializers.FloatField(
max_value=100,
min_value=0,
required=False,
allow_null=True,
default=0,
help_text=(
"Maximum score penalty for incomplete OCW courses in percent. "
"An OCW course with completeness = 0 will have this score penalty. "
"Partially complete courses have a linear penalty proportional to "
"the degree of incompleteness. Only affects results if there is a "
"search term."
),
)


class ContentFileSearchRequestSerializer(SearchRequestSerializer):
Expand Down
2 changes: 2 additions & 0 deletions learning_resources_search/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,7 @@ def test_learning_resources_search_request_serializer():
"search_mode": "phrase",
"slop": 2,
"min_score": 0,
"max_incompleteness_penalty": 25,
}

cleaned = {
Expand All @@ -903,6 +904,7 @@ def test_learning_resources_search_request_serializer():
"search_mode": "phrase",
"slop": 2,
"min_score": 0,
"max_incompleteness_penalty": 25,
}

serialized = LearningResourcesSearchRequestSerializer(data=data)
Expand Down
63 changes: 63 additions & 0 deletions openapi/specs/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2450,6 +2450,19 @@ paths:
schema:
type: integer
description: Number of results to return per page
- in: query
name: max_incompleteness_penalty
schema:
type: number
format: double
maximum: 100
minimum: 0
nullable: true
default: 0.0
description: Maximum score penalty for incomplete OCW courses in percent.
An OCW course with completeness = 0 will have this score penalty. Partially
complete courses have a linear penalty proportional to the degree of incompleteness.
Only affects results if there is a search term.
- in: query
name: min_score
schema:
Expand Down Expand Up @@ -2918,6 +2931,19 @@ paths:
schema:
type: integer
description: Number of results to return per page
- in: query
name: max_incompleteness_penalty
schema:
type: number
format: double
maximum: 100
minimum: 0
nullable: true
default: 0.0
description: Maximum score penalty for incomplete OCW courses in percent.
An OCW course with completeness = 0 will have this score penalty. Partially
complete courses have a linear penalty proportional to the degree of incompleteness.
Only affects results if there is a search term.
- in: query
name: min_score
schema:
Expand Down Expand Up @@ -3411,6 +3437,19 @@ paths:
schema:
type: integer
description: Number of results to return per page
- in: query
name: max_incompleteness_penalty
schema:
type: number
format: double
maximum: 100
minimum: 0
nullable: true
default: 0.0
description: Maximum score penalty for incomplete OCW courses in percent.
An OCW course with completeness = 0 will have this score penalty. Partially
complete courses have a linear penalty proportional to the degree of incompleteness.
Only affects results if there is a search term.
- in: query
name: min_score
schema:
Expand Down Expand Up @@ -3895,6 +3934,19 @@ paths:
schema:
type: integer
description: Number of results to return per page
- in: query
name: max_incompleteness_penalty
schema:
type: number
format: double
maximum: 100
minimum: 0
nullable: true
default: 0.0
description: Maximum score penalty for incomplete OCW courses in percent.
An OCW course with completeness = 0 will have this score penalty. Partially
complete courses have a linear penalty proportional to the degree of incompleteness.
Only affects results if there is a search term.
- in: query
name: min_score
schema:
Expand Down Expand Up @@ -9913,6 +9965,17 @@ components:
default: 0.0
description: Minimum score value a text query result needs to have to be
displayed
max_incompleteness_penalty:
type: number
format: double
maximum: 100
minimum: 0
nullable: true
default: 0.0
description: Maximum score penalty for incomplete OCW courses in percent.
An OCW course with completeness = 0 will have this score penalty. Partially
complete courses have a linear penalty proportional to the degree of incompleteness.
Only affects results if there is a search term.
source_type:
allOf:
- $ref: '#/components/schemas/SourceTypeEnum'
Expand Down