Files
UHD_Media_Tool/docs/runbook/search.md
T
rootandClaude Opus 5.5 a0946bf480 feat(search): global search aggregation backend (W9-G)
- GET /search?q=&scope=all|media|releases|actions&limit=: local works (title_alias
  exact/prefix/trgm + name prefix, slot status summary), TMDB candidates (cached,
  localEntityId), cross-site releases, page/action jump list; tookMs per group
- cross-site: enabled sites via shared per-site limiter, 15s AbortSignal timeout,
  site concurrency cap 4, same-query coalescing, 30s cache; partial + failedSites;
  results upserted to source_release; inventory overlay via GET /inventory
- GET /search/releases: site/resolution/source/size/free/H&R filters, sort, cursor,
  facets; shares the cache
- POST /search/releases/{siteId}/{siteReleaseId}/plan: forwards in-process to
  POST /acquisition-plans (same auth, validation, audit, rejections)
- contracts/search.ts, runbook, tests (partial/timeout, coalesce, cache, overlay,
  filters/sort, plan)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 15:05:58 +08:00

7.0 KiB

全局搜索运行手册(W9-G)

实现:apps/server/src/modules/search/(routes.ts 端点、releases.ts 跨站搜索/缓存/筛选/叠加、media.ts 本地作品与 TMDB、actions.ts 页面与操作清单、dispatch.ts 进程内转调);合同 packages/contracts/src/search.ts;测试 apps/server/test/search/。

端点

端点 scope 说明
GET /search?q=&scope=all|media|releases|actions&limit= read 聚合搜索。limit 是每组条数(1–50,默认 10)。不查的组返回 null
GET /search/releases?q=&sites=&resolution=&source=&minSize=&maxSize=&free=&hr=&sort=&order=&refresh=&cursor=&limit= read 资源结果页。与 /search 共用缓存
POST /search/releases/{siteId}/{siteReleaseId}/plan {slotId?, entityId?, note?} organize.execute 从搜索结果下载,走 POST /acquisition-plans 同一流程

/search 响应

  • media.local:本地作品。召回方式有三种:title_alias.normalized 精确命中(score 1)、前缀命中(0.9)、trgm 相似度 ≥ 0.3;另外按作品名/原名前缀补召回没有别名的手建作品。每条带 slotCounts(版本目标各状态计数)、versionCount、downloadingCount、subscribed。
  • media.tmdb:TMDB 候选。configured=false 表示没配置 TMDB,不代表没有结果;查询失败时 error 有值、items 为空。已有本地作品接受了该 TMDB 身份时,localEntityId 给出本地作品 ID(合并过的作品跟到最终作品)。
  • releases:跨站资源,只返回前 limit 条(按默认排序),total 是全部条数。覆盖信息见下文。
  • actions:页面与操作跳转的静态清单,按 IA-3 定稿导航分组(定时任务在任务页"计划"Tab;运行状态页路由确定后补)。标题前缀命中排在前面,关键词或路径包含查询词也算命中。
  • 每组各带 tookMs,顶层 tookMs 是整个请求的耗时。

跨站资源搜索

  • 站点范围:enabled=true 且未删除的站点,全部参与,sites= 筛选在拿到结果之后才做。不支持搜索或没有适配器定义的站点记为 skipped。
  • 限流:沿用 sources-identity 运行时里每个站点的共享限流器,和快照、工作台、详情、下载用的是同一个令牌桶。
  • 超时:单站 AbortSignal.timeout(15s),限流等待也算在内;超时记为 timeout。
  • 站点级并发:同时最多搜索 4 个站点(createSearchModule({ siteConcurrency }))。
  • 并发合并:相同查询词(去首尾空白、小写、合并空格)同时只会发一轮站点请求,后到的请求等同一个结果,coalesced=true。
  • 短缓存:结果缓存 30s,命中时 cached=true。只要有一个站点成功就缓存;全部失败不缓存。refresh=true 跳过缓存,但仍然参与合并。进程内缓存,多个 api 实例之间不共享。
  • 部分失败:任一站点失败或超时,partial=true,failedSites 列出这些站点(status 为 failed 或 timeout,附已脱敏的错误)。其它站点的结果照常返回。sites 是每个站点的覆盖情况:status、count、tookMs、error。
  • 落库:搜索结果按 site + 站点资源 ID upsert 到资源表(source_release),新资源照常发 release.observed,进入识别流水线,和工作台实时补查是同一条路径。sourceReleaseId 给站点工作台、下载、委托 Agent 用。落库失败只记警告,仍然返回结果,此时 sourceReleaseId=null。搜索不写 site_snapshot,也不改站点健康状态。

