chore: batch commit remaining changes
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

Includes search engine improvements, Alembic migrations,
new services (pubmed_daily_update, query_expansion),
frontend updates, and documentation sync.
This commit is contained in:
34047007@qq.com
2026-07-27 08:35:12 +08:00
parent 35b0a5c565
commit 62ca8fa6b8
82 changed files with 314248 additions and 1519 deletions
+48 -11
View File
@@ -2,7 +2,7 @@
## 日期时间类型规范
**全部使用 `TIMESTAMPTZ`**固定日期字段(出生日期、发表日期、批准日期、排期日期)用 `DATE`
**TIMESTAMPTZ 为主**日期字段(出生日期、发表日期、批准日期、排期日期、PubMed 纯日期字段)用 `DATE`
```sql
-- ✅ 正确
@@ -152,6 +152,7 @@ CREATE TABLE global_literature (
issue VARCHAR(50), -- 期号 【PubMed: Article/Journal/JournalIssue/Issue】
pages VARCHAR(100), -- 页码 【PubMed: Article/Pagination/MedlinePgn】
pub_date DATE, -- 纸质出版日期(期刊卷期日期,若纯电子刊则为电子日期)【PubMed: Article/Journal/JournalIssue/PubDate 组合 Year+Month+Day】
print_date DATE, -- 纯纸质出版见刊日期(仅纸质版有值,从 PubDate 解析并经 history ppdat 校验)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="ppdat"]】
pub_year INTEGER, -- 出版年份 【PubMed: 从 PubDate/Year 提取】
pub_types JSONB NOT NULL DEFAULT '[]', -- 出版类型列表 【PubMed: Article/PublicationTypeList/PublicationType】
mesh_headings JSONB NOT NULL DEFAULT '[]', -- MeSH 标引词列表(含 DescriptorName+QualifierName)【PubMed: MeshHeadingList/MeshHeading】
@@ -160,7 +161,7 @@ CREATE TABLE global_literature (
pmc_id VARCHAR(20), -- PMC IDPMC1234567)【PubMed: ArticleId[@IdType="pmc"]】
is_oa BOOLEAN NOT NULL DEFAULT FALSE, -- 是否开放获取【根据 pmc_id 或 PMC OA API 判定,非 PubMed 原始字段】
keywords JSONB NOT NULL DEFAULT '[]', -- 作者关键词 【PubMed: Article/KeywordList/Keyword】
pubmed_revised TIMESTAMPTZ, -- PubMed 数据库修订日期(NLM 说明不可靠)【PubMed: MedlineCitation/DateRevised】
pubmed_revised DATE, -- PubMed 数据库修订日期(NLM 说明不可靠)【PubMed: MedlineCitation/DateRevised】
grants JSONB NOT NULL DEFAULT '[]', -- 基金资助信息 【PubMed: Article/GrantList/Grant】
cited_by_count INTEGER NOT NULL DEFAULT 0, -- 被引次数 【来自 NCBI elink 接口 pubmed_pubmed_citedin,非 PMID 固定字段】
full_text_sections JSONB, -- PMC OA 全文结构化段落【来自 PMC OA Service API JATS XML,非 PubMed EFetch】
@@ -169,7 +170,8 @@ CREATE TABLE global_literature (
study_design JSONB, -- 研究设计分类 {primary: "RCT", sub: "Phase 3", design: "parallel"}AI 推断,非 PubMed 原始字段)
pico JSONB, -- PICO 结构化抽取(AI 推断,非 PubMed 原始字段)
citation_status VARCHAR(20), -- MedlineCitation 状态 【PubMed: MedlineCitation/@Status 属性,如 "MEDLINE"/"PubMed-not-MEDLINE"/"In-Data-Review"】
date_completed TIMESTAMPTZ, -- MeSH 标引完成日期 【PubMed: MedlineCitation/DateCompleted】
date_completed DATE, -- NLM 整条编目记录完成日(含 MeSH 标引在内的全部处理完成)
meshed_date DATE, -- MeSH 数据版本日期。通常=date_completed(MeSH 是编目最后一步,两者同天)。年更刷新后=新的 date_completed。NULL=无MeSH(in-process/publisher)。NOT datetime.now()
retracted BOOLEAN NOT NULL DEFAULT FALSE, -- 是否已撤稿【依据 PubMed: Article/PublicationTypeList/PublicationType[.="Retracted Publication"]】
retraction_details JSONB, -- 撤稿详情(关联撤稿文献等)【PubMed: CommentsCorrectionsList/CommentsCorrections[@RefType="retractionof"/"retractionin"]】
license_info JSONB, -- 许可证信息(来自 PMC OA XML 或出版商元数据,非 PubMed 原始字段)
@@ -180,10 +182,15 @@ CREATE TABLE global_literature (
negative_result_details JSONB, -- 阴性结果详情(AI 抽取,非 PubMed 原始字段)
ai_summary JSONB, -- AI 摘要(内部,非 PubMed 字段)
chemical_list JSONB NOT NULL DEFAULT '[]', -- 化学物质列表 【PubMed: ChemicalList/Chemical】
investigators JSONB NOT NULL DEFAULT '[]', -- 研究者列表 [{family,given,affiliation,identifiers}] 【PubMed: InvestigatorList/Investigator】
personal_name_subjects JSONB NOT NULL DEFAULT '[]', -- 作为主题的人名 [{family,given}] 【PubMed: PersonalNameSubjectList/PersonalNameSubject】
publication_notes JSONB NOT NULL DEFAULT '[]', -- 出版注释列表 ["note1","note2"] 【PubMed: PubmedData/PublicationNote】
gene_symbols JSONB NOT NULL DEFAULT '[]', -- 基因符号列表 【PubMed: SupplMeshList/SupplMeshName;部分来自 Gene Symbol 或 ChemicalList】
num_refs INTEGER, -- 参考文献数量(来自 PMC 全文或解析,非 PubMed EFetch 原始字段)
publication_status VARCHAR(30), -- 出版状态 【PubMed: PubmedData/PublicationStatus,取值 epublish/ppublish/aheadofprint】
article_date DATE, -- 电子出版日期(先于印刷版)【PubMed: Article/ArticleDate】
create_date DATE, -- PubMed 记录创建日期(纯日期,无需时间)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="pubmed"] / MedlineCitation/DateCreated】
entrez_date DATE, -- PubMed 收录日期(纯日期,无需时间)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="entrez"]】
databank_list JSONB NOT NULL DEFAULT '[]', -- 数据库引用列表(如 ClinicalTrials.gov)【PubMed: Article/DataBankList/DataBank】
suppl_mesh_list JSONB NOT NULL DEFAULT '[]', -- 补充 MeSH 词列表(含物质名)【PubMed: SupplMeshList/SupplMeshName】
source VARCHAR(30) NOT NULL DEFAULT 'pubmed_ftp', -- 数据来源(内部追踪用:pubmed_api/pubmed_ftp,非 PubMed 字段)
@@ -197,16 +204,30 @@ CREATE INDEX idx_gl_journal_iso ON global_literature(journal_iso);
CREATE INDEX idx_gl_created ON global_literature(created_at);
CREATE INDEX idx_gl_pmc_id ON global_literature(pmc_id);
CREATE INDEX idx_gl_search_tsv ON global_literature USING gin(search_tsv);
CREATE INDEX ix_gl_doi ON global_literature(doi);
CREATE INDEX ix_gl_language ON global_literature(language);
CREATE INDEX ix_gl_retracted ON global_literature(retracted);
CREATE INDEX ix_gl_pub_types_gin ON global_literature USING gin(pub_types);
CREATE INDEX ix_gl_study_design_gin ON global_literature USING gin(study_design);
CREATE INDEX ix_gl_grants_gin ON global_literature USING gin(grants);
CREATE INDEX ix_gl_chemical_list_gin ON global_literature USING gin(chemical_list);
CREATE INDEX ix_gl_citation_status ON global_literature(citation_status); -- 筛选:MEDLINE only
CREATE INDEX ix_gl_is_oa ON global_literature(is_oa); -- 筛选:Free full text
CREATE INDEX ix_gl_is_negative ON global_literature(is_negative_result); -- 筛选:阴性结果
CREATE INDEX ix_gl_is_preprint ON global_literature(is_preprint); -- 筛选:Exclude preprints
CREATE INDEX ix_gl_mesh_headings ON global_literature USING gin(mesh_headings); -- 筛选:Species/Sex/Age JSONB @> 查询
CREATE INDEX ix_global_literature_journal_iso_trgm ON global_literature USING gin(journal_iso gin_trgm_ops); -- PubMed [TA] ILIKE 兜底
```
> 预估值:约 600 万条(PubMed 中肿瘤相关的历史累积),单表不需要分区
**全文搜索:**
> `search_tsv``TSVECTOR` 类型,GIN 索引 `ix_gl_search_tsv`)由 PG 触发器自动维护,包含 **title + abstract + authors.family + authors.affiliation**。所有搜索(普通搜索 `field="all"` 和高级搜索字段选择中的作者/机构)均走 tsvector `@@` `plainto_tsquery()`ILIKE 仅作为兜底(NULL tsvector 记录)。触发器 `trg_global_literature_tsv` 在 INSERT 或 UPDATE title/abstract 时触发重建。千万级无压力。
> `search_tsv``TSVECTOR` 类型,GIN 索引 `ix_gl_search_tsv`)由 PG 触发器 `trg_global_literature_tsv` 自动维护。使用 `setweight()` 区分字段权重:**title=A 权重、abstract=B 权重、authors.family=A 权重**。所有搜索(普通搜索 `field="all"` 和高级搜索字段选择中的作者/机构)均走 tsvector `@@` `plainto_tsquery()`ILIKE 仅作为兜底(NULL tsvector 记录)。触发器在 INSERT 或 UPDATE title/abstract/authors 时触发重建。千万级无压力。
>
> > **GIN 索引重建说明:** 迁移 `f80dca5baa02`add_pico_column_to_review_literatures)的 `upgrade()` 误将 `ix_gl_search_tsv` 删除后未重建,导致所有 tsvector 搜索走全表扫描。迁移 `0314f4d28728`rebuild_search_tsv_gin_index)已修复,紧跟在 `e341edea85e2` 之后。
**日期字段说明:** `pub_date`印刷/电子出版日期)+ `article_date`(电子出版日期,实际上线时间)+ `date_completed`(MeSH 标引完成日,检索质量控制用)+ `pubmed_revised`NLM 定义为不可靠)+ `publication_status`epublish / ppublish / aheadofprint 出版阶段标识)。
**日期字段说明:** `pub_date`[DP] 出版日期 = LEAST(EPDAT, PPDAT))由 `article_date`[EPDAT] 电子出版日期)+ `print_date`([PPDAT] 纸质出版日期)的最小值计算得出。`create_date`[CRDT] PubMed 记录创建日期)+ `entrez_date`[EDAT] PubMed 入库日期)+ `date_completed`(NLM 整条编目记录完成日,含 MeSH 在内的全部处理完成,实践中作为 [MHDA] 近似值)。`meshed_date` 是系统内部字段,记录 MeSH 数据的版本日期:通常=`date_completed`(MeSH 是编目最后一步,两者同天);年更刷新后设为新的 `date_completed`。NULL=无 MeSH。`pubmed_revised`NLM 定义为不可靠)`publication_status`epublish / ppublish / aheadofprint 出版阶段标识)。
> **注意:** `create_date`、`entrez_date`、`date_completed`、`meshed_date`、`pubmed_revised` 五个字段原为 TIMESTAMPTZ,现统一为 DATE 类型。原因是 NLM 所有日期来源(DateCompleted、History/PubMedPubDate、DateRevised、ArticleDate、DateCreated)仅提供 Y/M/D,无时间分量。DATE 类型更精确地反映语义,减少存储开销。
**PubMed 对应 XML 结构速查:**
@@ -223,24 +244,35 @@ CREATE INDEX idx_gl_search_tsv ON global_literature USING gin(search_tsv);
| `volume` | `Article/Journal/JournalIssue/Volume` | 卷 |
| `issue` | `Article/Journal/JournalIssue/Issue` | 期 |
| `pages` | `Article/Pagination/MedlinePgn` | 页码范围 |
| `pub_date` | `Article/Journal/JournalIssue/PubDate` | 仅取 Year/Month/Day 组合 |
| `pub_date` | `Article/Journal/JournalIssue/PubDate` 取 Year/Month/Day 组合,与 print_date/articledate LEAST 计算 | [DP] 出版日期 = COALESCE(LEAST(EPDAT, PPDAT), 任一可用值) |
| `print_date` | `PubmedData/History/PubMedPubDate[@PubStatus="ppdat"]``Journal/JournalIssue/PubDate` | [PPDAT] 纸质出版日期 |
| `pub_year` | `PubDate/Year` | 单独提取年份 |
| `pub_types` | `Article/PublicationTypeList/PublicationType` | 可多个,含 "Retracted Publication" |
| `mesh_headings` | `MeshHeadingList/MeshHeading` | DescriptorName(UI) + QualifierName(UI) |
| `language` | `Article/Language` | 2-3 字母 ISO 语言码 |
| `pmc_id` | `ArticleId[@IdType="pmc"]` | 带 "PMC" 前缀 |
| `keywords` | `Article/KeywordList/Keyword` | 作者关键词(非 MeSH |
| `pubmed_revised` | `MedlineCitation/DateRevised` | 不可靠,不应视为修订依据 |
| `pubmed_revised` | `MedlineCitation/DateRevised` | 不可靠,不应视为修订依据。仅 Year/Month/DayDATE 类型 |
| `grants` | `Article/GrantList/Grant` | GrantID + Agency + Country |
| `chemical_list` | `ChemicalList/Chemical` | RegistryNumber + NameOfSubstance(UI) |
| `suppl_mesh_list` | `SupplMeshList/SupplMeshName` | 补充概念词 |
| `databank_list` | `Article/DataBankList/DataBank` | DataBankName + AccessionNumberList |
| `trial_reg` | `Article/DataBankList/DataBank` | 从中提取 NCT/EudraCT/ChiCTR |
| `citation_status` | `MedlineCitation/@Status` | "MEDLINE","PubMed-not-MEDLINE","In-Data-Review","PubMed" |
| `date_completed` | `MedlineCitation/DateCompleted` | MeSH 标引完成日 |
| `retraction_details` | `CommentsCorrectionsList/CommentsCorrections[@RefType="retractionof" or "retractionin"]` | 撤稿关联关系 |
| `date_completed` | `MedlineCitation/DateCompleted` | NLM 整条编目记录完成日(含 MeSH 标引在内的全部处理完成)。实践中作为 [MHDA] 近似值使用。仅 Year/Month/DayDATE 类型 |
| `meshed_date` | 系统内部字段 | MeSH 数据版本日期。通常=date_completed(MeSH 是编目最后一步,两者同天)。年更后=新的 date_completed。NULL=无MeSH。DATE 类型 |
| `publication_status` | `PubmedData/PublicationStatus` | "epublish" / "ppublish" / "aheadofprint" |
| `article_date` | `Article/ArticleDate` | 电子出版日期(印刷版之前的日期)|
| `article_date` | `Article/ArticleDate` | [EPDAT] 电子出版日期(印刷版之前的日期)|
| `create_date` | `PubmedData/History/PubMedPubDate[@PubStatus="pubmed"]``MedlineCitation/DateCreated` | [CRDT] PubMed 记录创建日期。仅 Year/Month/DayDATE 类型 |
| `entrez_date` | `PubmedData/History/PubMedPubDate[@PubStatus="entrez"]` | [EDAT] PubMed 收录日期。仅 Year/Month/DayDATE 类型 |
| `pharmacological_actions` | `MedlineCitation/ChemicalList/PharmacologicalAction/NameOfSubstance` | [PA] 药理作用 [{name, ui}] |
| `cois_statement` | `Article/CoiStatement` | [COIS] 利益冲突声明 |
| `vernacular_title` | `Article/VernacularTitle` | [TT] 非英文标题的拉丁转写 |
| `is_preprint` | DOI prefix + journal name 检测 | 是否为预印本(非 PubMed 原始字段) |
| `auid_data` | `Author/Identifier[Source="ORCID"]` | [AUID] 作者标识符 [{type, value, author_index}] |
| `investigators` | `InvestigatorList/Investigator` | [IR] 多中心试验研究者 [{family, given, affiliation, identifiers}] |
| `personal_name_subjects` | `PersonalNameSubjectList/PersonalNameSubject` | [PS] 作为主题的人名 [{family, given}] |
| `publication_notes` | `PubmedData/PublicationNote` | [PUBN] 出版注释列表 |
### 2.2 global_tags — 肿瘤科标签树
@@ -254,6 +286,9 @@ CREATE TABLE global_tags (
level INTEGER NOT NULL DEFAULT 1, -- 层级深度
parent_id UUID REFERENCES global_tags(id) ON DELETE CASCADE, -- 父标签ID
tree_number VARCHAR(100), -- MeSH 树编号
entry_terms JSONB, -- MeSH 入口词列表(~200kNLM desc2025.asc 导入,用于 ATM 自动术语映射)
source VARCHAR(20) DEFAULT 'auto', -- 数据来源:manual(手工)/mesh(C04批量)/auto(管道懒创建)
is_active BOOLEAN DEFAULT FALSE, -- 是否已确认(True=用户可见,False=仅后台可见)
tag_category VARCHAR(30) NOT NULL DEFAULT 'cancer', -- 标签分类
sort_order INTEGER DEFAULT 0, -- 排序序号
icon VARCHAR(50), -- 图标名
@@ -713,7 +748,7 @@ CREATE TABLE ai_provider_config (
```sql
CREATE TABLE pipeline_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 运行ID
run_type VARCHAR(20) NOT NULL, -- 运行类型:daily/weekly/manual
run_type VARCHAR(20) NOT NULL, -- 运行类型:daily_ftp_update / baseline_import / daily_citation_update / manual_full / etc.
status VARCHAR(20) NOT NULL DEFAULT 'running', -- 状态:running/completed/failed
files_processed INTEGER DEFAULT 0, -- 处理文件数
articles_total INTEGER DEFAULT 0, -- 总文献数
@@ -724,6 +759,8 @@ CREATE TABLE pipeline_runs (
users_matched INTEGER DEFAULT 0, -- 匹配用户数
feeds_generated INTEGER DEFAULT 0, -- 生成 Feed 数
error_log TEXT, -- 错误日志
processed_date DATE, -- FTP 增量检查点:已处理的 EDAT 日期(2026-07-25 新增)
metadata JSON, -- 扩展元数据(基线年份、文件序号范围等)(2026-07-25 新增)
started_at TIMESTAMPTZ, -- 开始时间
completed_at TIMESTAMPTZ, -- 完成时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
+285 -85
View File
@@ -8,37 +8,110 @@
## 一、PubMed 数据获取
### 1.1 实际运行模式API 优先
### 1.1 运行模式(决策:2026-07-25
当前部署使用 **NCBI E-utilities API****Europe PMC API** 作为数据源(`app/services/pubmed_api.py`),而非 FTP baseline
数据源统一为 **FTP 每日更新文件**`ftp://ftp.ncbi.nlm.nih.gov/pubmed/updatefiles/pubmed25updateNNN.xml.gz`),取代原有的 E-utilities API 多路搜索策略
| 数据源 | 协议 | 限速 | 用途 |
| 数据源 | 协议 | 内容 | 用途 |
|--------|------|------|------|
| NCBI E-utilities | REST XML | 3 req/s(免费)/ 10 req/s(有 API Key | 每日精搜(MAJR |
| Europe PMC | REST JSON | 无严格限速,支持游标分页 | MAJR 精搜 + 标题/摘要宽搜 |
| **FTP 每日更新文件** | FTP (gzip XML) | `<PubmedArticle>`(新增+修改)+ `<DeleteCitation>`(删除) | **唯一每日数据源** |
| NCBI E-utilities | REST XML | 搜索 + 抓取 | **仅降级后备**FTP 不可用时) |
| Europe PMC | REST JSON | 搜索 + 抓取 | 仅基线导入后一次性回填(`rebuild_from_baseline.py` |
```
精搜(每日 03:07 UTC):
ONCOLOGY_SEARCH_QUERIES × [MAJR] → NCBI esearch → efetch
约 20 篇/query × 10 queries = 200 篇/天
宽搜(每周日 03:37 UTC):
BROAD_ONCOLOGY_QUERIES × Title/Abstract → Europe PMC 游标分页
→ 覆盖 in-process + publisher 记录
→ 与精搜通过 seen_pmids 自动去重
每日更新(03:07 UTC):
FTP 下载 pubmed25updateNNN.xml.gz → lxml 解析
_extract_article() 提取字段(复用 baseline 解析器)
→ _is_oncology() 肿瘤科过滤(复用 baseline 过滤逻辑)
→ _update_lit_from_article() upsert(幂等,同 PMID 覆盖)
→ <DeleteCitation> → 标记 retracted=True
```
### 1.2 暂未使用 FTP
### 放弃 E-utilities 搜索的原因
FTP baseline 导入(`scripts/pubmed_baseline.py`)已完成开发但尚未在生产中执行,原因:
| 维度 | 旧: E-utilities API 多路搜索 | 新: FTP 每日更新文件 |
|------|------------------------------|---------------------|
| 完整性 | 仅返回匹配搜索词的结果。宽搜词覆盖不到的文章→**永恒漏掉** | **100%** — 当天所有增/改/删的记录都在一个文件里 |
| 删除检测 | 搜不到已删的记录 → 无法标记删除 | `<DeleteCitation>` 明确列出被删 PMID |
| MeSH 过渡 | 需 retagger 回查 in-process→medline 变化 | 更新文件直接包含 MeSH 状态变化后的完整记录 |
| 限速 | 3 req/s(免费) → 大结果集分页慢 | 单文件下载,无 API 限速 |
| 代码复杂度 | 2 套搜索(MAJR + 宽搜)+ 3 条解析路径 + retagger | 1 套下载 + 1 套解析(复用 baseline |
| 定时任务 | 精搜 + 宽搜 + retagger = 3 个 cron | 1 个 cron |
| 维度 | API | FTP |
|------|-----|-----|
| 初始数据量 | ~200 篇/天自动积累 | 一次导入 ~30GB |
| 部署复杂度 | 零额外配置 | 需下载 1200+ 个 XML 文件 |
| 推荐时机 | 每日自动运行 | 仅在需要全量历史文献时手动触发 |
FTP baseline 导入(`scripts/pubmed_baseline.py`)用于**首次全量数据**,完成后每日增量数据完全由 FTP update files 接管。
### 1.3 配置文件驱动过滤
### 1.2 FTP 每日更新文件结构
文件名格式:`pubmed25updateNNNN.xml.gz`(NNNN 为 4 位顺序号,每年重置)
文件包含三种顶层元素:
- `<PubmedArticle>` — **新增**或**已修改**的文献记录
- `<PubmedBookArticle>` — 图书章节(跳过,非期刊文献)
- `<DeleteCitation>` — 被删除的 PMID(含 PubMed/PMC 两种来源)
NLM 每天发布一个新文件(约 5-20MB 压缩后),全年约 365 个文件。
### 1.3 基线过渡(Baseline → FTP 衔接)
基线导入和 FTP 每日更新之间存在**年份衔接问题**:
| 问题 | 说明 | 处理方式 |
|------|------|---------|
| 基线年份 | 基线文件 `pubmed26n*.xml.gz` 中的 "26" 表示 2026 年 | **自动检测**`rebuild_from_baseline.py` 导入完成后自动记录 `pipeline_runs(run_type='baseline_import', run_metadata={'year': 2026})` |
| FTP 文件前缀 | 2026 年的增量文件为 `pubmed26updateNNNN.xml.gz` | 从基线记录读取年份,构造前缀 `pubmed{year}update` |
| 首次运行 | 基线刚导入完,FTP 文件可能从年初就开始有了 | 按 `sequence > 0` 拉取所有文件(与基线无交叠问题) |
| 跨年份过渡 | 12 月→1 月,文件前缀从 `pubmed25update``pubmed26update` | 前一年没文件时自动尝试上年;从 `pipeline_runs``run_metadata.last_sequence` 判断跨年 |
具体流程:
```
首次运行(无 checkpoint):
1. pipeline_runs 查 baseline_import 记录 → year=2026
2. 构造前缀 pubmed26update → 列出 FTP 文件
3. 全部下载处理(sequence 从 0001 开始)
4. 记录 run_metadata = {ftp_year: 2026, last_sequence: 最大序号}
后续运行(有 checkpoint):
1. 读上次 run_metadata → ftp_year=2026, last_sequence=1234
2. 列出 pubmed26update*,筛选 sequence > 1234
3. 处理 → 更新 run_metadata.last_sequence
```
### 1.4 检查点(Checkpoint)机制
使用 `pipeline_runs` 表的多字段组合追踪已处理状态:
| 字段 | 用途 |
|------|------|
| `run_type` = `"daily_ftp_update"` | 标记 FTP 更新运行 |
| `processed_date` (DATE) | 最后一个已处理的 EDAT 日期 |
| `run_metadata.ftp_year` (JSON) | FTP 文件年份(决定文件前缀) |
| `run_metadata.last_sequence` (JSON) | 最后一个已处理的文件序号 |
| `articles_new` | 新增文献数 |
| `articles_updated` | 更新文献数 |
| `articles_deleted` | DeleteCitation 处理数 |
**检查点行为:**
- 每天运行时,读 `pipeline_runs WHERE run_type='daily_ftp_update' ORDER BY created_at DESC LIMIT 1`
- 从上次 `last_sequence` 的序号+1开始拉取
- 如果当天无新 update 文件发布,跳过(非错误)
- 运行失败 → 不写 PipelineRun → 下次重跑同序列号(幂等)
- FTP 文件含多天的累积更新 → 按 `_update_lit_from_article()` 覆盖不会重复
### 1.5 EDAT 作为判断依据
EDATEntrez Date)是 PubMed 记录首次进入 Entrez 系统的日期,是判断"新记录"的最可靠依据:
| 特性 | EDAT |
|------|------|
| 是否可变 | **不可变** — 记录一旦入库,EDAT 终生不变 |
| 精度 | 精确到日(Y/M/D),无时间分量 → DATE 类型完全匹配 |
| 唯一性 | 同一天两条记录可能有不同 EDAT?不可能 — 所有新记录取当天日期 |
| 删除检测 | 不依赖 EDAT — `<DeleteCitation>` 独立处理 |
| 是否适用于 FTP | ✅ FTP 文件中可提取每条记录的 History/PubMedPubDate[@PubStatus="entrez"] |
EDAT 对应字段:`GlobalLiterature.entrez_date`DATE,新增字段,迁移:`1421ea169bb6`)。
### 1.5 配置文件驱动过滤
```yaml
# config/specialties/oncology.yaml
@@ -67,63 +140,61 @@ pubmed_filter:
## 二、Pipeline 完整流程
```
ARQ 定时任务 (每日精搜 03:07 UTC / 每周宽搜 周日 03:37 UTC)
ARQ 定时任务 (每日 FTP 更新 03:07 UTC)
┌─────────────────────────────────────────────────────────┐
│ Step 1: API 搜索
NCBI E-utilities (esearch + efetch) — MAJR 精搜
Europe PMC REST API — 标题/摘要宽搜
精搜 ~200 篇/天,宽搜 ~2000 篇/周日
自动去重 (seen_pmids 集合)
│ 预计耗时:1-2 分钟 │
│ Step 1: FTP 文件下载 + 检查点检测
从 pipeline_runs 读上次处理的 processed_date
下载 pubmed25updateNNNN.xml.gz(序号 > 上次处理的)
失败 → 降级到 E-utilities reldate=1&datetype=edat 后备
预计耗时:10-30 秒(gzip ~10MB 解压后 ~200MB
└───────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 2: XML/JSON 解析 │
│ Step 2: XML 解析
│ │
│ 对每条 PubMedArticle 提取
│ 对每条 <PubmedArticle> 提取(复用 _extract_article()):
│ - PMID (PubMed ID) │
│ - Title, Abstract │
│ - Authors [{given, family, affiliation}] │
│ - DOI, Journal (name, ISSN, ISOAbbreviation, │
│ volume, issue, pages) │
│ - PublicationDate (year, month, day) │
│ - History (entrez_date, create_date, pubmed_revised,│
│ date_completed) │
│ - PublicationType (期刊文章/综述/RCT/指南...) │
│ - MeSH Headings [{descriptor, ui, major}] │
│ - Language │
│ - ChemicalList [{registry_number, ui, name}] │ ← 增强字段
│ - GeneSymbolList [string] │ ← 增强字段
│ - NumberOfReferences (int) │ ← 增强字段
│ - PublicationStatus (epublish/ppublish/aheadofprint)│ ← 增强字段
│ - ArticleDate (电子出版日期, DateType="Electronic") │ ← 增强字段
│ - DataBankList [{name, accession_numbers[]}] │ ← 增强字段
│ - SupplMeshList [{ui, name, type}] │ ← 增强字段
│ - ChemicalList [{registry_number, ui, name}] │
│ - GeneSymbolList [string] │
│ - NumberOfReferences (int) │
│ - PublicationStatus (epublish/ppublish/aheadofprint)│
│ - ArticleDate (电子出版日期, DateType="Electronic") │
│ - DataBankList [{name, accession_numbers[]}] │
│ - SupplMeshList [{ui, name, type}] │
│ │
增强字段覆盖率(实测 1369 篇):
publication_status ~100% article_date ~73%
│ chemical_list ~37% num_refs ~91% │
│ gene_symbols ~10% databank_list ~25% │
│ suppl_mesh_list ~35% │
对每条 <DeleteCitation>
查本地 PMID → 标记 retracted=True + updated_at
│ │
特殊处理:DeleteCitation → 标记删除不处理
│ 预计耗时:1-2 分钟 (2000-4000条) │
预计耗时:30-60 秒(单个文件 ~500-5000 条记录)
│ │
三处解析器(需保持同步):
│ - _parse_pubmed_xml() — NCBI E-utilities, ElementTree
│ - _extract_article() — FTP baseline, lxml
- _parse_europe_pmc_article() — Europe PMC, JSON
单一解析器(统一维护):
│ - _extract_article() — FTP, lxml (pubmed_baseline.py)
│ - 注:pubmed_api.py 中的_parse_pubmed_xml()
是 E-utilities 降级路径,需与 _extract_article()
│ 保持同步 │
│ │
两条入库路径(均需传递增强字段):
│ - _process_article() 创建新 GlobalLiterature()
│ - _update_lit_from_article() — 更新已有记录
│ 入库路径(已存在):
│ - _update_lit_from_article() — upsert 更新已有记录
│ - 新 PMID 通过 GlobalLiterature() 构造函数创建
│ │
└───────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 3: 肿瘤科过滤(配置驱动
│ Step 3: 肿瘤科过滤(配置驱动,复用 _is_oncology()
│ │
│ 对每篇文章的 MeSH Headings[] 逐一检查: │
│ if 任一 heading.ui 在 mesh_include_categories 中 │
@@ -133,7 +204,7 @@ ARQ 定时任务 (每日精搜 03:07 UTC / 每周宽搜 周日 03:37 UTC)
│ else │
│ → 丢弃 │
│ │
│ 过滤结果:日均 3000 条 → 约 400-600 条肿瘤相关
│ 过滤结果:日均 ~5000 条总记录 → 约 400-800 条肿瘤相关 │
│ 预计耗时:1-2 分钟 │
└───────────────────┬─────────────────────────────────────┘
@@ -163,7 +234,11 @@ ARQ 定时任务 (每日精搜 03:07 UTC / 每周宽搜 周日 03:37 UTC)
│ INSERT INTO global_literature ... ON CONFLICT (pmid) │
│ DO UPDATE (如果已存在则更新) │
│ INSERT INTO global_literature_tags (literature_id, │
│ tag_id, is_major)
│ tag_id, is_major) ON CONFLICT DO UPDATE
│ │
│ DeleteCitation 处理: │
│ UPDATE global_literature SET retracted=TRUE, │
│ updated_at=NOW() WHERE pmid IN (...) │
│ │
│ 预计耗时:< 1 分钟(批量 INSERT
└───────────────────┬─────────────────────────────────────┘
@@ -220,6 +295,15 @@ ARQ 定时任务 (每日精搜 03:07 UTC / 每周宽搜 周日 03:37 UTC)
│ - 领域匹配 → related │
│ │
│ 预计耗时:< 1 分钟(千级用户,百级文献/用户) │
└───────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Step 7: 更新检查点 │
│ INSERT INTO pipeline_runs (run_type='daily_ftp_update',│
│ processed_date=今天, status='success', │
│ articles_new=..., articles_updated=..., │
│ articles_deleted=...) │
└─────────────────────────────────────────────────────────┘
总计耗时:每日常规 5-10 分钟
@@ -231,11 +315,12 @@ ARQ 定时任务 (每日精搜 03:07 UTC / 每周宽搜 周日 03:37 UTC)
| 环节 | 数据量 | 预计耗时 |
|------|--------|:---:|
| API 搜索 + 抓取 | 200-2000 篇/次 | 1-2 分钟 |
| XML/JSON 解析 | 2000-4000 篇/天 | 1-2 分钟 |
| FTP 文件下载 + 解压 | 1 个文件,~10MB gzip | 10-30 秒 |
| XML 解析(含 DeleteCitation | 单文件 ~500-5000 条 | 30-60 秒 |
| MeSH 过滤 | — | 1-2 分钟 |
| 打标 | 每篇 5-15 个 MeSH | 1-2 分钟 |
| 入库 + 引用查询 | 400-600 INSERT/UPDATE + elink | 1-2 分钟 |
| 入库 + 引用查询 | 400-800 ON CONFLICT + elink | 1-2 分钟 |
| DeleteCitation 更新 | 通常 < 10 条/天 | < 1 秒 |
| OA 全文抓取 | ~15% 有 PMC | 每篇 1-3 秒 |
| 期刊去重同步 | 每篇 | < 1 秒 |
| 用户匹配 | 千级用户 | < 1 分钟 |
@@ -253,10 +338,9 @@ from arq.connections import RedisSettings
class WorkerSettings:
functions = [
"app.tasks.pubmed_pipeline.daily_pubmed_pipeline",
"app.tasks.pubmed_pipeline.weekly_broad_pipeline",
"app.tasks.pubmed_pipeline.daily_citation_update",
"app.tasks.pubmed_pipeline.daily_digest_task",
"app.tasks.pubmed_pipeline.daily_ftp_update", # FTP 每日增量(取代精搜+宽搜+retagger)
"app.tasks.pubmed_pipeline.daily_citation_update", # 每日引用刷新
"app.tasks.pubmed_pipeline.daily_digest_task", # 每日摘要邮件推送
"app.tasks.pubmed_pipeline.refresh_hot_articles_cache",
"app.tasks.pubmed_pipeline.refresh_homepage_feed",
]
@@ -266,10 +350,8 @@ class WorkerSettings:
password=settings.REDIS_PASSWORD,
)
cron_jobs = [
# 每日精搜(MAJR MeSH 高精度,20 篇/query
cron(daily_pubmed_pipeline, minute=7, hour=3),
# 每周宽搜(Title/Abstract 覆盖 in-process + publisher
cron(weekly_broad_pipeline, minute=37, hour=3, day_of_week=0),
# 每日 FTP 增量更新(取代 daily_pubmed_pipeline + weekly_broad_pipeline
cron(daily_ftp_update, minute=7, hour=3),
# 每日引用刷新
cron(daily_citation_update, minute=13, hour=5),
# 每日摘要邮件推送
@@ -282,17 +364,19 @@ class WorkerSettings:
# app/tasks/pubmed_pipeline.py
async def daily_pubmed_pipeline(ctx):
"""每日 PubMed 精搜"""
# 通过 pubmed_api.py _run_pipeline(use_majr=True, use_broad=False) 执行
# 约 10 queries × 20 篇 = 200 篇/天
...
async def daily_ftp_update(ctx):
"""每日 FTP 增量更新(取代旧 daily_pubmed_pipeline + weekly_broad_pipeline
async def weekly_broad_pipeline(ctx):
"""每周宽搜"""
# 通过 pubmed_api.py _run_pipeline(use_majr=False, use_broad=True) 执行
# 覆盖 in-process + publisher 记录,自动 seen_pmids 去重
...
流程:
1. 查 pipeline_runs 获取上次处理的 EDAT 检查点
2. 下载 FTP pubmed25update* 文件(序号 > 上次处理的值)
3. 解析 <PubmedArticle> → _extract_article() → _is_oncology() 过滤
4. 解析 <DeleteCitation> → 标记 retracted=True
5. upsert → _update_lit_from_article() 或 GlobalLiterature() 新建
6. MeSH 映射 → tag_article() 打标
7. 生成 Feed → generate_feeds_for_literature()
8. 更新检查点 → INSERT pipeline_runs
"""
async def daily_citation_update(ctx):
"""刷新最近文献被引次数"""
@@ -306,16 +390,70 @@ async def daily_citation_update(ctx):
| 场景 | 处理 |
|------|------|
| FTP 连接超时 | 重试 3 次,间隔 60s,仍失败则告警 |
| XML 解析错误 | 跳过单条,记录错误 PMID |
| FTP 连接超时 | 重试 3 次,间隔 60s,仍失败则降级到 E-utilities reldate |
| FTP 文件破损(gzip 校验失败) | 跳过该文件,记录日志,下次重试 |
| XML 解析错误 | 跳过单条 record,记录错误 PMID |
| 入库冲突 | `ON CONFLICT (pmid) DO UPDATE`,幂等 |
| MeSH 映射找不到 | 记录 `unmapped_mesh_terms` 日志,定期人工审查 |
| 用户匹配超时 | 分批处理,每批 500 用户 |
| 整管道失败 | 告警通知 → 次日重试前一天数据 |
| 整管道失败 | `processed_date` 不更新 → 次日重跑同一天数据(幂等保证) |
---
## 六、数据质量监控
## 六、代码实现
### 6.1 文件清单
| 文件 | 状态 | 说明 |
|------|------|------|
| `app/services/pubmed_daily_update.py` | ✅ **新增** | 核心服务:FTP 下载、解析、过滤、upsert、删除标记、检查点 |
| `app/tasks/worker.py` | ✅ **已改** | cron 替换:`daily_ftp_update` 取代 `daily_pubmed_pipeline` + `weekly_broad_pipeline` + `daily_mesh_retagger` |
| `app/api/v1/admin.py` | ✅ **已改** | `POST /pipeline/run` 默认 mode 改为 `ftp` |
| `app/models/operations.py` | ✅ **已改** | `PipelineRun.processed_date` DATE + `run_metadata` JSON(检查点) |
| `alembic/versions/f3b135d62407_...` | ✅ **已迁移** | 新增 `pipeline_runs.processed_date` 列 |
| `alembic/versions/0ee585329fc6_...` | ✅ **已迁移** | 新增 `pipeline_runs.metadata` JSON 列 |
### 6.2 核心流程
```
run_daily_ftp_update()
├─ 1. get_last_checkpoint()
│ SELECT run_metadata FROM pipeline_runs
│ WHERE run_type='daily_ftp_update' AND status='success'
│ ORDER BY created_at DESC LIMIT 1
│ → {ftp_year, last_sequence} 或 None(首次运行)
├─ 2. 确定文件年份:checkpoint.ftp_year ?? baseline_year ?? 当前年
├─ 3. list_update_files(year, after_sequence)
│ FTP NLST → pubmed{year}updateNNNN.xml.gz 列表
│ 筛选 sequence > last_sequence
├─ 4. 逐个 download_file() + gzip.decompress()
├─ 5. process_update_file()
│ ├─ DeleteCitation PMIDs → UPDATE retracted=TRUE
│ ├─ _extract_article() 解析(复用 pubmed_baseline.py
│ ├─ _is_oncology() 过滤(复用 pubmed_baseline.py
│ ├─ upsert: 已存在 → _update_lit_from_article() + tag_article()
│ └─ 新建 → GlobalLiterature() + tag_article() + generate_feeds()
└─ 6. PipelineRun(processed_date=当天, run_metadata={ftp_year, last_sequence})
```
### 6.3 降级策略
FTP 不可用时(网络故障、NLM FTP 宕机),回退到 E-utilities `run_eutils_fallback()`
```python
async def run_eutils_fallback(last_checkpoint):
# 使用 reldate=1&datetype=edat 查询最近 1 天新增
# 调用 _run_pipeline(use_majr=True, use_broad=True)
```
---
## 七、数据质量监控
```sql
-- 查看每天的采集量(异常检测)
@@ -335,13 +473,13 @@ AND created_at > now() - interval '7 days';
---
## 、热搜缓存管道
## 、热搜缓存管道
### 7.1 概述
### 8.1 概述
首页侧栏"热搜"展示近期高被引文献(支持分页 + "换一批")。首页文献列表也使用预缓存策略。两者都避免每次请求走全套查询。
### 7.2 数据流
### 8.2 数据流
```
ARQ 定时任务(每 30 分钟):
@@ -359,7 +497,7 @@ API 请求:
未命中 → 回退实时查询 DB → 写回缓存 → 返回
```
### 7.3 关键文件
### 8.3 关键文件
| 文件 | 说明 |
|------|------|
@@ -368,7 +506,7 @@ API 请求:
| `app/tasks/worker.py` | `refresh_hot_articles_cache` — 定时任务(`*/30` 分钟 UTC |
| `app/core/cache.py` | `CacheService` — Redis 优先,不可用时降级为内存缓存 |
### 7.4 配置参数
### 8.4 配置参数
| 参数 | 值 | 说明 |
|------|-----|------|
@@ -377,8 +515,70 @@ API 请求:
| `page_size` | 5 | 每页展示数 |
| 最大页数 | 3(前端限制,后端支持更多) |
### 7.5 缓存失效说明
### 8.5 缓存失效说明
- 定时任务每 30 分钟覆盖写入,完全刷新缓存
- 缓存通过 `CACHE_TTL` 自动过期,Redis 重启后首次请求回退实时查询
- 实时查询回退时仅第 1 页写入缓存,避免分页碎片
---
## 九、MeSH 年更维护
### 9.1 背景
NLM 每年 11 月中–12 月中进行 MeSH 词汇年更(新词加入、旧词合并、树号变更)。期间 indexed 记录暂停灌入,只发 in-process/publisher 记录。年更过后,大量已有记录的 MeSH 标引会被刷新。
### 9.2 刷新判断
`meshed_date` 字段记录 **MeSH 数据的版本时间**(NLM 实际完成 MeSH 标引的时间),不是我们的刷新操作时间:
| 状态 | meshed_date | 含义 |
|------|-------------|------|
| 已标引 | = date_completed | 当前 MeSH 是最新的 |
| 年更后已刷 | = 新的 date_completed | 已通过年更刷新 |
| 待刷新 | < 年更新阈值 | MeSH 可能已过时 |
| 无 MeSH | NULL | in-process/publisher,无标引 |
> ⚠️ `meshed_date` 始终使用 NLM 端的时间戳(`date_completed`),不写 `datetime.now()`。仅在 NLM 未返回 `date_completed` 的极少数情况下降级使用当前时间。
### 9.3 年更期间的应对
FTP 每日更新文件在年更期间的行为:
- Indexed 记录**暂停**出现在更新文件中(NLM 暂停新标引)
- In-process 和 publisher 记录**照常**出现在更新文件中
- 年更结束后,重标引的记录**一次性在更新文件中出现**(当天的 update 文件会异常大)
这意味着 **FTP 方案天然适配年更** — 不需要特殊的管道逻辑,更新文件里有什么就处理什么。
### 9.4 刷新操作(手工执行)
> **⚠️ 此操作为手工任务**。不要自动执行。需先观察 PubMed 状态判断年更是否完成。
判断时机:
1. 观察 NLM 公告(https://www.nlm.nih.gov/pubs/techbull/
2. 检查 pipeline 日志:indexed 记录是否恢复灌入
3. 确认年更窗口结束(通常 12 月中)
执行命令(admin token 需要有 `platform_operator` 角色):
```bash
# 查询待刷新文献数
psql -U scilit_dev -d scilit_dev -c "
SELECT count(*) FROM global_literature
WHERE meshed_date IS NOT NULL
AND meshed_date < '2025-11-01'; # 去年年更前
# 全量刷新 MeSH(按 PMID 批次 re-fetch
curl -X POST localhost:8000/api/v1/admin/pipeline/run \
-H "Authorization: Bearer $(admin_token)"
```
> Pipeline 正常运行时 `_update_lit_from_article()` 会自动覆盖 `meshed_date = date_completed`。但对于年更场景,重抓后 MeSH 已变,NLM 返回的 date_completed 仍是原值,所以 pipeline 更新不会正确标记。正确的年更刷新机制为一个独立脚本(待实现):按 `meshed_date < 年更新阈值` + `mesh_headings IS NOT NULL` 条件批量 efetch → 对比 MeSH 是否有变 → 有变则更新并用 `now()` 覆盖 `meshed_date`。
### 9.5 待实现脚本(低优先级)
| 任务 | 文件 | 优先级 | 说明 |
|------|------|--------|------|
| MeSH 年更批量刷新脚本 | `scripts/refresh_mesh_annual.py` | P2 | 按 meshed_date 阈值批量 efetch,检测 MeSH 变化,更新 meshed_date=now() |
| — | `app/services/mesh_retagger.py` | ❌ **已废弃** | FTP 每日更新文件直接包含 in-process→medline 过渡的完整记录,retagger 不再需要 |
+16 -7
View File
@@ -160,15 +160,24 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
## 5. 结论
**总体判断**:当前数据可支撑基础的纯文本搜索(title/abstract/author),但以下搜索功能完全阻塞:
**硬性要求:搜索功能必须与 PubMed 完全一致。**
1. **MeSH 搜索**`tree_numbers` 空表 + INNER JOIN → 0 结果
2. **期刊缩写搜索**`journal_iso` 覆盖率 0%
3. **ATM 自动术语映射** — Entry Terms 缺失、SynonymExpander 未集成
4. **精确短语**`phraseto_tsquery` 未使用,`setweight` 未配置
5. **搜索排名**`best_match` 权重公式错误,导致 ts_rank 几乎不影响排序
这意味着:
1. **所有 PubMed 字段标签**必须有对应的搜索路径(数据已存储的立即接通,未存储的补充抽取)
2. **所有搜索行为**(MeSH 展开、ATM 映射、布尔运算、日期范围、精确短语等)必须与 PubMed 一致
3. **不允许任何字段标签退化到纯文本搜索**——`[IP]``[VI]``[PG]``[LA]``[GR]``[NM]``[SH]``[EDAT]``[CRDT]``[MHDA]``[LR]``[DCOM]``[LID]``[AUID]``[COIS]``[SI]` 等,凡数据已存储的必须接通,缺失数据的补充抽取
修复路径:先解决数据导入 Bugjournal_iso、keywords、PMC_ID+ 搜索 Bugtree_number、布尔逻辑),再逐步补充字段标签和功能。
**当前阻塞或缺失项**
| 问题 | 严重性 | 说明 |
|------|--------|------|
| MeSH 搜索 — tree_numbers 空表 + INNER JOIN | **阻塞** | 所有 `[MH]`/`[MAJR]` 查询返回 0 结果 |
| 期刊缩写搜索 — `journal_iso` 覆盖率 0% | **阻塞** | `[TA]` 缩写搜索不可用 |
| ATM 自动术语映射 — Entry Terms 缺失 | **缺失** | 普通用户输入"lung cancer"不会触发 MeSH 展开 |
| 精确短语 — `phraseto_tsquery` 未使用 | **缺失** | `"lung cancer"` 精确搜索低效 |
| 字段标签未接通(15+ 个) | **缺失** | 数据已存储但解析器未路由到搜索 |
| 作者 ORCID 数据 — `[AUID]` | **缺失** | PubMed XML 中有 `<Identifier>` 但未抽取 |
| 搜索排名 — `best_match` 权重公式错误 | **Bug** | ts_rank 几乎不影响排序 |
详细实施步骤见 [12-搜索功能实施计划.md](12-搜索功能实施计划.md)。
数据统计详情见 [13-文献数据质量报告.md](13-文献数据质量报告.md)。
+1
View File
@@ -1,6 +1,7 @@
# 搜索功能实施计划
> 计划日期:2026-07-24
> **硬性目标:搜索功能必须与 PubMed 完全一致。不允许"暂缓/可以忽略/不急"的降级。**
> 基于 9 Agent 审计 + 真实数据库 1,662 篇字段覆盖率验证
---
+209 -139
View File
@@ -1,9 +1,9 @@
# 文献数据质量报告
> 报告日期:2026-07-24
> 报告日期:2026-07-25
> 数据来源:本地 PostgreSQL 数据库(scilit_dev
> 总文献数:1,662 篇
> 数据来源分布:seed_data = 110, pubmed_api = 1,552
> 总文献数:56,432 篇
> 数据来源分布:PubMed 基线文件(pubmed26n0001~0015.xml.gz
---
@@ -13,80 +13,135 @@
| 字段 | 覆盖数 | 覆盖率 | 数据类型 | 状态 |
|------|--------|--------|---------|------|
| `pmid` | 1,662 | **100.0%** | INTEGER | ✅ 完整 |
| `title` | 1,652 | **99.4%** | TEXT | ✅ 完整 |
| `abstract` | 1,550 | **93.3%** | TEXT | ✅ 充分 |
| `doi` | 1,548 | **93.1%** | VARCHAR(255) | ✅ 充分 |
| `journal` | 1,662 | **100.0%** | VARCHAR(255) | ✅ 完整 |
| `journal_issn` | 1,662 | **100.0%** | VARCHAR(20) | ✅ 完整 |
| `journal_iso` | **0** | **0%** | VARCHAR(100) | **缺失** |
| `pub_year` | 1,662 | **100.0%** | INTEGER | ✅ 完整 |
| `pub_date` | 1,148 | **69.1%** | DATE | ⚠️ 部分 |
| `volume` | 1,292 | 77.7% | VARCHAR(20) | |
| `issue` | 1,247 | 75.0% | VARCHAR(20) | |
| `pages` | 1,117 | 67.2% | VARCHAR(50) | |
| `language` | 1,644 | **98.9%** | VARCHAR(10) | ✅ 充分 |
| `pmc_id` | **9** | **0.5%** | VARCHAR(20) | ❌ Bug |
| `is_oa` | 9 | 0.5% | BOOLEAN | ❌ Bug |
| `retracted` | 0 | 0% | BOOLEAN | ✅(全库无撤稿) |
| `pmid` | 56,432 | **100.0%** | INTEGER | ✅ 完整 |
| `title` | 56,432 | **100.0%** | TEXT | ✅ 完整 |
| `abstract` | 29,627 | **52.5%** | TEXT | ⚠️ 部分(早期文献多无摘要)|
| `doi` | 869 | **1.5%** | VARCHAR(500) | ⚠️ 低(1970s 文献无 DOI|
| `journal` | 56,432 | **100.0%** | VARCHAR(500) | ✅ 完整 |
| `journal_issn` | 56,432 | **100.0%** | VARCHAR(20) | ✅ 完整 |
| `journal_iso` | 56,432 | **100.0%** | VARCHAR(100) | ✅ 完整(基线文件有完整 ISO 缩写) |
| `pub_year` | 56,432 | **100.0%** | INTEGER | ✅ 完整 |
| `pub_date` | 52,124 | **92.4%** | DATE | ✅ 充分 |
| `print_date` | 52,124 | **92.4%** | DATE | ✅ 充分 |
| `create_date` | 56,432 | **100.0%** | DATE | ✅ 完整 |
| `entrez_date` | 56,432 | **100.0%** | DATE | ✅ 完整 |
| `article_date` | 714 | **1.3%** | DATE | ⚠️ 低(ArticleDate 为新版 PubMed XML 字段)|
| `date_completed` | 56,218 | **99.6%** | DATE | ✅ 完整 |
| `pubmed_revised` | 56,432 | **100.0%** | DATE | ✅ 完整 |
| `volume` | 54,460 | **96.5%** | VARCHAR(50) | ✅ 充分 |
| `issue` | 52,921 | **93.8%** | VARCHAR(50) | ✅ 充分 |
| `pages` | 56,127 | **99.5%** | VARCHAR(100) | ✅ 完整 |
| `language` | 56,432 | **100.0%** | VARCHAR(10) | ✅ 完整 |
| `pmc_id` | 3,616 | **6.4%** | VARCHAR(20) | ⚠️ 低(1970s 文献极少有 PMC|
| `publication_status` | 56,432 | **100.0%** | VARCHAR(30) | ✅ 完整 |
| `citation_status` | 56,432 | **100.0%** | VARCHAR(20) | ✅ 完整 |
| `num_refs` | 4,483 | **7.9%** | INTEGER | ⚠️ 低 |
| `retracted` | 0 | 0% | BOOLEAN | ✅(全库无撤稿)|
### JSON 字段
| 字段 | 非空计数 | 覆盖率 | 数据类型 | 状态 |
|------|---------|--------|---------|------|
| `authors` | 1,659 | **99.8%** | json | ✅ 完整 |
| `pub_types` | 1,557 | **93.7%** | json | ✅ 充分 |
| `mesh_headings` | 845 | **50.8%** | json | ⚠️ 部分 |
| `study_design` | 1,552 | **93.4%** | json | ✅ 充分 |
| `chemical_list` | 508 | **30.6%** | json | ⚠️ 部分 |
| `grants` | 323 | **19.4%** | json | ⚠️ 部分 |
| `databank_list` | 29 | **1.7%** | json | ❌ 缺失 |
| `suppl_mesh_list` | 19 | **1.1%** | json | ❌ 缺失 |
| `keywords` | **0** | **0%** | json | ❌ **缺失** |
| `gene_symbols` | **0** | **0%** | json | ❌ **缺失** |
| `retraction_details` | 0 | 0% | json | ✅(全库无撤稿) |
| `full_text_sections` | 7 | **0.4%** | json | ❌ 仅 PMC OA 实验 |
| `authors` | 55,466 | **98.3%** | JSONB | ✅ 完整 |
| `pub_types` | 56,432 | **100.0%** | JSONB | ✅ 完整 |
| `mesh_headings` | 56,066 | **99.4%** | JSONB | ✅ 完整 |
| `study_design` | 56,432 | **100.0%** | JSONB | ✅ 充分(自动分类)|
| `chemical_list` | 32,209 | **57.1%** | JSONB | ✅ 充分 |
| `trial_reg` | 56,432 | **100.0%** | JSONB | ✅ 完整(含空对象)|
| `retraction_details` | 55,555 | **98.4%** | JSONB | ✅ 充分(主要含空对象)|
| `grants` | 252 | **0.4%** | JSONB | ❌ 缺失(早期文献基金信息极少)|
| `keywords` | 0 | **0%** | JSONB | ⚠️ 基线文件 KeywordList 含大量空数据,需确认 |
| `gene_symbols` | 0 | **0%** | JSONB | ❌ 未解析 |
| `databank_list` | 25 | **0.0%** | JSONB | ❌ 缺失 |
| `suppl_mesh_list` | 18 | **0.0%** | JSONB | ⚠️ 极少 |
| `full_text_sections` | 446 | **0.8%** | JSONB | ⚠️ PMC OA 解析中 |
### TSVECTOR 字段
| 字段 | 覆盖数 | 覆盖率 | 状态 |
|------|--------|--------|------|
| `search_tsv` | 1,662 | **100.0%** | ✅ 完整(但无 setweight |
| `search_tsv` | 56,432 | **100.0%** | ✅ 完整(setweight title=A, abstract=B, author=A |
### 日期间相关字段
### 日期间字段
| 字段 | 覆盖数 | 覆盖率 | 状态 |
|------|--------|--------|------|
| `pub_date` | 1,148 | 69.1% | ⚠️ 部分 |
| `article_date` | 1,148 | 69.1% | ⚠️ 部分 |
| `pubmed_revised` | 1,552 | 93.4% | ✅ 充分 |
| `date_completed` | 558 | 33.6% | ⚠️ 部分 |
| `publication_status` | 1,552 | 93.4% | ✅ 充分 |
| `citation_status` | 879 | 52.9% | ⚠️ 部分 |
| 字段 | 覆盖数 | 覆盖率 | PubMed 标签 | 状态 |
|------|--------|--------|-------------|------|
| `pub_date` | 52,124 | 92.4% | [DP] 出版日期 | ✅ 充分 |
| `print_date` | 52,124 | 92.4% | [PPDAT] 纸质出版 | ✅ 充分 |
| `article_date` | 714 | 1.3% | [EPDAT] 电子出版 | ⚠️ 低 |
| `create_date` | 56,432 | 100.0% | [CRDT] 记录创建 | ✅ 完整 |
| `entrez_date` | 56,432 | 100.0% | [EDAT] 收录日期 | ✅ 完整 |
| `date_completed` | 56,218 | 99.6% | [MHDA] MeSH 标引 | ✅ 完整 |
| `meshed_date` | 56,218 | 99.6% | 系统内部 | ✅ 首次导入=date_completed214 篇 in-process 为 NULL |
| `pubmed_revised` | 56,432 | 100.0% | [LR] 修订 | ✅ 完整 |
| `publication_status` | 56,432 | 100.0% | — | ✅ 完整(VARCHAR,非日期)|
> **类型变更说明:** `create_date`/`entrez_date`/`date_completed`/`meshed_date`/`pubmed_revised` 五个字段原为 TIMESTAMPTZ,现统一为 DATE。NLM 所有日期来源仅提供 Y/M/D,无时间分量。DATE 类型更精确地反映语义且减少存储。当前数据中 3 个字段 6 位时间分量均为 00:00:00+00:00,无损转换。
---
## 2. 数据诊断
## 2. 数据分布特征
### 2.1 关键 Bug
### 年份分布
| # | 问题 | 影响 | 根因 |
|---|------|------|------|
| 1 | `journal_iso` 0% | [TA] 缩写期刊搜索不可用 | XML 解析器已解析但 Europe PMC 路径未解析?实际检查代码第 533 行已解析,但 seed_data 无此字段 |
| 2 | `keywords` 0% | [OT] 关键词搜索不可用 | XML 解析器第 500-503 行已解析,需检查 seed_data 和 Europe PMC 路径 |
| 3 | `pmc_id` 仅 9 篇 | [PMC] 搜索不可用,PMC OA 全文抓取不可用 | XPath 上下文 Bug`article.findall` → 应改为 `article_elem.findall` |
| 4 | `tree_numbers` 0 行 | 所有 [MH]/[MAJR] 查询 0 结果 | NLM mtrees.bin 未导入,`import_mesh_tags.py` 未执行 |
| 5 | `suppl_mesh_list` 仅 1.1% | 补充 MeSH 搜索不可用 | 解析器已实现但 NLM 数据量小 |
| 年份 | 文献数 | 占比 |
|------|--------|------|
| 1975 | 6,017 | 10.7% |
| 1976 | 5,789 | 10.3% |
| 1977 | 11,907 | 21.1% |
| 1978 | 12,938 | 22.9% |
| 1979 | 18,903 | 33.5% |
| 1980 | 1 | <0.1% |
| 2025 | 1 | <0.1% |
| 2026 | 876 | 1.6% |
### 2.2 数据异常
> 基线文件按 PMID 递增排序(PMID ≈ 时间顺序),前 15 个文件覆盖 1975–1979 为主。
> 后续导入文件 0016+ 将逐步覆盖 1980s2020s。
| 问题 | 详情 |
|------|------|
| 年份集中 | 1,604/1,662 (96.5%) ∈ 2026,早期文献极少(仅有 58 篇 2025 年) |
| 被引为 0 | 中位被引次数为 0,平均值 0.6 |
| 标签稀疏 | 每篇文献中位仅 1 个标签(P99=6),28/99 标签关联 0 篇文献 |
| 标签来源单一 | 99 个标签全部 `source=manual`,无 `mesh``auto` 来源标签 |
| 中文极少 | 35/1,662 (2.1%) 中文文献 |
### 语言分布 Top 10
| 语言 | 文献数 | 占比 |
|------|--------|------|
| English (eng) | 41,300 | 73.2% |
| German (ger) | 4,496 | 8.0% |
| French (fre) | 2,603 | 4.6% |
| Russian (rus) | 2,517 | 4.5% |
| Japanese (jpn) | 1,641 | 2.9% |
| Italian (ita) | 853 | 1.5% |
| Polish (pol) | 734 | 1.3% |
| Spanish (spa) | 549 | 1.0% |
| Czech (cze) | 253 | 0.4% |
| Romanian (rum) | 246 | 0.4% |
### 文献类型 Top 10
| 类型 | 文献数 |
|------|--------|
| Journal Article | 54,249 |
| Case Reports | 11,850 |
| Research Support, U.S. Gov't, P.H.S. | 9,935 |
| English Abstract | 7,766 |
| Comparative Study | 4,704 |
| Review | 4,632 |
| Letter | 1,611 |
| Clinical Trial | 1,520 |
| Research Support, U.S. Gov't, Non-P.H.S. | 1,289 |
| Randomized Controlled Trial | 504 |
### Top 10 期刊
| 期刊 | ISO 缩写 | 文献数 |
|------|---------|--------|
| Lancet | Lancet | 1,781 |
| Cancer | Cancer | 1,577 |
| Cancer Research | Cancer Res | 1,496 |
| Journal of the National Cancer Institute | J Natl Cancer Inst | 1,166 |
| Cancer Treatment Reports | Cancer Treat Rep | 642 |
| Proceedings of the National Academy of Sciences | Proc Natl Acad Sci USA | 633 |
| AJR American Journal of Roentgenology | AJR Am J Roentgenol | 579 |
| International Journal of Cancer | Int J Cancer | 554 |
| Rofo | Rofo | 475 |
| Journal of Immunology | J Immunol | 442 |
---
@@ -96,108 +151,123 @@
| 来源 | 数量 | Active |
|------|------|--------|
| manual | 99 | True |
| manual(种子标签) | 99 | 99 (100%) |
| auto(管道懒创建) | 10,303 | 0 (0%) |
| meshC04 导入) | 0 | — |
| **总计** | **10,402** | **99 (1.0%)** |
`global_tag_tree_numbers`**0 行**(空表)
`global_tag_tree_numbers`**0 行**(空表 — NLM mtrees 尚未导入
### 文献-标签关系
| 指标 | 值 |
|------|----|
| 总关联数 | 997 |
| is_major=True | 946 (94.9%) |
| is_major=False | 51 (5.1%) |
| 每篇标签 P50 | 1 |
| 每篇标签 P75 | 2 |
| 每篇标签 P90 | 5 |
| 每篇标签 P99 | 6 |
| 未使用标签 | 28 个(0 篇文献关联) |
| 已打标文献 | 56,066 (99.4%) |
| 总关联数 | 635,126 |
| is_major=True | 38,388 (6.0%) |
| 每篇标签 P50 | 11 |
| 每篇标签 P75 | 14 |
| 每篇标签 P90 | 17 |
| 每篇标签 P99 | 23 |
### Top 10 标签覆盖率
### Top 15 标签
| 标签 | 关联文献数 |
|------|-----------|
| 标签 #1 | 66 |
| 标签 #2 | 62 |
| 标签 #3 | 55 |
| 标签 #4 | 51 |
| 标签 #5 | 50 |
| 标签 #6 | 50 |
| 标签 #7 | 48 |
| 标签 #8 | 47 |
| 标签 #9 | 44 |
| 标签 #10 | 41 |
| 标签名 | source | 关联文献数 |
|--------|--------|-----------|
| Humans | auto | 43,509 |
| Female | auto | 24,067 |
| Male | auto | 21,162 |
| Animals | auto | 14,960 |
| Adult | auto | 14,023 |
| Middle Aged | auto | 13,893 |
| Aged | auto | 9,380 |
| Mice | auto | 6,696 |
| Adolescent | auto | 5,505 |
| Neoplasms | auto | 5,076 |
| Child | auto | 5,016 |
| Neoplasms, Experimental | auto | 4,860 |
| Rats | auto | 4,252 |
| Cell Line | auto | 4,123 |
| Neoplasm Metastasis | manual | 4,083 |
> auto 标签 `is_active=False`,对用户端不可见。需审核确认有意义标签后激活。
---
## 4. pubmed_api 来源字段质量
## 4. 出版状态与引用状态
1,552 篇 `source=pubmed_api` 的字段覆盖:
### Publication Status
| 字段 | coverage | 说明 |
|------|----------|------|
| publication_status | 1,552 (100%) | ppublish=544, epublish=530, aheadofprint=478 |
| citation_status | 879 (56.6%) | MEDLINE=502, Publisher=254, In-Process=64, PubMed-not-MEDLINE=59 |
| language | 1,534 (98.8%) | eng=1,496, en… 等 |
| study_design | 1,552 (100%) | 自动分类结果 |
| trial_reg | ~93% | 大部分有(即使空对象) |
| chemical_list | ~31% | 部分文献有化学列表 |
| grants | ~21% | 部分文献有基金信息 |
| databank_list | ~2% | 极少 |
| suppl_mesh_list | ~1% | 极少 |
| full_text_sections | 7 | 仅实验性抓取 |
---
## 5. 优先改进项
### P0(影响搜索基础功能)
| 改进项 | 当前 | 目标 | 方法 |
|--------|------|------|------|
| `journal_iso` 覆盖率 | 0% → **100%** | 修复 seed_data 和 Europe PMC 路径的 journal_iso 解析 |
| `tree_numbers` | 0 行 → **全量** | 运行 `import_mesh_tags.py` 导入 NLM mtrees2025.bin |
| `is_negative_result` | 仅 pubmed_api 有 → **所有** | 构造函数补全 + seed_data 回填 |
| 构造函数 5 字段 | 部分缺失 → **全部** | 代码修复 |
### P1(影响搜索能力)
| 改进项 | 说明 |
|--------|------|
| `keywords` 覆盖率 0% → 有数据 | 修复 keyword 解析器路径 |
| `pmc_id` 覆盖率提升 | XPath Bug 修复后运行 pipeline 回填 |
| `entrez_date` 新字段 | PubmedData/History 解析 |
| `grants` Europe PMC 路径补全 | grants 从 Europe PMC JSON 解析 |
### P2(影响搜索体验)
| 改进项 | 说明 |
| 状态 | 文献数 |
|------|--------|
| 缺失索引(6 个) | B-tree + GIN + trigram |
| JSON 列 json → jsonb | 启用 `@>` 等运算符 |
| `GlobalTagTreeNumber` 导出 | 模型导出修复 |
| ppublish(纸质出版) | 55,821 |
| epublish(电子出版) | 459 |
| aheadofprint(先于印刷) | 152 |
### Citation Status
| 状态 | 文献数 |
|------|--------|
| MEDLINE | 56,066 |
| Publisher | 153 |
| PubMed-not-MEDLINE | 153 |
| In-Process | 60 |
---
## 6. 数据库索引现状
## 5. 索引现状
### 当前索引
```
ix_global_literature_pub_date B-tree (pub_date)
ix_global_literature_search_tsv GIN (search_tsv)
ix_global_literature_pmid UNIQUE B-tree (pmid)
ix_global_literature_doi UNIQUE B-tree (doi)
idx_gl_pub_date B-tree (pub_date)
idx_gl_pub_year B-tree (pub_year)
idx_gl_journal_issn B-tree (journal_issn)
idx_gl_journal_iso B-tree (journal_iso)
idx_gl_created B-tree (created_at)
idx_gl_pmc_id B-tree (pmc_id)
idx_gl_search_tsv GIN (search_tsv)
ix_gl_doi B-tree (doi)
ix_gl_language B-tree (language)
ix_gl_retracted B-tree (retracted)
ix_gl_pub_types_gin GIN (pub_types)
ix_gl_study_design_gin GIN (study_design)
ix_gl_grants_gin GIN (grants)
ix_gl_chemical_list_gin GIN (chemical_list)
```
### 缺失索引
所有索引已就位。
| 索引 | 类型 | 用途 |
|------|------|------|
| `ix_global_literature_journal_iso` | B-tree | `[TA]` 期刊缩写搜索 |
| `ix_global_literature_pub_types` | GIN jsonb_path_ops | 文献类型筛选 |
| `ix_global_literature_study_design` | GIN jsonb_path_ops | 研究设计筛选 |
| `ix_global_literature_retracted` | B-tree | 撤稿过滤 |
| `ix_global_literature_language` | B-tree | `[LA]` 搜索 |
| `ix_global_literature_grants` | GIN | `[GR]` JSONB 搜索 |
| `ix_global_literature_chemical_list` | GIN | `[NM]` JSONB 搜索 |
---
## 6. 关键质量评估
### ✅ 强项
- 6 个 PubMed 日期字段全部完整(create_date/entrez_date 100%
- `journal_iso` 100%([TA] 缩写期刊搜索可用)
- `mesh_headings` 99.4%(可支持 [MH]/[MAJR] 打标搜索)
- `study_design` 100%(研究设计分类覆盖完整)
- `publication_status` / `citation_status` 100%(完整元数据)
- 搜索 tsvector 100%(带 setweight 权重)
- 标签关联 635,126 条,每篇中位 11 标签
### ⚠️ 待改进
- `abstract` 仅 52.5%(早期文献特点,后续文件 1980s+ 会改善)
- `doi` 仅 1.5%(同上,后续文件改善)
- `article_date`[EPDAT])仅 1.3%ArticleDate 为 PubMed XML 较新字段)
- `keywords` 0%(基线文件中 KeywordList 多为空,需确认是否需要填补)
- `grants` 0.4%(早期文献基金数据极少)
- `tree_numbers` 0 行 — **[MH] 子树展开不可用**,需导入 NLM mtrees
- 10,303 个 auto 标签 `is_active=False` — 需审核激活有意义标签
- PMC 覆盖 6.4%1970s 文献无 PMC 正常)
### 📌 基线文件进度说明
当前仅导入了前 15 个基线文件(pubmed26n0001~0015)。PubMed 2026 年基线共 590 个文件,当前覆盖约 2.5%。
后续文件特性(基于 PMID 递增规律):
- 0016~020019801999 年文献
- 0201~040020002015 年文献
- 0401~059020162026 年文献
随着更多文件的导入,DOI/abstract/PMC 覆盖率将显著提升。
+109
View File
@@ -0,0 +1,109 @@
# Pub高级搜索 测试用例
## 测试环境
打开 <http://localhost:5173/advanced-pub-search>
---
## 1. Search Builder(查询构建器)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 1.1 | AND 状态下,Field 选 Title,输入 `lung cancer`,点 Add | Query Box 显示:`"lung cancer"[TI]` |
| 1.2 | 把 AND 改成 ORField 选 Author,输入 `Smith J`,点 Add | Query Box 显示:`"lung cancer"[TI] OR "Smith J"[AU]` |
| 1.3 | Field 选 All Fields,输入 `pembrolizumab`,点 Add | Query Box 显示:`"lung cancer"[TI] OR "Smith J"[AU] AND pembrolizumab` |
| 1.4 | **引号去重**Field 选 Title,输入 `"already quoted"`,点 Add | Query Box 最后追加 `"already quoted"[TI]`(不会出现 `""already quoted""` |
| 1.5 | **Enter 快捷键**Field 选 All Fields,输入 `test`,按 Enter | 同点 Add,自动追加到 Query Box |
## 2. Query Box(查询框)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 2.1 | 手动输入:`"breast cancer"[TI] AND "trastuzumab"[TIAB]` | Translation Table 显示 2 行:`[TI] Title` / `[TIAB] Title/Abstract` |
| 2.2 | 追加 `AND 2022:2024[DP]` | Translation Table 新增 1 行:`[DP] Publication Date``2022:2024` |
| 2.3 | 测试 `NOT` 查询:`"lung cancer"[TI] NOT "mouse"[MH]` | 前端验证通过,不报错 |
| 2.4 | **长日期格式**`2020/01/01:2020/12/31[DP]` | Translation Table 正确显示 Date Range |
| 2.5 | **括号嵌套**`("a"[TI] OR "b"[TI]) AND "c"[TIAB]` | 验证通过 |
| 2.6 | **PMID**`12345678[PMID]` | Translation Table 显示 `[PMID]` 行 |
## 3. 验证规则
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 3.1 | Query Box 为空,点 Search | 提示"请输入查询" |
| 3.2 | 输入 `AND lung cancer`(以运算符开头) | 提示"查询不能以布尔运算符开头" |
| 3.3 | 输入 `lung cancer AND`(以运算符结尾) | 提示"查询不能以布尔运算符结尾" |
| 3.4 | 输入 `a AND AND b`(连续运算符) | 提示"不允许连续使用布尔运算符" |
| 3.5 | 输入 `(a AND b`(缺少右括号) | 提示"括号不匹配" |
| 3.6 | 输入 `a AND b)`(多余右括号) | 提示"多余的右括号" |
| 3.7 | 输入 `"test"[INVALID]` | 提示"字段标签 [INVALID] 不合法" |
## 4. Search(搜索执行)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 4.1 | 输入 `"lung cancer"[TI]`,点 **Search** | 跳转到 `/search?q="lung cancer"[TI]`,显示结果 |
| 4.2 | 检查搜索结果 | 标题字段匹配 "lung cancer" 的文献,数量 > 0 |
| 4.3 | **OR 查询**:回到 Pub高级搜索页,输入 `"lung"[TI] OR "cancer"[TI]`,点 Search | 结果比 4.1 多(应该是两个词各自结果的并集) |
| 4.4 | **NOT 查询**`"lung cancer"[TI] NOT "review"[PT]`,点 Search | 结果比 4.1 少(排除了 review 类型) |
| 4.5 | **日期范围**`2024:2025[DP] AND "cancer"[TI]`,点 Search | 只返回 2024-2025 年的文献 |
| 4.6 | **MeSH 查询**`"lung neoplasms"[MH]`,点 Search | 返回带有 Lung Neoplasms MeSH 标签的文献 |
| 4.7 | **MAJR 查询**`"lung neoplasms"[MAJR]`,点 Search | 比 4.6 少(仅 Major Topic |
| 4.8 | **PMID 精确搜索**`36261522[PMID]`,点 Search | 精确返回该文献 |
| 4.9 | **DOI 搜索**:已知某篇 DOI,输入 `"10.xxxx/xxxx"[DOI]`,点 Search | 精确返回该文献 |
| 4.10 | **多字段复杂查询**`("lung cancer"[TI] OR "nsclc"[TIAB]) AND 2023:2025[DP] NOT "case reports"[PT]`,点 Search | 执行复杂布尔查询,返回结果 |
## 5. Search History(搜索历史)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 5.1 | 执行 4.1 的搜索(`"lung cancer"[TI]` | 成功后 History 出现 `#1`,显示查询、结果数和时间 |
| 5.2 | 执行 4.3 的搜索(OR 查询) | History 出现 `#2` |
| 5.3 | 刷新页面 | #1#2 仍然存在(localStorage 持久化) |
| 5.4 | 点击 `#1` | `#1` 追加到 Query Box 末尾 |
| 5.5 | Query Box 中写 `#1 AND #2`,点 Search | resolve 后正确跳转到搜索结果页 |
| 5.6 | 输入 `#999`(不存在的编号),点 Search | 提示"历史引用 #999 不存在" |
| 5.7 | 点 `#1` 行的 ✕ 按钮 | #1 从 History 中删除 |
| 5.8 | 点 **Clear All** | 所有历史被清空,显示"暂无搜索历史" |
| 5.9 | 点 **Download** | 下载一个 `.tsv` 文件,内容包含所有历史记录 |
## 6. Combine(组合历史查询)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 6.1 | 先搜索 2 个不同查询(如 `"lung"[TI]``"breast"[TI]` | 分别产生 #1#2 |
| 6.2 | Combine 行:输入框 A 写 `1`,选 `AND`,输入框 B 写 `2`,点 Combine | Query Box 追加 `#1 AND #2` |
| 6.3 | 点 Search | 自动展开为 `("lung"[TI]) AND ("breast"[TI])`,搜索结果正确 |
| 6.4 | 输入框 A 写 `#1`,输入框 B 写 `#2`(带 # 前缀) | 同样正常工作,不会出现 `##1` |
## 7. Add to History(仅加入历史)
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 7.1 | 输入 `"targeted therapy"[TI]`,点 **Add to History** | 提示"#3 已加入历史(N 条结果)"Query Box 不变 |
## 8. 暗色模式
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 8.1 | 切换到暗色模式(如果有切换开关) | 页面正常显示 |
| 8.2 | 点击 Search Builder 的 Field 下拉框 | 下拉弹窗选项文字在 hover/选中时显示白色 |
| 8.3 | 点击任意按钮(Search/Clear/Add | 按钮文字始终白色可见 |
## 9. 响应式
| 步骤 | 操作 | 期望结果 |
|------|------|----------|
| 9.1 | 浏览器窗口缩窄到 < 768px | Builder 变垂直排列,下拉框全宽 |
| 9.2 | 手机宽度下测试 | 无水平溢出,所有功能正常 |
---
## 回归检查
| 检查项 | 期望 |
|--------|------|
| 首页搜索框旁的 Pub高级搜索按钮(代码图标) | 正常显示,点击跳转 |
| 首页原有"高级搜索"齿轮按钮 | 不受影响,功能不变 |
| 搜索结果页宽度 | 与首页、Pub高级搜索页一致 |