# 肿瘤科标签体系设计 ## 设计原则 - 基于 MeSH (Medical Subject Headings),从 NLM XML 导入 - MeSH C04 (Neoplasms) 为核心,交叉 E02(治疗) + D27(药物) - 补充 MeSH 未覆盖的临床维度:驱动基因/靶点、临床终点、临床场景 - 三级来源体系:手工维护(manual)→ MeSH 导入(mesh)→ 管道懒创建(auto) - 用户端只展示已激活(is_active=true)的标签 --- ## 一、标签分类(tag_category) | 分类 | 枚举值 | 来源 | 示例 | |------|--------|------|------| | 癌种部位 | `cancer` | MeSH C + 手工 | 肺癌、乳腺癌、结直肠癌 | | 组织分型 | `histology` | MeSH C04.557 | 腺癌、鳞癌、淋巴瘤 | | 驱动基因/靶点 | `gene` | 手工维护(无 MeSH UI,但种子标签有 name_en) | EGFR、ALK、KRAS、PD-L1 | | 治疗方式 | `treatment` | MeSH E + 手工 | 靶向治疗、免疫治疗、放疗 | | 研究类型 | `study_type` | MeSH Publication Type | 指南、RCT、Meta分析 | | 临床终点 | `endpoint` | 手工维护 | OS、PFS、ORR | | 临床场景 | `scenario` | 手工维护 | 新辅助、一线治疗、维持治疗 | | 其他 MeSH | `mesh_other` | 管道懒创建 | 无法归类的 MeSH Descriptor | `mesh_other` 是管道路径的兜底分类,按 mesh_ui 前缀映射(见 §3.5)。 --- ## 二、三级来源体系 ### 2.1 来源定义 | 来源 | 数量(生产) | 含义 | is_active 默认值 | |------|-------------|------|-----------------| | `manual` | 99 | 手工维护的种子标签,有中文名、有层级 | `true` | | `mesh` | ~670 | C04 + E02 + D27 批量导入标签(NLM 权威,全英文) | `true` | | `auto` | ~20,122 | 管道懒创建,meeting 未匹配 MeSH 术语自动创建 | `false` | ### 2.2 manual 标签详情(99 条) ``` category total active articles has_mesh_ui cancer 33 33 484 27 treatment 18 18 126 7 gene 18 18 46 0 endpoint 10 10 153 4 study_type 9 9 103 4 scenario 11 11 85 3 ``` - 45/99 有 mesh_ui 能直接匹配 PubMed MeSH - 17 个关键基因标签(EGFR、ALK、KRAS、BRAF 等)通过 `tmp_fix_tag_mesh_ui.py` 脚本映射到正确 mesh_ui - 28 个标签 article_count=0(主要为 grouping 节点和部分罕见癌种),is_selectable=false 的 grouping 节点不参与打标 - 33 个非 selectable 的 grouping 节点(如"肺癌→非小细胞肺癌"下的层级节点) ### 2.3 mesh 标签详情(~670 条) 从 `scripts/import_mesh_tags.py` 导入,import_mesh_tags.py 先按 name_en 匹配,匹配不到的创 building 建有 mesh_ui 的新标签。 - **导入策略**:先匹配已有 manual 标签的 name_en,命中则关联不新建;未命中则创建新行 - **匹配问题**:约 15 个标签因命名不一致(如 seed "Lung Cancer" vs MeSH "Lung Neoplasms")未匹配到,创建了重复标签 ### 2.4 auto 标签详情(~20,122 条) 自动创建,无中文名,无层级,is_active=false。主要用于: 1. 保证打标覆盖(即使没有 manual/mesh 标签匹配) 2. 后台可见,供管理员审核后提升为 manual 标签 --- ## 三、MeSH → 标签匹配策略(三阶段) ### 3.1 总体流程 ``` mesh_headings[](来自 PubMed 文章) │ ├─ 阶段 1:mesh_ui 精确匹配 ──→ 查询 global_tags WHERE mesh_ui IN (...) │ 命中 → 使用 manual/mesh 标签 │ 未命中 → 进入阶段 2 │ ├─ 阶段 2:name_en 回退匹配 ──→ 查询 global_tags WHERE name_en IN (...) │ 命中 → 使用 manual/mesh 标签(解决无 mesh_ui 的种子标签) │ 未命中 → 进入阶段 3 │ └─ 阶段 3:懒创建 ──→ GlobalTag(source="auto", is_active=false) name_zh=null,仅存英文名 等待后台审核后激活 ``` ### 3.2 阶段 1:mesh_ui 精确匹配 ```python # tag_service.py:42-46 result = await db.execute( select(GlobalTag).where(GlobalTag.mesh_ui.in_(mesh_uis)) ) matched: list[GlobalTag] = list(result.scalars().all()) ``` - 每条 PubMed 文章的 MeSH Headings 包含 `{descriptor, ui, major}` - 用 `ui`(如 `D008175`)精准匹配 `global_tags.mesh_ui` - 命中率高,但要求标签预先设置 mesh_ui ### 3.3 阶段 2:name_en 回退匹配 ```python # tag_service.py:54-62 — 阶段 2 回退 result = await db.execute( select(GlobalTag).where(GlobalTag.name_en.in_(unmatched_names)) ) ``` - 为没有 mesh_ui 的种子标签提供匹配机会(如大部分 gene 标签) - 依赖 MeSH heading 的 `descriptor` 字段与标签 `name_en` 的精确匹配 - **局限性**:MeSH 官方名称(如 "ErbB Receptors")与 seed 标签名称("EGFR")不一致时无法匹配 ### 3.4 阶段 3:懒创建 ```python # tag_service.py:70-86 — 懒创建 tag = GlobalTag( mesh_ui=ui, name_en=mh.get("descriptor", ""), name_zh=None, source="auto", is_active=False, ) ``` - 前面两阶段均未命中时才触发 - 创建 source=auto, is_active=false 的标签 - 无中文名、无层级 - 确保每篇文章的每个 MeSH heading 都能映射到标签 ### 3.5 分类推断规则 懒创建时按 mesh_ui 前缀决定 tag_category: ```python _CATEGORY_MAP = { "C": "cancer", "D": "gene", "E": "treatment", "V": "study_type", # F, G, H, I, J, K, L, M, N, Z → "mesh_other" } ``` --- ## 四、关键词回填策略 ### 4.1 为无 mesh_ui 的 manual 标签补充文章关联 部分 manual 标签(如 scenario/endpoint 类别)没有 MeSH 对等词,无法通过 mesh_ui 或 name_en 匹配。改用标题/摘要关键词 ILIKE 匹配: ```python # tmp_keyword_tag_backfill.py(一次执行) UPDATE global_tags SET is_active = false WHERE source = 'manual' AND article_count = 0 AND is_selectable = true; # → 临时停用 32 个零文章 manual 标签 # 对每个零文章标签,按 keywords[] 做 title ILIKE 查询 INSERT INTO global_literature_tags (literature_id, tag_id, is_major) SELECT lit.id, :tag_id, false FROM global_literature lit WHERE ... AND NOT EXISTS (...) LIMIT 200; ``` ### 4.2 回填结果 | 标签 | 关键词 | 新增关联数 | |------|--------|-----------| | Case Report | case report, case series | ~35,686 | | Adjuvant | adjuvant | ~20,603 | | Real World Study | real.world, retrospective | ~6,002 | | Overall Survival | overall survival | ~4,968 | | 其他 12 个 | 各场景关键词 | ~16,683 | | **合计** | | **~83,942** | **局限**:ILIKE 匹配假阳性高(如 "adjuvant" 可能匹配到非临床场景),但作为初始覆盖足够。 --- ## 五、article_count 维护 ### 5.1 增量更新 ```python # tag_service.py:114 — 每次新增关联时更新 tag.article_count += 1 ``` - `tag_article()` 中每次新增 `GlobalLiteratureTag` 记录时 +1 - 不需要定期重算,实时准确 - 删除标签关联时不自动减(当前平台无删除操作入口) ### 5.2 手动全量刷新 ```sql UPDATE global_tags gt SET article_count = (SELECT count(*) FROM global_literature_tags lt WHERE lt.tag_id = gt.id) WHERE source = 'manual'; ``` --- ## 六、标签激活体系 ### 6.1 激活规则 | 场景 | 操作 | |------|------| | source=manual, article_count=0, is_selectable=true | is_active=false(临时隐藏,待回填后激活) | | source=manual, article_count>0, is_active=false | is_active=true(回填完成后激活) | | source=mesh, article_count=0 | is_active=false(无文献的过分精细节点) | | source=auto | is_active=false(待审核) | ### 6.2 生产环境执行 ```sql -- 停用零文章 mesh 标签 UPDATE global_tags SET is_active = false WHERE source = 'mesh' AND article_count = 0; -- 165 个零文章 mesh 标签 → 停用 -- 停用零文章且非 selectable 的 manual 标签 UPDATE global_tags SET is_selectable = false WHERE source = 'manual' AND article_count = 0 AND EXISTS (SELECT 1 FROM global_tags t2 WHERE t2.source = 'manual' AND t2.path LIKE global_tags.path || '::%'); -- 8 个 grouping 节点 → 不可选 -- 停用零文章 manual 叶子标签 UPDATE global_tags SET is_active = false WHERE source = 'manual' AND article_count = 0 AND is_selectable = true; -- 32 个 → 停用。mesh_ui 修复 + 关键词回填后重新激活 ``` --- ## 七、标签树结构 ### 7.1 level 的含义 | level | 含义 | 示例 | selectable | |-------|------|------|------------| | 1 | 大类(按部位/类型分组) | 肺癌、乳腺癌 | false | | 2 | 具体癌种 | 非小细胞肺癌、三阴性乳腺癌 | true | | 3+ | 亚型/细分 | 肺腺癌、19del | true | **注意**:level 不代表层级深度,而是按语义粒度划分。某些 level=2 的节点也是 grouping 节点(如"妇科肿瘤"下有子节点但 level=2),通过 is_selectable=false 标记。 ### 7.2 grouping 节点识别 ```sql -- 判断某节点是否为 grouping 节点 -- 存在其他 manual 标签的 path 以此为前缀 SELECT EXISTS ( SELECT 1 FROM global_tags t2 WHERE t2.source = 'manual' AND t2.path LIKE '当前标签.path' || '::%' AND t2.id != '当前标签.id' ); ``` ### 7.3 完整标签树 #### 📍 癌种部位(cancer, 33 条) ``` 肿瘤科标签体系 ├── 肺癌 (level 1, selectable=false) │ ├── 非小细胞肺癌 NSCLC (level 2) │ │ ├── 肺腺癌 (level 3) │ │ ├── 肺鳞癌 (level 3) │ │ └── 大细胞肺癌 (level 3) │ └── 小细胞肺癌 SCLC (level 2) ├── 乳腺癌 (level 1, selectable=false) │ ├── HR+/HER2- Luminal型 (level 2) │ ├── HER2+ (level 2) │ └── 三阴性乳腺癌 TNBC (level 2) ├── 结直肠癌 (level 1, selectable=false) │ ├── 结肠癌 (level 2) │ ├── 直肠癌 (level 2) │ └── MSI-H/dMMR 结直肠癌 (level 2) ├── 胃癌 (level 1, selectable=false) │ ├── 胃腺癌 (level 2) │ └── 胃食管结合部癌 (level 2) ├── 肝癌 (level 1, selectable=false) │ ├── 肝细胞癌 HCC (level 2) │ ├── 胆管癌 (level 2) │ └── 肝转移癌 (level 2) ├── 食管癌 (level 1, selectable=false) ├── 胰腺癌 (level 1, selectable=false) ├── 前列腺癌 (level 1, selectable=false) ├── 膀胱癌 / 肾癌 (level 1, selectable=false) ├── 头颈癌 (level 1, selectable=false) │ ├── 鼻咽癌 (level 2) │ ├── 口腔癌 (level 2) │ ├── 喉癌 (level 2) │ └── 甲状腺癌 (level 2) ├── 妇科肿瘤 (level 2, selectable=false) │ ├── 卵巢癌 (level 3) │ ├── 宫颈癌 (level 3) │ └── 子宫内膜癌 (level 3) ├── 黑色素瘤 / 皮肤癌 (level 1, selectable=false) ├── 脑肿瘤 (level 1, selectable=false) │ ├── 胶质母细胞瘤 GBM (level 2) │ ├── 脑膜瘤 (level 2) │ └── 脑转移瘤 (level 2) ├── 血液肿瘤 (level 1, selectable=false) │ ├── 白血病 (level 2, selectable=false) │ │ ├── AML (level 3) │ │ ├── ALL (level 3) │ │ ├── CML (level 3) │ │ └── CLL (level 3) │ ├── 淋巴瘤 (level 2, selectable=false) │ │ ├── 霍奇金淋巴瘤 HL (level 3) │ │ └── 非霍奇金淋巴瘤 NHL (level 3) │ │ ├── DLBCL (level 4) │ │ ├── 滤泡性淋巴瘤 (level 4) │ │ ├── 套细胞淋巴瘤 (level 4) │ │ └── T细胞淋巴瘤 (level 4) │ └── 多发性骨髓瘤 (level 2) ├── 肉瘤 (level 1, selectable=false) ├── 原发不明肿瘤 CUP (level 1) └── 儿童肿瘤 (level 1) ``` #### 🧬 驱动基因/靶点(gene, 18 条) ``` ├── EGFR → 19del / L858R / T790M / C797S / exon20ins ├── ALK → EML4-ALK / 耐药突变 ├── ROS1 ├── BRAF V600E ├── KRAS → G12C / G12D ├── HER2 (ERBB2) → 扩增 / 突变 ├── MET → exon14跳读 / 扩增 ├── RET ├── NTRK1-3 ├── FGFR1-4 ├── IDH1/2 ├── FLT3 ├── KIT / PDGFRA ├── BRCA1/2 / HRD ├── MSI-H / dMMR ├── TMB-H ├── PIK3CA / PTEN / AKT ├── TP53 / RB1 / MYC └── PD-L1 ``` 所有 gene 标签均无 mesh_ui,依靠 stage 2 name_en 回退匹配或关键词回填。 #### 💊 治疗方式(treatment, 18 条) ``` ├── 靶向治疗 → TKI / 单克隆抗体 / ADC / 双特异性抗体 ├── 免疫治疗 → PD-1抑制剂 / PD-L1抑制剂 / CTLA-4抑制剂 / LAG-3抑制剂 / CAR-T ├── 化疗 → 铂类 / 紫杉类 / 抗代谢 / 拓扑异构酶抑制剂 ├── 放疗 → 常规分割 / SBRT/SRS / 质子 / 重离子 ├── 手术 → 微创 / 机器人 / 器官保留 ├── 内分泌治疗 → 他莫昔芬 / 芳香化酶抑制剂 / 抗雄激素 ├── 抗血管生成 → 贝伐珠单抗 / 安罗替尼 / 阿帕替尼 ├── 骨髓移植 → 自体 / 异基因 └── 支持治疗 → 骨转移 / 止吐 / 疼痛 / 营养 ``` 7/18 有 mesh_ui。 #### 📊 研究类型(study_type, 9 条) ``` ├── 临床实践指南 → NCCN / CSCO / ESMO ├── 随机对照试验 RCT → 3期 / 2期 ├── 系统综述/Meta分析 ├── 真实世界研究 RWS ├── 病例报告/病例系列 ├── 转化研究 ├── 基础研究 └── 卫生经济学/HTA ``` 4/9 有 mesh_ui。 #### 📈 临床终点(endpoint, 10 条) ``` ├── OS (总生存期) ├── PFS (无进展生存期) ├── DFS (无病生存期) ├── ORR (客观缓解率) / DCR (疾病控制率) ├── 安全性/AE ├── 生物标志物 → ctDNA / CTC / MRD ├── 生活质量 QoL / PRO └── 耐药机制 ``` #### 🏥 临床场景(scenario, 11 条) ``` ├── 早期/可手术 → 新辅助 / 辅助 ├── 局部晚期 → 同步放化疗 / 转化治疗 ├── 晚期/转移性 → 一线 / 二线+ / 后线 ├── 维持治疗 ├── 姑息/支持治疗 ├── 寡转移 / 寡进展 ├── 老年肿瘤 ├── 儿童肿瘤 └── 遗传性肿瘤 / 家系 ``` --- ## 八、API 端点 ### 8.1 公共标签 ``` GET /public/tags → 全量标签树(Redis 缓存,1h TTL,key="public:tags") GET /public/cancer-tags → 仅癌种标签树 ``` 返回字段: ```json { "id": "uuid", "name_zh": "肺癌", "name_en": "Lung Cancer", "path": "肿瘤科标签体系::肺癌", "tag_category": "cancer", "level": 1, "article_count": 62, "children": [...] } ``` ### 8.2 管理后台标签 ``` GET /admin/tags?page=1&page_size=20&search=&source=&category=&active= → 分页列表,支持 source/mesh/manual/auto + category + active 筛选 → 默认按 article_count DESC 排序 POST /admin/tags/refresh-counts → 全量刷新 article_count ``` ### 8.3 首页热门标签 ```vue const cancerTags = computed(() => allTags.value .filter((t: TagOption) => t.tag_category === 'cancer' && t.level === 2 && t.name_zh) .sort((a, b) => ((b.article_count as number) || 0) - ((a.article_count as number) || 0)) ) // 限制显示 7 条 + "更多" ``` - 只选 level=2 的具体癌种(排除 grouping 节点) - 过滤掉无中文名的标签 - 按 article_count 降序排列 --- ## 九、前端标签显示规则 ### 9.1 名称显示 ```vue {{ tag.name_zh || tag.name_en }} ``` 如果 `name_zh` 为空(auto 标签常见),退而显示 `name_en`。此规则在以下组件统一使用: - [LiteratureCard.vue](frontend/src/components/literature/LiteratureCard.vue) - [LiteratureDetailView.vue](frontend/src/views/app/LiteratureDetailView.vue)(登录态) - [LiteratureDetailView.vue](frontend/src/views/public/LiteratureDetailView.vue)(公开态) - [CancerBrowseView.vue](frontend/src/views/public/CancerBrowseView.vue) - [InterestSettingsView.vue](frontend/src/views/app/InterestSettingsView.vue) ### 9.2 文献卡片标签加载 ```python # tag_loader.py — 共享标签加载工具 async def load_tags_for_literature(db, literature_ids): select(GlobalLiteratureTag.literature_id, GlobalTag.id, GlobalTag.name_zh, GlobalTag.name_en, # ← 必须同时加载 GlobalTag.path, GlobalTag.tag_category, GlobalLiteratureTag.is_major) ... ``` **历史修复**:曾因 SELECT 遗漏 `name_en` 导致 name_zh=null 的标签显示为空白。 ### 9.3 暗色模式 ```vue // PublicLayout.vue + AppLayout.vue — 用户头像背景色适配暗色模式 const avatarStyle = computed(() => ({ background: ui.isDark ? '#1a3a5c' : '#e6f4ff', color: 'var(--text-primary)', })) ``` 使用 computed `:style` 绑定而非 CSS class,因为 NAvatar 有内联样式优先级高于外部 CSS。 --- ## 十、缓存策略 | 缓存 | Key | TTL | 刷新机制 | |------|-----|-----|---------| | 公共标签树 | `public:tags` | 3600s | 被动过期,首次请求回退 DB | | 首页 Feed | `homepage:feed` | 1800s | ARQ 定时每 30min 刷新 | | 热门文章 | `hot_articles` | 1800s | ARQ 定时每 30min 刷新 | --- ## 十一、数据维护 ### 11.1 标签导入 ```bash # 种子标签导入(dev 环境) cd backend && python scripts/seed_data.py # C04 MeSH 批量导入 python scripts/import_mesh_tags.py \ --mesh-xml desc2025.xml.gz \ --categories C04 \ --cross-categories E02 E04 D27 \ --specialty oncology ``` ### 11.2 一次性数据清洗 ```sql -- 查看标签分布 SELECT source, tag_category, count(*), sum(article_count) FROM global_tags GROUP BY source, tag_category ORDER BY source, tag_category; -- 查看零文章标签 SELECT name_zh, name_en, source, tag_category, is_active FROM global_tags WHERE (article_count = 0 OR article_count IS NULL) ORDER BY source, tag_category; -- 刷新 article_count UPDATE global_tags gt SET article_count = (SELECT count(*) FROM global_literature_tags lt WHERE lt.tag_id = gt.id); -- 合并重复标签(name_en 近似但不同 ID) -- 先将旧标签的 lit association 迁移到新标签 INSERT INTO global_literature_tags (literature_id, tag_id, is_major) SELECT lt.literature_id, :target_tag_id, lt.is_major FROM global_literature_tags lt WHERE lt.tag_id = :source_tag_id AND NOT EXISTS (SELECT 1 FROM global_literature_tags lt2 WHERE lt2.literature_id = lt.literature_id AND lt2.tag_id = :target_tag_id); -- 然后删除旧标签 DELETE FROM global_literature_tags WHERE tag_id = :source_tag_id; DELETE FROM global_tags WHERE id = :source_tag_id; ``` ### 11.3 生产部署注意事项 ```bash # 部署前确保标签变更已提交 cd backend && python -m alembic -c alembic/alembic.ini upgrade head # 重建前端 cd frontend && rm -rf dist node_modules/.vite && npm run build # 重启 worker(使标签缓存刷新) docker compose restart worker ``` --- ## 十二、已知问题与改进方向 ### 12.1 约 26% 的文献无标签 - **原因**:`mesh_headings=[]` 的文献(in-process/publisher 记录,NLM 尚未标引) - **影响范围**:约 322,507 篇(生产数据) - **改进方向**:标题/摘要关键词匹配(已对 scenario/endpoint 标签实现,可推广到癌种) ### 12.2 name_en 匹配精度 - MeSH 官方术语名称与种子标签名称不一致(如 "ErbB Receptors" ≠ "EGFR") - 当前方案:为关键基因标签手动设置正确的 mesh_ui 绕过此问题 ### 12.3 不完善的自动分类 - `_guess_category()` 仅按 mesh_ui 首字母分类,粒度过粗 - Phase 2 计划:基于 LLM 的精细分类 ### 12.4 删除标签关联不减 article_count - 当前 article_count 只增不减 - 如需删除标签关联(如同一 auto 标签合并到 manual 标签),需手动刷新计数 ### 12.5 重复标签 - 约 15 个 seed 标签因 name_en 命名差异未匹配到 C04 导入标签,导致重复 - 可通过 `tmp_fix_tag_mesh_ui.py` 类似的合并脚本处理 --- ## 十三、关键维护脚本 | 脚本 | 用途 | 执行环境 | |------|------|---------| | `seed_data.py` | 导入 99 条种子标签 + 期刊 | dev | | `import_mesh_tags.py` | 导入 C04 MeSH 标签 | 一次性 | | `tmp_fix_tag_mesh_ui.py` | 修复 17 个 gene/treatment 标签的 mesh_ui 映射 | 生产(已执行) | | `tmp_keyword_tag_backfill.py` | 关键词回填 16 个 scenario/endpoint 标签 | 生产(已执行) | | `tmp_audit_tags.py` | 审计标签分布 | 生产 |