feat(search): enable OCR text search on page-tag aggregation endpoints
The `?text=` param on `/v1/me/page-tags/chapters` and `/mangas` was
reserved and returned 501 `text_search_not_yet_supported`. Now that OCR
populates `search_doc`, flip it to real search.
- `aggregate_chapters_for_tag` / `aggregate_mangas_for_tag` take an optional
`text`; when non-blank they JOIN `page_analysis` and filter on
`search_doc @@ plainto_tsquery('simple', $n)`, consistent with
`repo::page_analysis::page_search`. `match_count` and the sample
thumbnails reflect the filtered set. `text` is always bound, never
interpolated.
- Drop `ensure_text_unsupported` and the 501 guard. `AppError::NotImplemented`
stays as a generic variant for future reservations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -392,10 +392,9 @@ pub struct AggregateParams {
|
||||
pub limit: i64,
|
||||
#[serde(default)]
|
||||
pub offset: i64,
|
||||
/// Reserved for the planned OCR text-search input. Accepted on
|
||||
/// the wire so adding OCR later won't break the API shape, but
|
||||
/// rejected with 501 `text_search_not_yet_supported` if non-empty
|
||||
/// until the backend supports it.
|
||||
/// OCR text filter. When non-empty, only pages whose analysis
|
||||
/// `search_doc` matches the query (`plainto_tsquery`) are aggregated.
|
||||
/// Blank/absent ⇒ tag-only aggregation.
|
||||
#[serde(default)]
|
||||
pub text: Option<String>,
|
||||
}
|
||||
@@ -411,32 +410,17 @@ fn parse_order(raw: Option<&str>) -> AppResult<Order> {
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_text_unsupported(text: Option<&str>) -> AppResult<()> {
|
||||
// Future OCR search will plug in here. Until then, return a
|
||||
// distinct code (`text_search_not_yet_supported`) so clients can
|
||||
// detect "feature pending" vs. a generic 4xx — the code is the
|
||||
// wire contract, not the message.
|
||||
if text.is_some_and(|s| !s.trim().is_empty()) {
|
||||
return Err(AppError::NotImplemented {
|
||||
code: "text_search_not_yet_supported",
|
||||
message: "text search is reserved for the planned OCR input but not yet supported",
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_chapters_for_tag(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(user): CurrentUser,
|
||||
Query(params): Query<AggregateParams>,
|
||||
) -> AppResult<Json<PagedResponse<TaggedChapterAggregate>>> {
|
||||
ensure_text_unsupported(params.text.as_deref())?;
|
||||
let tag = normalize_tag(¶ms.tag)?;
|
||||
let order = parse_order(params.order.as_deref())?;
|
||||
let limit = params.limit.clamp(1, 200);
|
||||
let offset = params.offset.max(0);
|
||||
let (items, total) = repo::page_tag::aggregate_chapters_for_tag(
|
||||
&state.db, user.id, &tag, order, limit, offset,
|
||||
&state.db, user.id, &tag, order, limit, offset, params.text.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
|
||||
@@ -447,13 +431,12 @@ async fn list_mangas_for_tag(
|
||||
CurrentUser(user): CurrentUser,
|
||||
Query(params): Query<AggregateParams>,
|
||||
) -> AppResult<Json<PagedResponse<TaggedMangaAggregate>>> {
|
||||
ensure_text_unsupported(params.text.as_deref())?;
|
||||
let tag = normalize_tag(¶ms.tag)?;
|
||||
let order = parse_order(params.order.as_deref())?;
|
||||
let limit = params.limit.clamp(1, 200);
|
||||
let offset = params.offset.max(0);
|
||||
let (items, total) = repo::page_tag::aggregate_mangas_for_tag(
|
||||
&state.db, user.id, &tag, order, limit, offset,
|
||||
&state.db, user.id, &tag, order, limit, offset, params.text.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
|
||||
|
||||
Reference in New Issue
Block a user