作品关联与库存叠加

叠加每次请求时现算,不进缓存,所以识别完成后刷新一次就能看到。

  • entityMatch=resolved:资源已经识别到作品。
  • entityMatch=alias:资源还没识别,但解析出的中文或英文标题归一化后精确命中别名,且只命中一个作品(年份相差不超过 1)。这个关联没有确认,只用来显示库存。
  • inventory:转调 GET /inventory 得到,字段同工作台叠加(versions / gaps / downloading / subscribed / candidateReasons),其中 candidateReasons 只保留针对本条资源的评估。另有 state 字段,取值按优先级依次为 downloading > gap(有未满足的版本目标)> owned > subscribed > none。

结果页筛选与排序

  • sites、resolution、source 都是逗号分隔,大小写不敏感;解析不出的值记为 unknown,可以用来筛选。
  • minSize、maxSize 单位是字节,大小未知的资源会被排除。
  • free=true 只保留下载计量为 0 的资源,free=false 排除它们。
  • hr=true 只保留明确标了 H&R 的资源;hr=false 只排除明确 H&R 的,unknown 保留。目前观众等站点的定义没有 H&R 选择器,结果一律是 unknown。
  • sort=default|seeders|size|uploadedAt|site,配合 order=asc|desc。default 按做种数降序,做种数相同时按发布时间从新到旧;空值始终排在最后。
  • facets 统计的是未筛选的全部结果,包括站点、分辨率、来源三项。total 是筛选后的条数,unfilteredTotal 是筛选前的条数。游标是不透明的偏移量。

从搜索结果下载

  1. 按 (siteId, siteReleaseId) 找资源行。找不到时,用缓存里的站点结果补写一行(对应搜索时落库失败的情况)。缓存里也没有就返回 404,需要重新搜索。
  2. 进程内转调 POST /acquisition-plans,把 {...body, sourceReleaseId} 发给它。调用者身份沿用本次请求的身份,scope、入参校验、download.plan → download.submit、审计和拒绝原因都和直接调用那个端点一样,比如资源没关联作品时返回 412,作品没有版本目标时返回 no_slot。

进程内转调(dispatch.ts)

第一次调用时,把 /api/v1 下的模块路由表复制到一个内部 Hono 实例上,并注入当前请求已经通过鉴权的 principal / actor / session。对方端点抛出的 ApiException 原样抛回,由外层统一转成错误信封。搜索模块只读其它模块的 service 导出和站点运行时,库存叠加和下载都转调对方端点,不复制业务逻辑。

前端要点

  • 命令面板用 scope=media 和 scope=actions 快速出结果(毫秒级,TMDB 首次查询约 1 秒)。资源组单独请求 scope=releases,或者直接跳到结果页 /console/search?q=。最慢的站点可能要等到 15s 超时。
  • 资源行的操作和站点工作台一致。sourceReleaseId 可以直接给工作台详情和委托 Agent 用;下载用 POST /search/releases/{siteId}/{siteReleaseId}/plan。
  • partial=true 时列出 failedSites,提示哪些站点没有结果。cached / fetchedAt 用来标注数据时间,"刷新"按钮带 refresh=true。

排障

  • 某个站点一直 timeout:先在站点页做健康检测;限流太保守时,令牌等待也会占用 15s 预算,可以调整站点的 rate_limit。
  • 某个站点一直 skipped:可能是定义不支持搜索,也可能没有随产品发布的适配器定义(看 error 文本)。
  • 资源有结果但没有库存叠加:资源还没识别,而且标题没有精确命中唯一的作品别名。等识别流水线处理完,或者在工作台手动指定作品后刷新。