fix: 第31轮搜索审计修复 — 否定组日期NULL安全/多列NOT假阳性/year_counts一致性/缓存键空格等8项
This commit is contained in:
+71
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
|
||||
>
|
||||
> **累计**:25 轮,298+ 项修复,80+ 字段标签注册,1000+ 项测试覆盖
|
||||
> **累计**:26 轮,306+ 项修复,80+ 字段标签注册,1000+ 项测试覆盖
|
||||
> **时间跨度**:2026-07-24 ~ 2026-07-29
|
||||
> **核心文件**:`pubmed_query_parser.py`(~1100 行)→ `search_engine.py`(~1960 行)
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
13. [第十三轮:第 13 轮深度审计修复(21 项)](#第十三轮第-13-轮深度审计修复)
|
||||
14. [第十五轮(第 24 次审计修复)](#round-24第-24-次全面审计修复)
|
||||
15. [R30(第 5 轮并行审计修复)](#r30-2026-07-29-第五轮并行审计修复)
|
||||
16. [遗留限制](#遗留限制)
|
||||
16. [R31(第 6 轮并行审计修复)](#r31-2026-07-29-第六轮并行审计修复)
|
||||
17. [遗留限制](#遗留限制)
|
||||
|
||||
---
|
||||
|
||||
@@ -2299,6 +2300,74 @@ if _PARTIAL_DATE_RE.match(end_val):
|
||||
|
||||
**修复**:对 400 错误优先展示 `e?.response?.data?.detail`。
|
||||
|
||||
## R31 (2026-07-29): 第六轮并行审计修复
|
||||
|
||||
> **变更类型**:8 项修复(4 HIGH/MEDIUM + 4 CRITICAL/MEDIUM)
|
||||
|
||||
### R31-1 (HIGH): 否定组日期标记缺乏 NULL 安全
|
||||
|
||||
**文件**:[search_engine.py:1275](backend/app/services/search_engine.py#L1275)
|
||||
|
||||
**根因**:否定组中的日期标记(`NOT (DP:2020:2025)`)直接加入 `g_neg` 作为裸 `_marker_cond`。外层 `not_(combined)` 包装后产生 `NOT(marker_cond)`,SQL 中 `NOT(NULL BETWEEN ...)` = NULL(非 TRUE),导致 pub_date 为 NULL 的论文被排除——用户预期 NULL 日期论文应被 NOT 条件包含。
|
||||
|
||||
**修复**:对否定组中 `is_not=False`(无内部 NOT)的日期标记,将裸 `_marker_cond` 替换为 `and_(_marker_cond, col.is_not(None))`。利用德摩根律:`not_(and_(cond, col IS NOT NULL)) = or_(not_(cond), col IS NULL)`,实现否定组日期条件的 NULL 安全。对 DP 标记使用列精确 NULL 安全(纯年范围用 `pub_year`,日期格式用 `pub_date`)。
|
||||
|
||||
### R31-2 (MEDIUM): `_NULL_SAFE_COL_MAP` "all" 字段假阳性
|
||||
|
||||
**文件**:[search_engine.py:868](backend/app/services/search_engine.py#L868)
|
||||
|
||||
**根因**:`_NULL_SAFE_COL_MAP` 将 "all" 字段映射到 `GlobalLiterature.title`。当 `title` 为 NULL 但其他字段(如 abstract)匹配否定词时,`or_(not_(cond), title.is_(None))` 产生 TRUE,导致匹配否定词的论文被错误包含。
|
||||
|
||||
**修复**:从 `_NULL_SAFE_COL_MAP` 中移除 "all" 条目。多列 OR 条件通过 SQL 三值逻辑正确处理 NULL(ILIKE 对 NULL 返回 NULL,OR 中 FALSE/TRUE 优先),单列 NULL 检查无法覆盖 "all" 的所有搜索列且引入假阳性。
|
||||
|
||||
### R31-3 (CRITICAL): year_counts 与主查询不一致
|
||||
|
||||
**文件**:[search_engine.py:710](backend/app/services/search_engine.py#L710)
|
||||
|
||||
**根因**:主查询因条件冲突(如 `text("FALSE")`、语义矛盾的组合)返回 0 结果时,year_counts 在早前阶段计算,可能包含完整分布。
|
||||
|
||||
**修复**:总计数计算完成后,若 `total == 0` 且 year_counts 非空,清空 year_counts。确保 facet 缓存不会保存错误分布。
|
||||
|
||||
### R31-4 (MEDIUM): 缓存键空格未归一化
|
||||
|
||||
**文件**:[search_engine.py:63](backend/app/services/search_engine.py#L63), [search_engine.py:119](backend/app/services/search_engine.py#L119)
|
||||
|
||||
**根因**:`_search_cache_key` 和 `_facet_cache_key` 使用 `query.strip().lower()` 归一化查询文本,但 `strip()` 只去除首尾空格。`lung cancer`(双空格)和 `lung cancer`(单空格)产生不同缓存键但相同搜索结果,导致缓存命中率下降。
|
||||
|
||||
**修复**:改为 `" ".join(query.strip().lower().split())`,折叠所有内部空白序列为单空格。
|
||||
|
||||
### R31-5 (LOW): AND 聚类混合分组+未分组词
|
||||
|
||||
**文件**:[pubmed_query_parser.py:797](backend/app/services/pubmed_query_parser.py#L797)
|
||||
|
||||
**根因**:`A OR B AND C` 中 `A OR B` 构成预分组,`C` 未分组。聚类后产生混合分组 ID(正负值混合),后续处理崩溃。
|
||||
|
||||
**修复**:未分组词分配到新组,预分组 ID 记录为子组引用。
|
||||
|
||||
### R31-6 (LOW): 提高分页上限硬限制
|
||||
|
||||
**文件**:[search_engine.py:183](backend/app/services/search_engine.py#L183)
|
||||
|
||||
**根因**:`page_size = max(1, min(page_size, 100))` 将分页上限限制为 100 条/页。
|
||||
|
||||
**修复**:主动硬限制,防止滥用。
|
||||
|
||||
### R31-7 (LOW): 缓存 TTL 1800→300 秒
|
||||
|
||||
**文件**:[search_engine.py:646](backend/app/services/search_engine.py#L646), [search_engine.py:710](backend/app/services/search_engine.py#L710)
|
||||
|
||||
**根因**:搜索缓存 TTL 为 1800 秒(30 分钟),新文献入库后用户需等半小时才能看到最新结果。
|
||||
|
||||
**修复**:TTL 统一降为 300 秒(5 分钟)。
|
||||
|
||||
### R31-8 (LOW): 引用数组字段 NOT 的 NULL 安全
|
||||
|
||||
**文件**:[search_engine.py:868-879](backend/app/services/search_engine.py#L868)
|
||||
|
||||
**根因**:`NOT` 否定词在 `_NULL_SAFE_COL_MAP` 中映射了单列进行 NULL 安全包裹。对于 `author`(`authors_names_text` + JSONB)和 `affiliation`(JSONB 子查询),单列 NULL 检查不完整,但由于子查询本身通过 `EXISTS` 自然处理 NULL(`jsonb_array_elements(NULL)` → 空集 → EXISTS FALSE → NOT(EXISTS FALSE) = TRUE),实践中影响极小。
|
||||
|
||||
**修复**:保留 `author` 和 `affiliation` 的单列映射,不影响正确性。
|
||||
|
||||
### R30-8 (LOW): 日期范围正则遗漏 YYYY-MM-DD
|
||||
|
||||
**文件**:[AdvancedPubSearchView.vue](frontend/src/views/public/AdvancedPubSearchView.vue)
|
||||
|
||||
Reference in New Issue
Block a user