mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 22:05:50 +01:00
Advanced title search, advanced title+author search, and the title typeahead returned zero results every time, and the sort fallback added in #1183 blamed the sort value for it. Hardcover turns the `fields` search parameter into Typesense's `query_by` but keeps `num_typos` and `query_by_weights` as fixed-length presets per query_type. For query_type=Book the preset expects exactly five fields, so a shorter list is not searched loosely - the whole search is rejected with a null results body. Confirmed against the live API: 1, 2, 3, 4 and 6 fields are all rejected, only 5 works, and weights must match one-for-one when sent. Every Book-type list we sent was the wrong length - the title typeahead and advanced title search sent 2, title+author sent 3. - Send BOOK_SEARCH_FIELDS (the full five) for every narrowed Book search and express the intent through weights instead. Weights only bias ranking - a field weighted 0 still matches - so a title search now ranks titles first rather than restricting to them. That is the closest behaviour Hardcover still allows, and there is no client-side filter to restore the old precision. - Pin the field and weight counts in tests, since the failure mode is a silent zero results rather than an error. The sort fallback from #1183 also misread these rejections: - Select the `error` field on every search and log Hardcover's own explanation. The reason is only ever in that sibling field, so a rejection surfaced as "returned no result body" with nothing to act on. Reading it is what made the field-count rule findable. - Drop `sort` entirely on the retry instead of sending an empty string. An empty sort is a value like any other and can be rejected too. - Arm the 900s sticky window only after the sortless retry succeeds. It was armed before the retry and never rolled back, so one rejected typeahead disabled sorting process-wide for 15 minutes whatever the actual cause. Verified against the live Hardcover API: advanced title search 0 -> 84 results, title+author 0 -> 139, title typeahead 0 -> 84 with the exact title top. 2566 unit tests pass; ruff, basedpyright and vulture clean. Refs #1183. The sort_by regression #1183 was written for is gone from Hardcover's side - every sort value it rejected, including the one in the report, is accepted again today. Two plain-search rejections in that report (fields=None) remain unexplained: they could not be reproduced under any per_page, page depth, sort value or query shape, and are most likely transient upstream. They now self-report the reason if they recur.
148 lines
5.9 KiB
Python
148 lines
5.9 KiB
Python
from shelfmark.metadata_providers.hardcover import (
|
|
TITLE_SUGGESTION_FIELDS,
|
|
TITLE_SUGGESTION_WEIGHTS,
|
|
HardcoverProvider,
|
|
)
|
|
|
|
|
|
class TestHardcoverFieldOptions:
|
|
def test_search_fields_enable_typeahead_for_author_and_series(self):
|
|
provider = HardcoverProvider(api_key="test-token")
|
|
fields_by_key = {field.key: field for field in provider.search_fields}
|
|
|
|
assert fields_by_key["author"].suggestions_endpoint == (
|
|
"/api/metadata/field-options?provider=hardcover&field=author"
|
|
)
|
|
assert fields_by_key["title"].suggestions_endpoint is None
|
|
assert fields_by_key["series"].suggestions_endpoint == (
|
|
"/api/metadata/field-options?provider=hardcover&field=series"
|
|
)
|
|
|
|
def test_get_search_field_options_returns_author_suggestions(self, monkeypatch):
|
|
provider = HardcoverProvider(api_key="test-token")
|
|
captured: dict[str, object] = {}
|
|
|
|
monkeypatch.setattr(
|
|
provider,
|
|
"_execute_query",
|
|
lambda query, variables: (
|
|
captured.update({"query": query, "variables": variables})
|
|
or {
|
|
"search": {
|
|
"results": {
|
|
"hits": [
|
|
{"document": {"id": 1, "name": "Brandon Sanderson"}},
|
|
{"document": {"id": 1, "name": "Brandon Sanderson"}},
|
|
{"document": {"id": 2, "name": "Brian Sanderson"}},
|
|
],
|
|
"found": 3,
|
|
}
|
|
}
|
|
}
|
|
),
|
|
)
|
|
|
|
options = provider.get_search_field_options("author", query="sand")
|
|
|
|
assert options == [
|
|
{"value": "id:1", "label": "Brandon Sanderson"},
|
|
{"value": "id:2", "label": "Brian Sanderson"},
|
|
]
|
|
assert captured["variables"] == {
|
|
"query": "sand",
|
|
"queryType": "Author",
|
|
"limit": 7,
|
|
"page": 1,
|
|
"sort": "_text_match:desc,books_count:desc",
|
|
"fields": "name,name_personal,alternate_names",
|
|
"weights": "4,3,2",
|
|
}
|
|
|
|
def test_get_search_field_options_returns_filtered_title_suggestions(self, monkeypatch):
|
|
provider = HardcoverProvider(api_key="test-token")
|
|
captured: dict[str, object] = {}
|
|
|
|
monkeypatch.setattr(
|
|
"shelfmark.metadata_providers.hardcover.app_config.get",
|
|
lambda key, default=None: {
|
|
"HARDCOVER_EXCLUDE_COMPILATIONS": True,
|
|
"HARDCOVER_EXCLUDE_UNRELEASED": True,
|
|
}.get(key, default),
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
provider,
|
|
"_execute_query",
|
|
lambda query, variables: (
|
|
captured.update({"query": query, "variables": variables})
|
|
or {
|
|
"search": {
|
|
"results": {
|
|
"hits": [
|
|
{
|
|
"document": {
|
|
"title": "Mistborn: The Final Empire",
|
|
"compilation": False,
|
|
"release_year": 2006,
|
|
}
|
|
},
|
|
{
|
|
"document": {
|
|
"title": "Mistborn Trilogy",
|
|
"compilation": True,
|
|
"release_year": 2001,
|
|
}
|
|
},
|
|
{
|
|
"document": {
|
|
"title": "Ghostbloods 1",
|
|
"compilation": False,
|
|
"release_year": 2028,
|
|
}
|
|
},
|
|
{
|
|
"document": {
|
|
"title": "Mistborn: The Final Empire",
|
|
"compilation": False,
|
|
"release_year": 2006,
|
|
}
|
|
},
|
|
{
|
|
"document": {
|
|
"title": "Mistborn: Secret History",
|
|
"compilation": False,
|
|
"release_year": 2016,
|
|
}
|
|
},
|
|
],
|
|
"found": 5,
|
|
}
|
|
}
|
|
}
|
|
),
|
|
)
|
|
|
|
options = provider.get_search_field_options("title", query="mistborn")
|
|
|
|
assert options == [
|
|
{"value": "Mistborn: The Final Empire", "label": "Mistborn: The Final Empire"},
|
|
{"value": "Mistborn: Secret History", "label": "Mistborn: Secret History"},
|
|
]
|
|
assert captured["variables"] == {
|
|
"query": "mistborn",
|
|
"queryType": "Book",
|
|
"limit": 7,
|
|
"page": 1,
|
|
"sort": "_text_match:desc,users_count:desc",
|
|
# Hardcover rejects a Book search that narrows to fewer fields than its
|
|
# preset expects, so the typeahead sends the full list and leans on weights.
|
|
"fields": TITLE_SUGGESTION_FIELDS,
|
|
"weights": TITLE_SUGGESTION_WEIGHTS,
|
|
}
|
|
|
|
def test_get_search_field_options_skips_short_text_queries(self):
|
|
provider = HardcoverProvider(api_key="test-token")
|
|
|
|
assert provider.get_search_field_options("author", query="a") == []
|
|
assert provider.get_search_field_options("title", query="i") == []
|