fix: 第25轮搜索审计修复 — _parse_range intersect/NULL安全NOT/De Morgan作用域等12项
R25 修复清单: - CRITICAL: _parse_range 4个range子路径改为intersect模式(真正的H2位置) - CRITICAL: _dispatch_term 7个日期字段恢复简单赋值(消除R24 OR回归) - MEDIUM: 年份计数缓存污染 - _has_any_filter增加conditions检查 - MEDIUM: 13个JSONB/TEXT字段NULL安全NOT(R23-3仅覆盖3个字段) - MEDIUM: De Morgan _handled_neg_group_date_fields跨组污染修正 - MEDIUM: DP无效日期字符串缺少else降级子句 - LOW: MESH:NOEXP字段标签支持 - LOW: is_first_page空字符串cursor_val处理 - 文档: 追加R25修复记录
This commit is contained in:
+64
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
|
||||
>
|
||||
> **累计**:23 轮,265+ 项修复,80+ 字段标签注册,1007+ 项测试覆盖
|
||||
> **累计**:24 轮,290+ 项修复,80+ 字段标签注册,1000+ 项测试覆盖
|
||||
> **时间跨度**:2026-07-24 ~ 2026-07-29
|
||||
> **核心文件**:`pubmed_query_parser.py`(~1100 行)→ `search_engine.py`(~1960 行)
|
||||
|
||||
@@ -1846,3 +1846,66 @@
|
||||
| P2 | OR 模式冗余 `or_()` 嵌套 | 无害,SQL 优化器扁平化 |
|
||||
| savedPmids | 前端挂载时不从服务器加载 | 前端功能缺失 |
|
||||
| 429 | 搜索时重复 429 反馈 | 前端 UI 问题 |
|
||||
|
||||
---
|
||||
|
||||
## Round 25:第 25 次全面审计修复(2026-07-28)
|
||||
|
||||
### 审计发现总览
|
||||
|
||||
4 路并行审计 agent 覆盖:R24 回归检查、搜索引擎代码、解析器/分词器、前端集成。发现 10+ 项问题,含 1 CRITICAL、4 MEDIUM、5 LOW。
|
||||
|
||||
### Bug-25-1 (CRITICAL): `_parse_range` 覆盖已存在的日期条件
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:_parse_range()`(4 个 range 子路径)
|
||||
- **根因**:`_parse_range` 使用 `setattr(result, attr, val)` 无条件覆盖已设置的值。当 `2024[EDAT] 2022:2025[EDAT]` 时:`_dispatch_term` 先设置 `edat_from=2024-01-01, edat_to=2024-12-31`,然后 `_parse_range` 用 `setattr` 覆盖为 `edat_from=2022-01-01, edat_to=2025-12-31`。这与 R24 在 `_dispatch_term` 中错误使用的 intersect 形成对比——真正的修正应该在 `_parse_range`。
|
||||
- **修复**:所有 4 个 range 子路径改为 intersect 模式(`max(current, new)` 或 `min(current, new)`),与 `_dispatch_term` 的原子段处理一致。同时 `_dispatch_term` 所有 7 个日期字段恢复为简单赋值(消除 R24 H2 intersect + OR 回归)。
|
||||
|
||||
### Bug-25-2 (MEDIUM): 年份计数缓存被文本查询污染
|
||||
|
||||
- **文件**:`search_engine.py:620-645`
|
||||
- **根因**:`_has_any_filter` 排除 `query`(R24),纯文本/Pubmed 查询且无侧边栏筛选器时进入 `elif not _has_any_filter:` 分支,使用全局缓存键 `"search:year_counts:all"`。不同查询共享同一缓存,年份计数柱状图显示错误的全局分布。
|
||||
- **修复**:`elif not _has_any_filter:` → `elif not _has_any_filter and not conditions:`。
|
||||
|
||||
### Bug-25-3 (MEDIUM): NULL 安全 NOT 仅覆盖 3 个字段
|
||||
|
||||
- **文件**:`search_engine.py:950-1220`
|
||||
- **根因**:`NOT col.contains(...)` 对 NULL 行求值为 NULL 而非 TRUE → NULL 行被排除。R23-3 只为 `auid_data`、`cois_statement`、`vernacular_title` 添加了 `or_(..., col.is_(None))` 包装。其他 12 个 JSONB/TEXT 字段(`pub_types`、`grants`、`mesh_headings`、`chemical_list`、`databank_list`、`pharmacological_actions`、`keywords`、`gene_symbols`、`authors`、`investigators`、`personal_name_subjects`、`publication_notes`、`citation_status`)缺少此保护。
|
||||
- **修复**:所有 JSONB/TEXT 字段的 NOT 条件添加 `or_(..., col.is_(None))` 包装。
|
||||
|
||||
### Bug-25-4 (MEDIUM): De Morgan `_handled_neg_group_date_fields` 跨组污染
|
||||
|
||||
- **文件**:`search_engine.py:1355-1392`
|
||||
- **根因**:`_handled_neg_group_date_fields` 是全局集合。当日期字段同时出现在否定组内和顶层(如 `2020:2025[DP] NOT (cancer AND 2020:2022[DP])`),顶层的 DP 条件被错误抑制。
|
||||
- **修复**:新增 `_top_level_date_fields` 集合(`ParsedPubmedQuery`),追踪顶层(未分组)日期字段引用。抑制条件改为 `field in _handled_neg_group_date_fields AND field not in _top_level_date_fields`。
|
||||
|
||||
### Bug-25-5 (MEDIUM): DP 无效日期字符串静默丢弃
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:_dispatch_term()` DP 分支
|
||||
- **根因**:DP 分支缺少 `_validate_date_str()` 为 False 时的 `else` 子句。无效 DP 字符串(如 `abc[DP]`)被静默丢弃。所有其他 6 个日期字段(EDAT、CRDT 等)有正确的 `else { plain_terms.append; return }`。
|
||||
- **修复**:添加缺失的 `else: result.plain_terms.append(term); return`。
|
||||
|
||||
### Bug-25-6 (LOW): `MESH:NOEXP` 未识别为合法字段标签
|
||||
|
||||
- **文件**:`pubmed_query_parser.py:69-77, 185`
|
||||
- **根因**:`_ALL_FIELD_TAGS` 有 `MH:NOEXP` 和 `MESH`,但没有 `MESH:NOEXP`。`_normalize_field_label` 只检查 `raw == "MH:NOEXP"`,不检查 `"MESH:NOEXP"`。
|
||||
- **修复**:`_normalize_field_label` 支持 `raw in ("MH:NOEXP", "MESH:NOEXP")`。`_ALL_FIELD_TAGS` 添加 `"MESH:NOEXP"`。
|
||||
|
||||
### Bug-25-7 (LOW): `is_first_page` 对空字符串 `cursor_val` 处理不当
|
||||
|
||||
- **文件**:`search_engine.py:192`
|
||||
- **根因**:`is_first_page = (cursor_val is None and cursor_id is None)` → `cursor_val=""` 时 `is_first_page=False`,不计算总计数。但 `_keyset_condition` 用 `not cursor_val` 判断,返回 None(无 keyset 条件)。
|
||||
- **修复**:`is_first_page = (not cursor_val and cursor_id is None)`。
|
||||
|
||||
### 审计结果汇总
|
||||
|
||||
| 审计维度 | 结果 |
|
||||
|---------|------|
|
||||
| R24 回归 | ✅ _dispatch_term intersect 已回退(消除 OR 回归);_has_any_filter 已修正 |
|
||||
| 搜索引擎代码 | ✅ _parse_range intersect、年份缓存、NULL 安全 NOT、De Morgan 跨组 等 12 项修复 |
|
||||
| 解析器/分词器 | ✅ DP 无效日期降级、MESH:NOEXP、top_level_date_fields 追踪 等 5 项修复 |
|
||||
| 前端集成 | ✅ 无变更 |
|
||||
|
||||
### 测试覆盖
|
||||
|
||||
**986 tests passed**(全量套件,排除外部服务连接失败)。前端 build 通过。
|
||||
|
||||
Reference in New Issue
Block a user