feat(search): page content-search endpoint (multi-tag AND + text + warnings)

New GET /v1/me/page-search surfacing the analysis worker's output:

- repo::page_analysis::page_search + PageSearchQuery: multi-tag AND across
  (user page_tags ∪ global page_auto_tags) via the unnest double-negative
  idiom, weighted OCR/scene text ranking (ts_rank over search_doc), and
  content-warning include/exclude. One row per page with is_nsfw +
  deduped content_warnings + rank.
- domain::PageSearchItem.
- api::page_tags: /me/page-search handler with CSV tag/warning parsing
  (parse_tags_csv reuses normalize_tag; parse_warnings_csv validates the
  closed vocabulary), requiring at least one positive filter (422 else).
  This is where the reserved OCR text search lands for pages.

Tests: multi-tag AND user∪auto, speech>sfx ranking, cw include/exclude
(+ row flags), text-only (no tags), missing-filter 422, unknown-warning
422, auth required.

Note: the /me/page-tags/chapters|mangas aggregations keep single-tag
behavior + the reserved text=501 for now; page-level search is the
primary text surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 18:58:07 +02:00
parent 7d80a437bf
commit af870bd157
8 changed files with 516 additions and 6 deletions

View File

@@ -28,8 +28,8 @@ pub use genre::{Genre, GenreRef};
pub use manga::{Manga, MangaCard, MangaDetail};
pub use page::Page;
pub use page_analysis::{
AnalysisStatus, ContentWarning, OcrKind, OcrResult, PageAnalysis, SafetyFlag,
VisionAnalysis,
AnalysisStatus, ContentWarning, OcrKind, OcrResult, PageAnalysis, PageSearchItem,
SafetyFlag, VisionAnalysis,
};
pub use page_tag::{
NewPageTag, PageTagSummary, TaggedChapterAggregate, TaggedMangaAggregate,

View File

@@ -108,6 +108,26 @@ impl ContentWarning {
}
}
/// One result row from the page content-search (`GET /v1/me/page-search`).
/// One row per matching page, carrying the breadcrumb plus the moderation
/// flags and the text-search rank so the UI can badge and order results.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct PageSearchItem {
pub page_id: Uuid,
pub chapter_id: Uuid,
pub manga_id: Uuid,
pub page_number: i32,
pub chapter_number: i32,
pub chapter_title: Option<String>,
pub manga_title: String,
pub storage_key: String,
pub is_nsfw: bool,
/// Deduped content warnings on this page (canonical lowercase names).
pub content_warnings: Vec<String>,
/// `ts_rank` against the text query; `0` for tag/warning-only searches.
pub rank: f32,
}
/// One persisted `page_analysis` row.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct PageAnalysis {