Files
backend/docs/05-标签体系设计.md
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

618 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 肿瘤科标签体系设计
## 设计原则
- 基于 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 精确匹配
```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 阶段 2name_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 TTLkey="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
<!-- 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 名称显示
```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` | 审计标签分布 | 生产 |