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.
20 KiB
20 KiB
肿瘤科标签体系设计
设计原则
- 基于 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。主要用于:
- 保证打标覆盖(即使没有 manual/mesh 标签匹配)
- 后台可见,供管理员审核后提升为 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 精确匹配
# 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 回退匹配
# 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 TTL,key="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。此规则在以下组件统一使用:
- LiteratureCard.vue
- LiteratureDetailView.vue(登录态)
- LiteratureDetailView.vue(公开态)
- CancerBrowseView.vue
- InterestSettingsView.vue
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 |
审计标签分布 | 生产 |