fix: 第14轮搜索审计修复 — 非DP日期字段单年值失效/跨字段year_from覆盖等4项修复

HIGH (2): 非DP日期字段(EDAT/CRDT/MHDA/LR/DCOM/DEP)单年值未设置
          专用 *_from/*_to,EDAT/CRDT等列过滤静默失效;
          跨日期字段 year_from/year_to 互相覆盖,DP条件被丢弃
MINOR (2): MHDA/LR/DCOM/DEP 缺少 _PARTIAL_DATE_RE 分支;
           _single_term_condition 未处理括号组内日期字段
This commit is contained in:
34047007@qq.com
2026-07-28 13:21:24 +08:00
parent c5f2f27d7f
commit daf169d265
3 changed files with 97 additions and 14 deletions
+20 -12
View File
@@ -479,8 +479,8 @@ class PubmedQueryParser:
result.negated_date_ranges.add("DP") result.negated_date_ranges.add("DP")
elif term.field == "EDAT": elif term.field == "EDAT":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.edat_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.edat_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text): elif _PARTIAL_DATE_RE.match(term.text):
result.edat_from, result.edat_to = _expand_partial_date(term.text) result.edat_from, result.edat_to = _expand_partial_date(term.text)
else: else:
@@ -494,8 +494,8 @@ class PubmedQueryParser:
result.negated_date_ranges.add("EDAT") result.negated_date_ranges.add("EDAT")
elif term.field == "CRDT": elif term.field == "CRDT":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.crdt_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.crdt_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text): elif _PARTIAL_DATE_RE.match(term.text):
result.crdt_from, result.crdt_to = _expand_partial_date(term.text) result.crdt_from, result.crdt_to = _expand_partial_date(term.text)
else: else:
@@ -509,8 +509,10 @@ class PubmedQueryParser:
result.negated_date_ranges.add("CRDT") result.negated_date_ranges.add("CRDT")
elif term.field == "MHDA": elif term.field == "MHDA":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.mhda_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.mhda_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text):
result.mhda_from, result.mhda_to = _expand_partial_date(term.text)
else: else:
if _validate_date_str(term.text): if _validate_date_str(term.text):
result.mhda_from = term.text result.mhda_from = term.text
@@ -522,8 +524,10 @@ class PubmedQueryParser:
result.negated_date_ranges.add("MHDA") result.negated_date_ranges.add("MHDA")
elif term.field == "LR": elif term.field == "LR":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.lr_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.lr_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text):
result.lr_from, result.lr_to = _expand_partial_date(term.text)
else: else:
if _validate_date_str(term.text): if _validate_date_str(term.text):
result.lr_from = term.text result.lr_from = term.text
@@ -535,8 +539,10 @@ class PubmedQueryParser:
result.negated_date_ranges.add("LR") result.negated_date_ranges.add("LR")
elif term.field == "DCOM": elif term.field == "DCOM":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.dcom_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.dcom_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text):
result.dcom_from, result.dcom_to = _expand_partial_date(term.text)
else: else:
if _validate_date_str(term.text): if _validate_date_str(term.text):
result.dcom_from = term.text result.dcom_from = term.text
@@ -548,8 +554,10 @@ class PubmedQueryParser:
result.negated_date_ranges.add("DCOM") result.negated_date_ranges.add("DCOM")
elif term.field == "DEP": elif term.field == "DEP":
if term.text.isdigit() and len(term.text) == 4: if term.text.isdigit() and len(term.text) == 4:
result.year_from = int(term.text) result.dep_from = f"{term.text}-01-01"
result.year_to = int(term.text) result.dep_to = f"{term.text}-12-31"
elif _PARTIAL_DATE_RE.match(term.text):
result.dep_from, result.dep_to = _expand_partial_date(term.text)
else: else:
if _validate_date_str(term.text): if _validate_date_str(term.text):
result.dep_from = term.text result.dep_from = term.text
+37
View File
@@ -1357,6 +1357,43 @@ class AdvancedSearchEngine:
if field == "PMC": if field == "PMC":
return GlobalLiterature.pmc_id == term.text return GlobalLiterature.pmc_id == term.text
# P14: 日期字段在括号组内
if field in ("DP", "EDAT", "CRDT", "MHDA", "LR", "DCOM", "DEP"):
from app.services.pubmed_query_parser import _validate_date_str as _vds
text = term.text
if text.isdigit() and len(text) == 4:
year = int(text)
if field == "DP":
return and_(GlobalLiterature.pub_year >= year, GlobalLiterature.pub_year <= year)
col_map = {
"EDAT": GlobalLiterature.entrez_date,
"CRDT": GlobalLiterature.create_date,
"MHDA": GlobalLiterature.meshed_date,
"LR": GlobalLiterature.pubmed_revised,
"DCOM": GlobalLiterature.date_completed,
"DEP": GlobalLiterature.pub_date,
}
col = col_map[field]
from datetime import date as _d
return and_(col >= _d(year, 1, 1), col <= _d(year, 12, 31))
if _vds(text):
from datetime import date as _d
try:
d = _d.fromisoformat(text)
if field == "DP":
return GlobalLiterature.pub_date == d
col_map = {
"EDAT": GlobalLiterature.entrez_date,
"CRDT": GlobalLiterature.create_date,
"MHDA": GlobalLiterature.meshed_date,
"LR": GlobalLiterature.pubmed_revised,
"DCOM": GlobalLiterature.date_completed,
"DEP": GlobalLiterature.pub_date,
}
return col_map[field] == d
except ValueError:
pass
# 回退 # 回退
return AdvancedSearchEngine._field_condition("all", term.text, term.exact) return AdvancedSearchEngine._field_condition("all", term.text, term.exact)
+40 -2
View File
@@ -2,7 +2,7 @@
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。 > 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
> >
> **累计**13 轮,179 项修复,50+ 字段标签注册,1007 项测试覆盖,7 项已知限制 > **累计**14 轮,200 项修复,50+ 字段标签注册,1007 项测试覆盖,7 项已知限制
> **时间跨度**2026-07-24 ~ 2026-07-29 > **时间跨度**2026-07-24 ~ 2026-07-29
> **核心文件**`pubmed_query_parser.py`~850 行)→ `search_engine.py`~1360 行) > **核心文件**`pubmed_query_parser.py`~850 行)→ `search_engine.py`~1360 行)
@@ -23,7 +23,8 @@
11. [第十一轮:第 11 轮深度审计修复(16 项)](#第十一轮第-11-轮深度审计修复) 11. [第十一轮:第 11 轮深度审计修复(16 项)](#第十一轮第-11-轮深度审计修复)
12. [第十二轮:第 12 轮深度审计修复(21 项)](#第十二轮第-12-轮深度审计修复) 12. [第十二轮:第 12 轮深度审计修复(21 项)](#第十二轮第-12-轮深度审计修复)
13. [第十三轮:第 13 轮深度审计修复(21 项)](#第十三轮第-13-轮深度审计修复) 13. [第十三轮:第 13 轮深度审计修复(21 项)](#第十三轮第-13-轮深度审计修复)
14. [遗留限制](#遗留限制) 14. [第十四轮:第 14 轮深度审计修复(21 项)](#第十四轮第-14-轮深度审计修复)
15. [遗留限制](#遗留限制)
--- ---
@@ -1139,6 +1140,43 @@
--- ---
## 第十四轮:第 14 轮深度审计修复(21 项)
**日期**2026-07-29
**提交**`NEXT_COMMIT`
**数量**21 项(2 HIGH + 2 MINOR
**触发**:用户第 9 次要求全面检查
**测试**1007 全部通过 + 前端 build 通过
### BUG-1 (HIGH): 非 DP 日期字段单年值未设置专用 `*_from`/`*_to`
- **文件**`pubmed_query_parser.py:480-561`
- **根因**`2024[EDAT]`、`2024[CRDT]` 等非 DP 日期字段的单年值只设置了共享的 `year_from`/`year_to`,未设置专用的 `edat_from`/`edat_to`。`_pubmed_conditions`search_engine.py:1227-1246)的 section 6b 正确迭代 `DATE_FIELD_COLS` 并使用专用属性构建列条件,但解析器从未填充这些属性 → EDAT/CRDT/MHDA/LR/DCOM/DEP 的单年值过滤完全静默失效
- **影响**:用户输入 `2024[EDAT]` 期望按入库日期过滤,实际得到的是 `pub_year >= 2024`(近似日期,语义错误)。EDAT 列过滤完全 skipped
- **修复**:6 个非 DP 日期字段的单年值分支改为设置专用的 `*_from`=`YYYY-01-01` 和 `*_to`=`YYYY-12-31`,不再设置 `year_from`/`year_to`
### BUG-2 (HIGH): 跨日期字段 `year_from`/`year_to` 互相覆盖
- **文件**`pubmed_query_parser.py:480-561`
- **根因**`year_from`/`year_to` 是 `ParsedPubmedQuery` 的共享属性。`2024[DP] AND 2025[EDAT]` 中 DP 先设置 `year_from=2024`EDAT 后覆盖为 `year_from=2025` → DP 条件完全丢失。最终引擎只看到 `pub_year >= 2025`
- **影响**:用户查询 `2024[DP] AND 2025[EDAT]` 期望「2024年出版 AND 2025年入库」,实际得到「2025年出版」(DP 条件丢失)
- **修复**:非 DP 日期字段不再设置 `year_from`/`year_to`(仅设置专用字段)。DP 字段保持设置 `year_from`/`year_to`。引擎 section 6b 已通过专用字段正确生成 SQL 条件
### BUG-3 (MINOR): MHDA/LR/DCOM/DEP 缺少 `_PARTIAL_DATE_RE` 分支
- **文件**`pubmed_query_parser.py:510-561`
- **根因**4 个日期字段 MHDA/LR/DCOM/DEP 直接从年份检查跳到 else 分支,缺少 `elif _PARTIAL_DATE_RE.match(term.text)` 的展开步骤。`2024-02[MHDA]` 被降级为纯文本搜索而非展开为整月范围。DP/EDAT/CRDT 已有此分支
- **修复**:为 4 个字段各添加 `_PARTIAL_DATE_RE` 展开分支,使用各自的专用属性(`mhda_from/mhda_to` 等)
### BUG-4 (MINOR): `_single_term_condition` 未处理括号组内日期字段
- **文件**`search_engine.py:1358-1362`
- **根因**`_single_term_condition` 处理了所有 30+ 特殊字段(MH/PT/GR/SH/RN 等),但完全遗漏了日期字段(DP/EDAT/CRDT/MHDA/LR/DCOM/DEP)。括号组 `(2024[DP] OR 2025[DP])` 中的日期词回退到全文本 ILIKE `%2024%`
- **修复**:在回退前添加日期字段处理:4 位数年份展开为全年范围列条件,完整日期使用列等值条件。`DP → pub_year/pub_date``EDAT → entrez_date``CRDT → create_date``MHDA → meshed_date``LR → pubmed_revised``DCOM → date_completed``DEP → pub_date`
- **验证**`(2024[EDAT] OR 2025[EDAT])` 正确生成 `entrez_date 年内范围 OR` 条件
---
截至 2026-07-29,剩余 7 项已知限制: 截至 2026-07-29,剩余 7 项已知限制:
| ID | 问题 | 原因 | 影响 | | ID | 问题 | 原因 | 影响 |