Files
backend/docs/05-标签体系设计.md
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

20 KiB
Raw Blame History

肿瘤科标签体系设计

设计原则

  • 基于 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 阶段 1mesh_ui 精确匹配

# 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 阶段 2name_en 回退匹配

# 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:懒创建

# 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

_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 匹配:

# 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 增量更新

# tag_service.py:114 — 每次新增关联时更新
tag.article_count += 1
  • tag_article() 中每次新增 GlobalLiteratureTag 记录时 +1
  • 不需要定期重算,实时准确
  • 删除标签关联时不自动减(当前平台无删除操作入口)

5.2 手动全量刷新

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 生产环境执行

-- 停用零文章 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 节点识别

-- 判断某节点是否为 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 TTLkey="public:tags"
GET /public/cancer-tags → 仅癌种标签树

返回字段:

{
  "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 首页热门标签

<!-- HomeView.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 名称显示

<!-- 所有标签组件的统一规则 -->
{{ tag.name_zh || tag.name_en }}

如果 name_zh 为空(auto 标签常见),退而显示 name_en。此规则在以下组件统一使用:

9.2 文献卡片标签加载

# 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 暗色模式

// 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 标签导入

# 种子标签导入(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 一次性数据清洗

-- 查看标签分布
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 生产部署注意事项

# 部署前确保标签变更已提交
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 审计标签分布 生产