# Content Repository(候选查询与数据访问层)|Plan > 对应规范:`spec_kit/Personalized Reco/modules/content-repository/spec.md` > > 依赖对齐: > > - DB 设计:`spec_kit/Personalized Reco/modules/db-design/plan.md` > - 回退梯度与场景口径:`spec_kit/Personalized Reco/spec.md`(Fallback Ladder + 场景默认参数) --- ## 1. 目标与交付物 ### 1.1 目标 - 为推荐引擎提供**与 ORM/SQL 解耦**的数据访问接口:候选召回与按 ID 批量获取。 - 将 DB 内部存储形态(JSON、关联表、NULL 语义等)统一“规范化”为上层稳定的 `ContentProfile` 结构。 - 在候选不足时支持按 `fallback_level (L0~L3)` 进行**可控降级**(降个性化/回退通用池/安全池),并且不产生 N+1 查询。 ### 1.2 交付物 - `modules/content-repository/plan.md`:本技术计划(本文件)。 - 代码实现(后续 tasks 阶段落地)建议位置: - `server/app/features/personalized_reco/content_repository/`(或等价目录) - 需要包含: - 抽象接口 `ContentRepository`(Protocol 或 ABC) - SQLAlchemy 实现 `SqlAlchemyContentRepository` - `ContentProfile`(DTO/数据结构)与规范化工具函数 - 单元测试与最小集成测试(后续 tasks 阶段落地): - risk_flags 映射与去重 - suitability 缺失兜底 - personalization_power 映射 - 查询不出现按 `content_id` 循环查 flags(避免 N+1) --- ## 2. 关键技术决策(V1) ### 2.1 存储形态(来自 DB Design 的已对齐结论) - `content_id`:MySQL 自增主键(`int`)。 - `context_suitability_json / need_suitability_json`:JSON 存储。 - `risk_flags`:关联表 `content_risk_flags(content_id, flag)`(同一 content 下 `uniq_content_flag` 去重)。 - `emotion_score`:`NULL` 表示 general。 - `review_confidence`:DB 可为 `NULL`;读取层输出时按 `0.7` 兜底(对齐规则口径)。 - `personalization_power`:DB 推荐存 `0/5/10`;读取层输出稳定为 `0.0/0.5/1.0`。 ### 2.2 职责边界(避免耦合) - `Content Repository` **不负责** Hard Filter/Soft Scoring/Rerank/Freqcap(这些由引擎编排与打分子模块完成)。 - `Content Repository` **负责**: - 按 `fallback_level` 对候选池做“降级约束”(例如限制 `personalization_power`、回退通用池/安全池) - 输出稳定结构(JSON 解析、默认值兜底、旧 flag 映射) --- ## 3. 接口与数据结构(V1) ### 3.1 接口定义(与 spec 对齐) - `fetch_candidates(scene, user_profile, fallback_level, limit, locale, exclude_content_ids=None) -> List[ContentProfile]` - `fetch_contents_by_ids(content_ids: List[int], locale) -> List[ContentProfile]` ### 3.2 `ContentProfile`(输出契约的推荐形态) 稳定字段(必须输出): - `content_id: int` - `text: str` - `stage: "general" | "expecting" | "parenting" | "unknown"` - `emotion_score: float | None`(`None` 表示 general) - `context_suitability: Dict[str, float]` - `need_suitability: Dict[str, float]` - `personalization_power: float`(`0/0.5/1`) - `risk_flags: List[str]` 可选字段(尽量输出): - `author_id: str | None` - `template_id: str | None` - `review_confidence: float`(缺失/NULL 按 `0.7` 输出) --- ## 4. 读取层规范化(Normalization) ### 4.1 text 选文案与多语言策略(不允许回退) 数据来源(当前 DB/ORM 约定): - `contents.text_en`:英文 - `contents.text_tc`:繁体中文 规则(**不允许语言回退**): - `locale=en*`:仅允许返回存在 `text_en` 的内容;输出 `text = text_en` - `locale=tc/zh-TW/zh-HK`:仅允许返回存在 `text_tc` 的内容;输出 `text = text_tc` 若内容缺少目标语言文本(例如 `locale=en*` 但 `text_en` 为空):该内容视为不可用,必须在候选/按 ID 获取时过滤掉。 ### 4.1 suitability JSON 解析与缺失兜底 固定 key 集合(对齐 DB Plan 的最小入库契约): - `context_suitability`:`family/work/relationship/friends/health` - `need_suitability`:`emotional_support/parenting_pressure/self_worth/anxiety_relief/rest_balance` 规则: - 若 DB 字段缺失/为 `NULL`/解析失败:**补齐为全 0.5**(以上所有 key 均为 `0.5`)。 - 若 DB JSON 存在但缺少部分 key:对缺少 key 补 `0.5`,其余按原值。 - 值域约束:期望值为 `0/0.5/1`;若出现其他值(例如字符串、越界浮点),按 `0.5` 兜底并记录告警日志(V1 可先打 debug,后续接入可观测模块)。 ### 4.2 `review_confidence` 兜底 - DB `review_confidence` 为 `NULL` 或缺失:输出 `0.7`。 ### 4.3 `personalization_power` 映射 若 DB 存 `0/5/10`: - `0 -> 0.0` - `5 -> 0.5` - `10 -> 1.0` 若读到其他值:按 `0.0` 兜底并记录告警日志。 ### 4.4 risk_flags 旧→新映射与输出约束 映射表(对齐 spec): - `block_stage_unknown` → `unsafe_for_stage_unknown` - `block_stage_parenting` → `unsafe_for_stage_parenting` - `block_emotion_low` → `unsafe_for_emotion_low` - `block_health_sensitive` → `block_health_medical`(V1 保守硬拦截) 输出约束: - 输出 `risk_flags` 必须去重。 - 输出不得包含旧命名。 - 输出建议稳定排序(便于测试与可观测):按字典序排序或按严重等级排序(V1 可先字典序)。 --- ## 5. 查询策略(V1) > 原则:DB 层先做“粗过滤”,应用层再做“精过滤/打分”。避免在 V1 过早依赖 JSON 路径查询索引。 ### 5.1 `fetch_contents_by_ids`(按 ID 批量获取) 目标: - 输入任意 `content_id` 列表,返回无重复的 `ContentProfile` 列表。 - 避免 N+1:不得按 `content_id` 循环查 `content_risk_flags`。 - 返回顺序:必须与输入 `content_ids` 一致(对“缺记录/缺语言文本”的 id 采取跳过策略,见下)。 缺记录/缺语言文本的处理(V1 约定): - 若某个 `content_id` 在 DB 中不存在,或按 `locale` 规则无法产出 `text`:该 id 在返回列表中**跳过**(不返回占位对象)。 推荐实现形态(两段式,避免 JOIN 导致重复行): 1. **批量拉主体与画像**(`contents` JOIN `content_profiles`),限制 `content_id IN (...)`。 2. **批量拉 risk_flags**:`SELECT content_id, flag FROM content_risk_flags WHERE content_id IN (...)`,在应用层按 `content_id` 聚合为集合,再做旧→新映射与去重。 备注: - 由于 `content_risk_flags` 是 1:N,直接三表 JOIN 容易导致行膨胀;两段式更便于组装与去重。 ### 5.2 `fetch_candidates`(候选召回,支持 L0~L3) 输入: - `scene: feed | push | widget` - `user_profile`(允许字段缺失) - `fallback_level: 0|1|2|3` - `limit` - `exclude_content_ids`(可选) #### 5.2.1 fallback_level 约束(对齐大规范 Fallback Ladder) 从 `spec_kit/Personalized Reco/spec.md` 对齐: - **L0**:正常召回配比(不在读取层实现复杂配比,读取层只保证候选池足够大且不过度放宽) - **L1**:放宽匹配 + 降个性化:限制 `personalization_power ≤ 0.5` - **L2**:回退通用池 + 进一步降个性化:限制 `personalization_power = 0`,且优先 `stage=general` - **L3**:兜底安全池:限制 `is_safe_pool = true`(安全池字段来自 DB Design) 缺失字段的最小处理(对齐大规范 Candidate Generation 口径): - 若 `user_profile` 缺失明显(need/context/emotion 任一缺失):读取层按**至少 L1** 的约束执行(即使入参 fallback_level=0)。 #### 5.2.2 stage 粗过滤策略(V1) 读取层可做的最小粗过滤(不引入复杂业务判断): - **L2/L3**:只取 `stage=general`(L3 额外 `is_safe_pool=true`)。 - **L0/L1**: - 优先取 `stage=用户匹配阶段` + `stage=general` - 若无法从 `user_profile` 明确阶段,则仅取 `stage=general`(避免误推) > 说明:更细粒度的阶段/跨维度规则(例如 unknown+parenting_pressure 的禁推)由 Hard Filter 子模块实现;读取层仅做粗过滤以减少扫描与传输。 #### 5.2.3 查询形态(避免 JOIN 行膨胀 + 保证 limit) 推荐采用“两段式候选召回”: 1. **先只查候选 ID 列表**(`contents` JOIN `content_profiles`),应用粗过滤(stage / personalization_power / is_safe_pool / exclude_content_ids),并增加 **locale 文本存在性过滤**(不允许语言回退),再用 `LIMIT limit * multiplier` 拉一批候选 ID(`multiplier` 例如 3~5,避免后续去重/过滤后不足)。 2. **再用 `fetch_contents_by_ids` 批量补全字段**(主体+画像+risk_flags),最终在应用层去重并截断到 `limit`。 排序(V1): - 若没有更明确的排序字段:使用 `updated_at DESC` 或随机抽样(需谨慎,MySQL `ORDER BY RAND()` 在大表会慢)。 - 推荐:V1 先用 `content_profiles.updated_at DESC` 或 `contents.created_at DESC`,后续由打分模块决定最终排序。 --- ## 6. 性能与可观测(V1) ### 6.1 性能约束 - 单次调用不得出现按 `content_id` 循环查库(避免 N+1)。 - `fetch_candidates` 必须在 DB 层支持 `limit`,并尽量通过粗过滤减少扫描。 ### 6.2 建议打点/日志(为 observability 子模块预留) 在 Repository 层建议输出 debug 级日志(或埋点字段,供上层汇总): - `scene` - `fallback_level`(入参)与 `effective_fallback_level`(考虑缺失字段自动至少 L1 后的实际约束级别) - `limit`、`exclude_content_ids_count` - `candidate_ids_size_raw`(第 1 段查到的候选 ID 数) - `candidate_size_returned`(最终返回数量) --- ## 7. 测试计划(V1) ### 7.1 单元测试(纯函数) - risk_flags 映射: - 输入包含旧 flag,输出只包含新命名 - 去重与稳定排序 - suitability 兜底: - DB 字段缺失/NULL/解析失败 → 全 0.5 - 部分 key 缺失 → 补齐 0.5 - personalization_power 映射: - 0/5/10 → 0.0/0.5/1.0 - 异常值 → 0.0 兜底 - review_confidence: - NULL/缺失 → 0.7 ### 7.2 最小集成测试(含数据库) - `fetch_contents_by_ids`: - 输入多个 id 返回无重复 - flags 聚合正确(同一 content 多条 flag 行能聚合成 list) - 查询次数断言(避免 N+1): - `fetch_contents_by_ids`:固定 2 次查询(主体+画像一次,flags 一次) - `fetch_candidates`:固定 3 次查询(候选 id 一次 + `fetch_contents_by_ids` 两次),或实现允许的常数级次数 --- ## 8. 风险与后续演进 ### 8.1 已知风险 - V1 不做 JSON 路径索引:候选量变大后,粗过滤不足可能导致候选池拉取过多、应用层过滤成本上升。 - `ORDER BY RAND()` 的性能风险:候选大表下不可用,需要替代策略(时间窗口抽样/预生成候选池)。 ### 8.2 V1.1 优化方向(与 DB Plan 对齐) - 为常用 need/context key 增加生成列/函数索引(从 JSON_EXTRACT 提取到 TINYINT)以加速召回。 - 为强规则风险(如 `block_health_medical`)增加派生布尔列或缓存表,减少 JOIN 成本。