feat(search): add GET /search/facets.json — context-aware facet values API#12980
feat(search): add GET /search/facets.json — context-aware facet values API#12980mekarpeles wants to merge 2 commits into
Conversation
…s API Returns only search-relevant facet options for one or more fields, intended to power the OlSelectPopover components in PR #12949's Lit search UX sidebar. - New endpoint: GET /search/facets.json?field=language&field=subject_facet&q=... - Accepts all PublicQueryOptions (q, title, author, filters, etc.) - Returns {field: [{value, label, count}]} with zero-count entries filtered - Validates field names against WorkSearchScheme.facet_fields - Maps internal author_key back to author_facet for API consistency - 8 new tests covering happy path, validation, filtering, and multi-field Closes #12978
RayBB
left a comment
There was a problem hiding this comment.
Here's some feedback (for the AI):
Can you:
- Please follow the pattern of the rest of the endpoints in this file by having request/response models. This will be very handy for all of our tools!
- Use the fastapi skill (https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) to ensure you're following best practices. Such as letting fastapi do the validation instead of validating in the endpoint.
- Avoid putting "business" logic in the endpoint the small transformation is on the border. Imo if it had much more we should def move it out of the endpoint and into a single testable function.
Do you intend this to be a publicly consumed API? If not, I'd recommend removing it from the openAPI spec outside of local dev (like how we do for the internal endpoints). Since this will likely be a "heavy" endpoint I think that's probably wise to do, at least for now.
- Add FacetValue response model + response_model= on decorator (per point 1) - Replace manual invalid-field check with FacetField Literal type so FastAPI validates values and returns 422 (per point 2) - Move _FACET_INTERNAL_RENAME to module level, out of the endpoint body (point 3) - Update test: invalid field now correctly expects 422, not 400 - Drop trivial openapi-schema presence test
|
Thanks for the thorough review @RayBB — addressed in 5c4fc62: 1. Request/response models — added 2. Let FastAPI do validation — replaced the hand-rolled 3. Business logic out of the endpoint — moved On the OpenAPI visibility question — this endpoint is designed to be called by the OL frontend (the |
Summary
Adds
GET /search/facets.json, a new FastAPI endpoint that returns context-aware facet values for use in the search sidebar's filter dropdowns.Unlike a generic "all values for field X" endpoint, this one accepts the full current search query and returns only the values that have results for that query (zero-count entries are filtered). This powers smart dropdowns — when a user has searched for "Tolkien", the language facet shows only languages that appear in Tolkien's books, not every language in the catalog.
Closes #12978
API Reference
Required parameter:
field— facet field to return. Repeat for multiple. Valid values:author_facet,first_publish_year,has_fulltext,language,person_facet,place_facet,public_scan_b,publisher_facet,subject_facet,time_facetAll
PublicQueryOptionsare accepted:q,title,author,subject,language,author_key,subject_facet,place_facet,time_facet,first_publish_year,publisher_facet,has_fulltext,public_scan,print_disabled, and more.Example request:
Example response:
{ "language": [ {"value": "English", "label": "English", "count": 1423}, {"value": "German", "label": "German", "count": 47}, {"value": "French", "label": "French", "count": 23} ], "subject_facet": [ {"value": "Fantasy fiction", "label": "Fantasy fiction", "count": 892}, {"value": "Imaginary places -- Fiction", "label": "Imaginary places -- Fiction", "count": 541} ] }Each entry has:
value— the filter value to pass back to/search.json(e.g.author_key=OL9A)label— the human-readable display label (differs fromvalueforauthor_facet)count— number of matching worksIntegration recipe for PR #12949 (
OlSelectPopover/ Lit search UX)This endpoint is designed to feed directly into the
OlSelectPopovercomponents added in PR #12949. Here's a minimal integration sketch:The response shape (
{value, label}per item) is intentionally compatible withOlSelectPopover'sitemsprop so no transformation is needed beyond strippingcountif the component doesn't need it.Multi-field call (one round trip for the whole sidebar):
Returns all four dropdowns' options in one Solr query (
rows=0), making it suitable for calling on every search navigation without noticeable latency impact.Implementation notes
rows=0Solr query (no document data fetched) — fast even for large result setsBOOK_SEARCH_FACETSrequest label for Grafana/stats trackingauthor_facet→author_keyrename is handled internally; response always usesauthor_facetto match the caller's inputDepends()(notQuery()) to unpackPublicQueryOptions— consistent with other endpoints likesearch_authors_jsonfieldparameter usesNonedefault (not[]) to satisfy ruff B006Testing
8 new tests in
TestSearchFacetsEndpoint:test_requires_field_param— 400 when nofieldgiventest_rejects_invalid_field— 400 + error detail for unknown field namestest_returns_facet_values_for_single_field— 200 with correct structuretest_filters_zero_count_values— zero-count entries excluded from responsetest_returns_multiple_fields— multiplefield=params worktest_author_facet_key_maps_to_author_facet— internal rename round-trips correctlytest_empty_query_returns_valid_response— noqparam still workstest_openapi_contains_facets_endpoint— endpoint appears in OpenAPI schemaAll 8 pass. All 44 pre-existing
TestSearchEndpointtests continue to pass.Checklist
fieldparameter againstWorkSearchScheme.facet_fieldsauthor_facetkey roundtrips correctly through internal rename