Files
backend/docs/13-搜索修复全记录.md
T
34047007@qq.com 7032accd31
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
docs: 更新第五轮修复记录提交哈希
2026-07-27 11:55:21 +08:00

419 lines
22 KiB
Markdown
Raw 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.
# PubMed 搜索合规修复全记录
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
>
> **累计**5 轮,63 项修复,50+ 字段标签注册,214 项测试覆盖
> **时间跨度**2026-07-24 ~ 2026-07-27
> **核心文件**`pubmed_query_parser.py`~620 行)→ `search_engine.py`~1250 行)
---
## 目录
1. [第一轮:Phase 0-7 基础修复(34 项)](#第一轮phase-0-7-基础修复)
2. [第二轮:第二轮审计修复(8 项)](#第二轮第二轮审计修复)
3. [第三轮:第三轮审计修复(14 项)](#第三轮第三轮审计修复)
4. [第四轮:字段补全与语义优化(11 项)](#第四轮字段补全与语义优化)
5. [第五轮:第 5 轮全面审计修复(4 项)](#第五轮第-5-轮全面审计修复)
6. [遗留限制](#遗留限制)
---
## 第一轮:Phase 0-7 基础修复
**提交**`723c4fc` / `62ca8fa` / `5f69277`
**日期**2026-07-24 ~ 2026-07-25
**数量**34 项
**触发**9 Agent 并行深度审计
### 背景
首次系统审计发现搜索功能存在大量阻塞性 Bug:MeSH 搜索因 INNER JOIN 返回 0 结果、布尔运算符 OR 被当作 AND 处理、PM C_ID 格式混乱、数据导入缺失 journal_iso 和 keywords 等关键字段。整体搜索几不可用。
### Phase 0 — 关键 Bug 修复(12 项)
| # | 修复项 | 文件 | 问题描述 | 根因分析 | 修改内容 |
|---|--------|------|---------|---------|---------|
| 0.1 | `_expand_mesh_tag_ids` INNER JOIN | `search_engine.py:518` | 所有 `[MH]`/`[MAJR]` 搜索返回 0 个结果。tree_numbers 表无 C04 数据,INNER JOIN 过滤掉所有标签 | `select(GlobalTag.id).join(GlobalTagTreeNumber)` 在有 tree_number 的标签时才返回行 | 改为 `select(GlobalTag.id).where(...)`,使 MeSH 搜索在无 tree_number 展开时仍能工作 |
| 0.2 | `recent_subq` 条件化 | `search_engine.py:314-319` | 有搜索词时仍被 `pub_date > 90d` 子查询过滤,漏掉旧文章 | 未检查 `query.strip()` 就附加 recent 子句 | 有搜索词或历史查询时跳过 recent 子查询 |
| 0.3 | OR 布尔运算符 | `search_engine.py:394-414` | 同字段多个词始终 AND`lung[TI] OR breast[TI]` 只返回同时命中 lung 和 breast 的文献 | `boolean` 参数只在文本搜索路径生效,未传入 `_field_conditions` | 同 field 按 `boolean_operator``or_()` 组合 |
| 0.4 | 多 [MH] 词 AND 组合 | `search_engine.py:516-519` | `lung neoplasms[MH] AND immunotherapy[MH]` 返回 0 结果 | 所有 mesh 词合并到一个 subq IN,必须所有标签同时关联同一篇文献 | 逐 term 独立 subq,按 boolean 组合(AND 用 intersect |
| 0.5 | PMC XPath 上下文 | `pubmed_api.py:440,447` | pmc_id/is_oa 解析错误,只有 9/1662 篇有值 | `article.findall('.//PMCID')``article` 而非 `article_elem` 上调用 | `article.findall``article_elem.findall` |
| 0.6 | 构造函数遗漏 | `pubmed_api.py:914-944` | `pubmed_revised`/`citation_status`/`date_completed`/`article_date`/`suppl_mesh_list` 未写入模型 | `_parse_pubmed_xml()` 返回值不包含这些字段 | 补全 5 字段传递 |
| 0.7 | ArticleTitle itertext | `pubmed_api.py:409` | 含 `<i>`/`<sub>` 等内联标记的标题截断 | `.text` 只返回第一个文本节点 | `"".join(itertext())` |
| 0.8 | PMC_ID 格式统一 | `pubmed_api.py:264` | "PMC1234567" vs "1234567" 混用 | `lstrip("PMC")` → 会误删 `PMC` 开头真实数字 | `art.get("pmcid", "").lstrip("PMC")` |
| 0.9 | 搜索端点异常处理 | `features.py:68` | 搜索异常暴露 500 内幕 | 无 try/except | 加异常处理返回 400 |
| 0.10 | field:all 硬编码 | `SearchView.vue:83` | 前端固定发送 `field: "all"` | 模板字符串手误 | 替换为动态 `field.value` |
| 0.11 | journal_iso 导入修复 | `pubmed_api.py` | 0% 覆盖率 | Journal ISOAbbreviation 未解析 | 增补 ISOAbbreviation 提取 |
| 0.12 | keywords 导入修复 | `pubmed_api.py` | 0% 覆盖率 | KeywordList 未解析 | 增补 KeywordList 解析 |
### Phase 1 — P0 新功能(8 项)
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | MeSH 树号导入 | `scripts/import_mesh_tags.py` | 导入 NLM `mtrees2025.bin`,填充 `GlobalTagTreeNumber` |
| 1.2 | `entrez_date` 解析 | `pubmed_api.py` + migration | 解析 `PubmedData/History/PubMedPubDate[@PubStatus="entrez"]``[EDAT]` 可搜 |
| 1.3 | `[AD]` 标签 | `search_engine.py` + `pubmed_query_parser.py` | 机构字段映射到 `authors` JSONB cast |
| 1.4 | `[LA]` 标签 | 同上 | 语言字段 = `GlobalLiterature.language` |
| 1.5 | `[EDAT]`/`[CRDT]`/`[MHDA]`/`[LR]`/`[DCOM]`/`[DEP]` | 同上 | 6 个日期字段的范围+独立语法 |
| 1.6 | setweight 迁移 | 迁移脚本 | ⏳ 待办:tsvector 重建带 A/B 权重 |
| 1.7 | ATM 引擎 v1 | `query_expansion.py` | SynonymExpander + MeSH 翻译 + 期刊翻译 |
| 1.8 | best_match 权重调优 | `search_engine.py` | ⏳ 待 setweight 后调优 |
### Phase 2 — 字段标签覆盖(14 项)
| # | 任务 | 状态 | 说明 |
|---|------|------|------|
| `[OT]` | keywords 搜索 | ✅ | 映射到 `all`(第四轮修复为独立 keywords JSONB |
| `[GR]` | grants 搜索 | ✅ | `jsonb_array_elements` + ILIKE |
| `[NM]` | 化学物质名 | ✅ | `chemical_list` JSONB contains |
| `[RN]` | Registry Number | ✅ | `chemical_list.registry_number` |
| `[SH]` | Subheading | ✅ | `mesh_headings.qualifiers` |
| `[SI]` | DataBank | ✅ | `databank_list.accession_numbers` |
| `[PA]` | Pharmacological Action | ✅ | `pharmacological_actions` JSONB |
| `[TW]` | 文本词 | ✅ | 映射到 `all` |
| `[TA]`/`[JT]` | 期刊全称+缩写 | ✅ | journal ILIKE + journal_iso ILIKE |
| `[CN]`/`[FAU]`/`[LAU]` | 作者变体 | ✅ | 均映射到 `author` 路径 |
| Entry Terms | MeSH 入口词 | ✅ | `desc2025.asc``GlobalTag.entry_terms` |
| 中文搜索 | CJK 支持 | ✅ | `simple` 词典而非 `english` |
| 多 affiliation | 捕获所有 | ✅ | `find``findall` + `\|` 连接 |
### Phase 3 — E-utilities 补充抽取(含 pubmed_api.py 修复)
- 反向兼容阶段,确保 pubmed_api.py 正确抽取所有字段
- PMC/trial_reg/retraction 等补充数据通路修复
### Phase 4 — 搜索质量
- 历史搜索查询 + 日期筛选整合
- `best_match` 排序引入 `cited_by_count` + 年度梯度
- 缺省排序降级保护
### Phase 5 — 解析器健壮性
- 括号嵌套超过 10 层保护
- 空查询短路
- token 超限截断
- 未知字段标签静默降级
- Unicode normalization(全角数字、零宽字符)
### Phase 6 — 前端搜索 UX
- SearchView URL 状态全量持久化(24 个参数)
- HomeView → SearchView 参数正确传递
- LiteratureCard `search` 事件冒泡
- 响应式布局适配(移动端搜索栏隐藏)
### Phase 7 — API 验证
- 请求参数 Pydantic validator
- `field` 只接受预定义值
- 查询长度限制
---
## 第二轮:第二轮审计修复
**提交**`e688241`
**日期**2026-07-26
**数量**8 项 + 6 项新测试
**触发**:审计发现 Phase 1-7 修复中的遗留 Bug
### 背景
8 项在首次审计中遗漏的中高级 Bug,全部在后端搜索核心路径。
### 修复清单
#### F1: `[MH:noexp]` 顶层 `_noexp` 标志丢失
- **文件**`search_engine.py` `_pubmed_conditions()` L640-650
- **问题**`mesh_terms` 处理时只提取 `.text`,丢弃了 `_noexp` 标志,导致 `asthma[MH:noexp]` 在顶层使用时仍然树展开,和 `[MH]` 无异
- **根因**:循环处理 vs 括号组内调用 `_single_term_condition()` 两条路径——后者正确传递 `noexp`,前者未分组
- **修复**:将 `mesh_terms``_noexp` 拆为两组,分别调用 `_expand_mesh_tag_ids`
```python
noexp_names = [t.text for t in pp.mesh_terms if t._noexp]
exp_names = [t.text for t in pp.mesh_terms if not t._noexp]
```
#### F2: 组内 NOT 违反 De Morgan 律
- **文件**`search_engine.py` L862-877
- **问题**`NOT (A AND B)` → 被解析为 `not_(A) AND not_(B)` = `NOT (A OR B)`,语义完全翻转
- **根因**:全 NOT 组内每个 term 独立 `not_()`,然后 AND 组合
- **修复**:组的 `all_not=True` 时,对所有 term 不做独立 NOT,改为 `not_(combined)` 包裹组合条件
#### F3: `_parse_not_expr` 不支持重复 NOT
- **文件**`pubmed_query_parser.py` L428-433
- **问题**`NOT NOT cancer` → 第二个 "NOT" 被降级为搜索字面词,变成 `NOT "NOT" AND cancer`
- **根因**:语法定义 `not_expr → NOT not_expr | primary`,但实现直接跳到 `_parse_primary(result, negated=True)`,丢失递归
- **修复**:改为递归调用 `_parse_not_expr` 并 toggle `is_not`
#### F4: SearchView Custom Range 不发送日期
- **文件**`SearchView.vue` L296-309
- **问题**:选择"自定义年份范围"→ 搜索,请求体不含任何日期参数,返回全库结果
- **根因**`datePreset === 'custom'` 分支未处理。仅 `!datePreset.value` 和 `datePreset.value !== 'custom'` 两个条件都失败
- **修复**:增加 `datePreset === 'custom'` 分支发送 `year_from`/`year_to`
#### F5: HomeView.restoreFromUrl 恢复不全
- **文件**`HomeView.vue` L348-364
- **问题**URL 含 `?q=cancer&retracted=only` 时,retracted 参数丢失
- **根因**:只恢复了 `tag`/`date_from`/`date_to`/`q`,缺失 `sort`/`field`/`retracted`/`negative_result`
- **修复**:补全 4 个缺失参数的读取
#### F6+F7: 死代码清理 — precision_mode + is_oa
- **文件**`AdvancedSearchPanel.vue` / `types/index.ts`
- **问题**UI 中 precision_mode 切换按钮存在但无任何效果,混淆用户
- **修复**:全链路移除 precision_mode 控件和类型字段
#### F8: `_expand_mesh_tag_ids` N+1 查询 → 批量
- **文件**`search_engine.py` L1119-1143
- **问题**N 个 MeSH 词 → 2N 次 `db.execute` + 2 次树查询 = 8 轮数据库往返
- **根因**`for m in mesh_names:` 循环内每次执行 2 次独立查询
- **修复**OR 合并所有词的 entry_terms 和 name_en ILIKE 到 2 次批量查询
---
## 第三轮:第三轮审计修复
**提交**`a37cc50`
**日期**2026-07-27
**数量**14 项(P0×5, P1×4, P3×5
**触发**:6 Agent 并行深度代码审计
### 背景
第三轮审计聚焦于隐藏较深的逻辑缺陷:PubMed 子集/状态/UID 字段的网关检查遗漏、字段到领域的错误映射、括号组的单 NOT 边缘情况、前端日期精度丢失等。
### P0 — 搜索结果错误(5 项)
#### P0-1: sb/stat/uid/dep 门控遗漏
- **文件**`search_engine.py` 两个 `has_pubmed_terms` 检查点(L162-179, L189-205
- **问题**`medline[SB]` 等解析后产出了 `pp.sb_terms`,但 `has_pubmed_terms` 未检查它,导致走纯文本路径,`[SB]` 条件被丢弃
- **根因**`has_pubmed_terms` gate 随字段新增未同步更新
- **修复**:在条件判断中添加 `pp.sb_terms`, `pp.stat_terms`, `pp.uid_terms`, `pp.dep_from`
#### P0-2: `[SB]` 映射到错误领域
- **文件**`search_engine.py` L830-839
- **问题**`medline[SB]` 被映射到 `nlm_subsets`(期刊级),但 PubMed 的 `medline[SB]` 是指记录级别 `citation_status=medline`
- **根因**:所有 `[SB]` 值笼统走 `nlm_subsets overlap` 路径
- **修复**
- `MEDLINE` → `citation_status == "medline"`
- `PUBMED` → no-op(所有记录都是 PubMed
- 单字母代码(AIM/S/D 等)→ `nlm_subsets overlap`(期刊级)
- 其他文本 → `citation_status == val.lower()`
#### P0-3: 括号组单 NOT 词丢失否定
- **文件**`search_engine.py` L882
- **问题**`NOT (cancer)` — 组内只有 1 个词,`len(group_conds) > 1` 条件失败,NOT 被丢失
- **根因**:全 NOT 组的包裹检查是 `> 1`(针对多词 NOT 组优化),但 1 个词的组也被 NOT 包裹才正确
- **修复**`> 1` → `>= 1`
#### P0-4: HomeView watch 丢弃参数
- **文件**`HomeView.vue`
- **问题**`sort`, `field`, `retracted`, `negative_result` 在 `watch(searchKey)` 的 URL 同步中被丢弃
- **修复**:把这些参数加入 `searchKey` computed 依赖和 URL 替换逻辑
#### P0-5: 日期精度丢失
- **文件**`SearchView.vue`
- **问题**URL 中的 `date_from=2025-03-15` 恢复后变成 `2025-03-14` 或丢失
- **根因**:使用 `date_from`/`date_to` ref 存储(受 DatePicker 组件双向绑定影响),而非原始 URL 值
- **修复**:增加 `urlDateFrom`/`urlDateTo` ref 直接存储原始日期字符串
### P1 — 功能缺失(4 项)
#### P1-1: `[ALL]` 未注册
- **文件**`pubmed_query_parser.py`
- **问题**`[ALL]` tag 未在 `_FIELD_TAG_MAP` 和 `_ALL_FIELD_TAGS` 中注册,导致被 `is_pubmed_syntax()` 识别但 tokeniser 不识别 → `UNKNOWN_FIELD` → 静默降级
- **修复**:在 `_FIELD_TAG_MAP` 添加 `"ALL": "all"`,在 `_ALL_FIELD_TAGS` 添加 `"ALL"`
#### P1-2: 独立日期字段降级
- **文件**`pubmed_query_parser.py` `_dispatch_term`
- **问题**`2024-01-01[DP]` 作为日期范围使用时正确,但作为独立字段值时(term.field 非 None 且非范围语法)被降级到 `plain_terms`
- **根因**`_dispatch_term` 缺少 `term.field` 为日期字段名称时的独立处理分支
- **修复**`_dispatch_term` 增加 DP/EDAT/CRDT/MHDA/LR/DCOM/DEP 裸字段处理→设置 `from=to=日期`
#### P1-3: 浮点日期范围不交换
- **文件**`pubmed_query_parser.py` `_parse_range`
- **问题**`2026:2024[DP]` 只对纯 digit 做了交换,`2024-12-01:2024-01-01[DP]` 这种完整日期不交换
- **修复**elif 增加非 digit ISO 字符串比较 + 交换
#### P1-5: MeSH entry_terms 大小写不敏感
- **文件**`scripts/import_mesh_full.py`
- **问题**Entry terms 导入时未统一 lowercase`@>` 匹配区分大小写,导致 `cancer` 无法匹配 `Cancer`
- **修复**`.lower()` 统一存储
### P3 — 健壮性(5 项)
| # | 修复项 | 文件 | 问题 | 修复 |
|---|--------|------|------|------|
| P3-2 | MeSH 展开无保护 | `search_engine.py` | `_expand_mesh_tag_ids` 和 `expand_atm` 的 DB 查询未包裹异常 | `try/except` 包裹 DB 查询块 |
| P3-4 | 参数无验证 | `features.py` | `sort`/`field`/`boolean` 参数接受任意值 | `field_validator` |
| P3-5 | GET 搜索无限制 | `literature.py` | 超长查询可耗尽资源 | 100 词上限 |
| P3-6 | 中文正则不一致 | `query_expansion.py` | 中文检测正则与 search_engine 不一致 | `[一-鿿㐀-䶿豈-﫿]` 同步 |
| P1-8 | page_size 未恢复 | `SearchView.vue` | URL 翻页参数丢失 | `restoreFromQuery` + `syncSearchToUrl` |
---
## 第四轮:字段补全与语义优化
**提交**`807972d`
**日期**2026-07-27
**数量**11 项
**触发**:系统跟踪遗留限制的逐个解决
### 背景
前三轮修复后仍有多项已知限制:8 个 PubMed 字段标签未注册、`[OT]` 语义过宽(映射到 `all`)、精确短语用 ILIKE 而非 GIN 索引。第四轮系统性地清理了这些残留项。
### 字段注册(8 项)
| # | 字段标签 | 映射目标 | 说明 |
|---|---------|---------|------|
| P4-1 | `[Title/Abstract]` | `all` | PubMed 长标签,等同 `[TIAB]` |
| P4-2 | `[OAB]` | `all` | Other Abstract |
| P4-3 | `[WORD]` | `all` | Word in text |
| P4-4 | `[FI]` | `GR` 同路径 | Funder Identifier,搜索 grant_id |
| P4-5 | `[SO]`/`[PL]` | `journal` | Source / Place of Publication(近似映射) |
| P4-6 | `[GEN]` | `gene_symbols` JSONB | 基因符号精确搜索(数据覆盖率依赖实际导入) |
| P4-7 | `[PMC]` | `pmc_id` 列 | PMCID 精确匹配(数据覆盖率依赖实际导入) |
**涉及修改**
- `_ALL_FIELD_TAGS` set:新增 8 个标签名
- `_FIELD_TAG_MAP`:注册映射关系
- `_SPECIAL_FIELDS`:加 `OT`、`GEN`、`PMC`
- `ParsedPubmedQuery`:加 `ot_terms`/`gene_terms`/`pmc_terms` list
- `_dispatch_term`:加 3 个 elif 分支
- `search_engine.py`:两个 `has_pubmed_terms` gate 加新字段
### 语义修复(3 项)
#### P4-8: `[OT]` → keywords JSONB(不再映射到 all
- **问题**`[OT]` 映射到 "all" 意味着在标题/摘要/PMID/DOI 全字段搜索,语义过宽。PubMed 的 `[OT]` 只搜索 Other Keywords`keywords` JSONB 列)
- **修复**
- `_dispatch_term` 将 `OT` 路由到 `ot_terms`(而非 `_FIELD_TAG_MAP` → `all` → `tiab_terms`
- `_pubmed_conditions` 增加 `ot_terms` 处理块:`GlobalLiterature.keywords.cast(JSONB).contains([t.text])`
- `_single_term_condition` 增加 OT 分支
#### P4-10: phraseto_tsquery 精确短语
- **问题**:精确短语 `"immune checkpoint"` 用 ILIKE `%immune checkpoint%` 实现,无法利用 GIN 索引,全表扫描
- **修复**`_field_condition` 的 `"all"` 字段精确短语路径从 ILIKE 改为 `search_tsv @@ phraseto_tsquery('english', term)`
- **限制**:通配符 `*` 时仍需 ILIKEtsvector 不支持截词)
### 引擎改进(1 项)
#### P4-9: `[PMC]` 搜索 SQL
- `_pubmed_conditions` 增加第 9 块:`pmc_id == term.text` 精确匹配
- `_single_term_condition` 增加 PMC 分支
---
## 第五轮:第 5 轮全面审计修复
**提交**`275a9f6`
**日期**2026-07-27
**数量**4 项
**触发**5 Agent 并行深度审计 + 第 2 轮规划核对
### 背景
第 4 轮后启动第 5 轮全面审计,5 个 agent 并行扫描 parser、engine、前端、文档。首先与第 2 轮审计计划(F1-F8)逐项核对,确认以下 8 项已在之前轮次完成:
| 计划 ID | 项目 | 完成轮次 | 状态 |
|---------|------|---------|------|
| F1 | `[MH:noexp]` 顶层支持 | 第 3/4 轮 | 已修复 |
| F2 | 组内 NOT De Morgan 律 | 第 2 轮 | 已验证正确 |
| F3 | `_parse_not_expr` NOT NOT 递归 | 第 4 轮 | 已修复 |
| F4 | SearchView Custom Range 日期 | 第 3 轮 | 已工作正常 |
| F5 | HomeView restoreFromUrl | 第 3 轮 | 已工作正常 |
| F6 | precision_mode 死代码 | 此前轮次 | 已移除 |
| F7 | is_oa 死字段 | 此前轮次 | 已注释 `unused` |
| F8 | `_expand_mesh_tag_ids` N+1 | 第 3/4 轮 | 已批量优化 |
实际在第 5 轮修复的 4 项均为 parser 边缘案例:
### P5-1: 尾部 NOT 导致 IndexError 降级
- **文件**`pubmed_query_parser.py` `_parse_not_expr` L498
- **问题**`cancer NOT` — 解析器 `_is_primary_start` 包含 `NOT`,隐式 AND 逻辑尝试将 NOT 作为新词开始,但 NOT 之后无 token → `peek()` 越界抛 `IndexError` → 整个查询降级为纯文本拆分,`NOT` 作为字面搜索词(影响极小但产生异常)
- **根因**`_parse_not_expr` 不检查是否已到 EOF
- **修复**:消费 NOT token 后立即检查 `peek().type == TokenType.EOF`,直接返回 `[]` 静默忽略
### P5-2: 单数字日期格式不被识别
- **文件**`pubmed_query_parser.py` `parse_pubmed_query` L679
- **问题**`2024-1-1[DP]` — tokeniser 的 DATE 模式只匹配 `\d{4}-\d{2}-\d{2}`,单数字月/日被解析为 `WORD('2024-1-1')` → `_dispatch_term` 设置 `date_from='2024-1-1'` → `date.fromisoformat()` 抛 `ValueError` → 日期条件被静默丢弃
- **根因**tokeniser 前缺少单数字日期归一化
- **修复**:在 `parse_pubmed_query` 的 NFKC 归一化后增加 `YYYY-M-D → YYYY-MM-DD` 正则替换
### P5-3: `is_pubmed_syntax` 不处理全角字符
- **文件**`pubmed_query_parser.py` `is_pubmed_syntax` L652
- **问题**:全角括号 `TI` 不被 `\[...\]` 识别 → 检出失败 → 走纯文本路径(解析器内 `parse_pubmed_query` 做 NFKC 但已不会进入)
- **根因**`is_pubmed_syntax` 未做 NFKC 归一化、与 `parse_pubmed_query` 行为不一致
- **修复**:函数开头增加 `query = unicodedata.normalize('NFKC', query)`
### P5-4: `extract_pubmed_query_for_prisma` 不能处理 `[Title/Article]`
- **文件**`pubmed_query_parser.py` `extract_pubmed_query_for_prisma` L706
- **问题**:归一化 regex `\[([\w:]+)\]` 不含 `/``[Title/Article]` 不被匹配、保持原文
- **根因**regex 字符类不含 `/`
- **修复**`[\w:]` → `[\w/:]`
---
## 各轮变更摘要
| 维度 | 第一轮 | 第二轮 | 第三轮 | 第四轮 | 第五轮 |
|------|--------|--------|--------|--------|--------|
| 修复数 | 34 | 8 | 14 | 11 | 4 |
| 后端文件变更 | 全部 | engine + parser | engine + parser + api | engine + parser | parser |
| 前端文件变更 | SearchView + HomeView + Card | SearchView + HomeView + Panel | SearchView + HomeView | 0 | 0 |
| 测试变更 | 新增 | +6 项 | 已有覆盖 | 0 | 0 |
| 新功能 | `[MH]`/`[EDAT]`/`[AD]`/`[LA]` 等 | — | — | `[GEN]`/`[PMC]`/`[Title/Abstract]` | — |
| 性质 | 从零搭建 | 审计修复 | 深度审计修复 | 字段补全 |
---
## 遗留限制
截至 2026-07-27,剩余 7 项已知限制:
| ID | 问题 | 原因 | 影响 |
|----|------|------|------|
| L1 | 混合 AND/OR 优先级(`A OR B AND C`) | 需 AST 重构,当前扁平列表无法保留嵌套结构 | 低(混合布尔极罕见) |
| L2 | Affiliation JSONB cast 假阳性 | 需独立 affiliation 列 + Alembic 迁移 + 重新填充 | 低 |
| L3 | retracted "yes"="only" | 命名语义,SQL 条件相同 | 无影响 |
| L4 | OR-mode NOT 检测不可靠 | `UnaryExpression + _sa_ops.inv` 不可靠用于复合 NOT | 低 |
| L6 | 字段标签 REF/ISBN 未注册 | 低使用频率或数据缺失 | 低 |
| L7 | GIN 索引缺失(基因/chemicals 等) | 需 DBA 操作,生产数据量大 | 中(大表性能) |
| L8 | `_dispatch_term` 回归不可见 | 需字段级测试 | 低 |
---
## 附录:测试覆盖统计
| 测试文件 | 用例数 | 范围 |
|---------|--------|------|
| `test_pubmed_query_parser.py` | ~40 | tokeniser、解析器、语法正确性 |
| `test_pubmed_search_integration.py` | ~60 | 字段映射、API 集成、前端格式 |
| `test_comprehensive_verify.py` | ~27 | 字段完整、NOT 语义、括号、日期 |
| **合计** | **214** | 全部通过 |