Skip to content

Commit 2a7f4c1

Browse files
New search input (#2383)
## Summary Fixes #{2289} ### Time to review: __5 mins__ ## Changes proposed > Added new request field to opportunity search route that allows users to specify scoring rule. ## Context for reviewers > Add two new static list of fields (scoring_rule) to query against when calling OpenSearch with a query. User can specify which scoring algorithm to use. If not provided, it will default to the "default" scoring_rule. ## Additional information > Screenshots, GIF demos, code examples or output to help show the changes working as expected. --------- Co-authored-by: nava-platform-bot <platform-admins@navapbc.com>
1 parent 134ff92 commit 2a7f4c1

File tree

6 files changed

+127
-13
lines changed

6 files changed

+127
-13
lines changed

api/openapi.generated.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,19 @@ components:
10361036
- object
10371037
allOf:
10381038
- $ref: '#/components/schemas/CloseDateFilterV1'
1039+
ExperimentalV1:
1040+
type: object
1041+
properties:
1042+
scoring_rule:
1043+
default: !!python/object/apply:src.services.opportunities_v1.experimental_constant.ScoringRule
1044+
- default
1045+
description: Scoring rule to query against OpenSearch
1046+
enum:
1047+
- default
1048+
- expanded
1049+
- agency
1050+
type:
1051+
- string
10391052
OpportunityPaginationV1:
10401053
type: object
10411054
properties:
@@ -1086,6 +1099,11 @@ components:
10861099
- object
10871100
allOf:
10881101
- $ref: '#/components/schemas/OpportunitySearchFilterV1'
1102+
experimental:
1103+
type:
1104+
- object
1105+
allOf:
1106+
- $ref: '#/components/schemas/ExperimentalV1'
10891107
pagination:
10901108
type:
10911109
- object

api/src/api/opportunities_v1/opportunity_schemas.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
OpportunityStatus,
1717
)
1818
from src.pagination.pagination_schema import generate_pagination_schema
19+
from src.services.opportunities_v1.experimental_constant import ScoringRule
1920

2021

2122
class SearchResponseFormat(StrEnum):
@@ -423,6 +424,17 @@ class OpportunityFacetV1Schema(Schema):
423424
)
424425

425426

