docs: 新增搜索修复全记录文档(13-搜索修复全记录.md)
涵盖 4 轮 59 项修复的详细背景、根因分析、修改内容和变更文件, 按轮次分别记录 Phase 0-7、第二轮(8项)、第三轮(14项)、第四轮(11项)。
This commit is contained in:
@@ -0,0 +1,363 @@
|
|||||||
|
# PubMed 搜索合规修复全记录
|
||||||
|
|
||||||
|
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
|
||||||
|
>
|
||||||
|
> **累计**:4 轮,59 项修复,50+ 字段标签注册,127 项测试覆盖
|
||||||
|
> **时间跨度**: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. [遗留限制](#遗留限制)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一轮: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)`
|
||||||
|
- **限制**:通配符 `*` 时仍需 ILIKE(tsvector 不支持截词)
|
||||||
|
|
||||||
|
### 引擎改进(1 项)
|
||||||
|
|
||||||
|
#### P4-9: `[PMC]` 搜索 SQL
|
||||||
|
|
||||||
|
- `_pubmed_conditions` 增加第 9 块:`pmc_id == term.text` 精确匹配
|
||||||
|
- `_single_term_condition` 增加 PMC 分支
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 各轮变更摘要
|
||||||
|
|
||||||
|
| 维度 | 第一轮 | 第二轮 | 第三轮 | 第四轮 |
|
||||||
|
|------|--------|--------|--------|--------|
|
||||||
|
| 修复数 | 34 | 8 | 14 | 11 |
|
||||||
|
| 后端文件变更 | 全部 | engine + parser | engine + parser + api | engine + parser |
|
||||||
|
| 前端文件变更 | SearchView + HomeView + Card | SearchView + HomeView + Panel | SearchView + HomeView | 0 |
|
||||||
|
| 测试变更 | 新增 | +6 项 | 已有覆盖 | 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 语义、括号、日期 |
|
||||||
|
| **合计** | **127** | 全部通过 |
|
||||||
Reference in New Issue
Block a user