427+
class ExperimentalV1Schema(Schema):
428+
scoring_rule = fields.Enum(
429+
ScoringRule,
430+
load_default=ScoringRule.DEFAULT,
431+
metadata={
432+
"description": "Scoring rule to query against OpenSearch",
433+
"default": ScoringRule.DEFAULT,
434+
},
435+
)
436+
437+
426438
class OpportunitySearchRequestV1Schema(Schema):
427439
query = fields.String(
428440
metadata={
@@ -433,7 +445,7 @@ class OpportunitySearchRequestV1Schema(Schema):
433445
)
434446

435447
filters = fields.Nested(OpportunitySearchFilterV1Schema())
436-
448+
experimental = fields.Nested(ExperimentalV1Schema())
437449
pagination = fields.Nested(
438450
generate_pagination_schema(
439451
"OpportunityPaginationV1Schema",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from enum import StrEnum
2+
3+
DEFAULT = [
4+
# Note that we do keyword for agency & opportunity number
5+
# as we don't want to compare to a tokenized value which
6+
# may have split on the dashes.
7+
"agency.keyword^16",
8+
"opportunity_title^2",
9+
"opportunity_number.keyword^12",
10+
"summary.summary_description",
11+
"opportunity_assistance_listings.assistance_listing_number^10",
12+
"opportunity_assistance_listings.program_title^4",
13+
]
14+
15+
EXPANDED = [
16+
"agency.keyword",
17+
"agency_name",
18+
"opportunity_title",
19+
"opportunity_number.keyword",
20+
"category_explanation",
21+
"summary.summary_description",
22+
"summary.applicant_eligibility_description",
23+
"summary.funding_category_description",
24+
"opportunity_assistance_listings.assistance_listing_number",
25+
"opportunity_assistance_listings.program_title",
26+
]
27+
28+
29+
AGENCY = [
30+
"agency.keyword",
31+
"agency_name",
32+
"summary.agency_contact_description",
33+
"summary.agency_email_address.keyword",
34+
"summary.agency_email_address_description",
35+
"summary.agency_phone_number.keyword",
36+
]
37+
38+
39+
class ScoringRule(StrEnum):
40+
DEFAULT = "default"
41+
EXPANDED = "expanded"
42+
AGENCY = "agency"

api/src/services/opportunities_v1/search_opportunities.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
IntSearchFilter,
1515
StrSearchFilter,
1616
)
17+
from src.services.opportunities_v1.experimental_constant import (
18+
AGENCY,
19+
DEFAULT,
20+
EXPANDED,
21+
ScoringRule,
22+
)
1723

1824
logger = logging.getLogger(__name__)
1925

@@ -41,17 +47,11 @@
4147
"estimated_total_program_funding": "summary.estimated_total_program_funding",
4248
}
4349

44-
SEARCH_FIELDS = [
45-
# Note that we do keyword for agency & opportunity number
46-
# as we don't want to compare to a tokenized value which
47-
# may have split on the dashes.
48-
"agency.keyword^16",
49-
"opportunity_title^2",
50-
"opportunity_number.keyword^12",
51-
"summary.summary_description",
52-
"opportunity_assistance_listings.assistance_listing_number^10",
53-
"opportunity_assistance_listings.program_title^4",
54-
]
50+
FILTER_RULE_MAPPING = {
51+
ScoringRule.EXPANDED: EXPANDED,
52+
ScoringRule.AGENCY: AGENCY,
53+
ScoringRule.DEFAULT: DEFAULT,
54+
}
5555

5656
SCHEMA = OpportunityV1Schema()
5757

@@ -76,11 +76,16 @@ class OpportunityFilters(BaseModel):
7676
close_date: DateSearchFilter | None = None
7777

7878

79+
class Experimental(BaseModel):
80+
scoring_rule: ScoringRule = Field(default=ScoringRule.DEFAULT)
81+
82+
7983
class SearchOpportunityParams(BaseModel):
8084
pagination: PaginationParams
8185

8286
query: str | None = Field(default=None)
8387
filters: OpportunityFilters | None = Field(default=None)
88+
experimental: Experimental = Field(default=Experimental())
8489

8590

8691
def _adjust_field_name(field: str) -> str:
@@ -148,7 +153,8 @@ def _get_search_request(params: SearchOpportunityParams) -> dict:
148153

149154
# Query
150155
if params.query:
151-
builder.simple_query(params.query, SEARCH_FIELDS)
156+
filter_rule = FILTER_RULE_MAPPING.get(params.experimental.scoring_rule, DEFAULT)
157+
builder.simple_query(params.query, filter_rule)
152158

153159
# Filters
154160
_add_search_filters(builder, params.filters)

api/tests/src/api/opportunities_v1/conftest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_search_request(
3232
order_by: str = "opportunity_id",
3333
sort_direction: str = "ascending",
3434
query: str | None = None,
35+
experimental: dict | None = None,
3536
funding_instrument_one_of: list[FundingInstrument] | None = None,
3637
funding_category_one_of: list[FundingCategory] | None = None,
3738
applicant_type_one_of: list[ApplicantType] | None = None,
@@ -106,6 +107,9 @@ def get_search_request(
106107
if format is not None:
107108
req["format"] = format
108109

110+
if experimental is not None:
111+
req["experimental"] = experimental
112+
109113
return req
110114

111115

api/tests/src/api/opportunities_v1/test_opportunity_route_search.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,3 +1305,35 @@ def test_search_query_facets_200(self, client, api_auth_token):
13051305
"funding_category",
13061306
"opportunity_status",
13071307
}
1308+
1309+
@pytest.mark.parametrize(
1310+
"search_request",
1311+
[
1312+
# default scoring rule
1313+
get_search_request(
1314+
query="literacy",
1315+
),
1316+
# agency scoring rule
1317+
get_search_request(
1318+
query="literacy",
1319+
experimental={"scoring_rule": "agency"},
1320+
),
1321+
# expanded scoring rule
1322+
get_search_request(
1323+
query="literacy",
1324+
experimental={"scoring_rule": "expanded"},
1325+
),
1326+
],
1327+
)
1328+
def test_search_experimental_200(self, client, api_auth_token, search_request):
1329+
# We are only testing for 200 responses when adding the experimental field into the request body.
1330+
resp = client.post(
1331+
"/v1/opportunities/search", json=search_request, headers={"X-Auth": api_auth_token}
1332+
)
1333+
assert resp.status_code == 200
1334+
1335+
search_request["format"] = "csv"
1336+
resp = client.post(
1337+
"/v1/opportunities/search", json=search_request, headers={"X-Auth": api_auth_token}
1338+
)
1339+
assert resp.status_code == 200

0 commit comments

Comments
 (0)