fix: 第17轮搜索审计修复 — 缓存键中毒/_pubmed_conditions异常保护/引号短语exact强制
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

- P0-1 (HIGH): sub-path A query原地修改后缓存键未更新 → 重新计算
- P0-2 (MEDIUM): _pubmed_conditions无异常保护 → try/except降级flat text
- P0-3 (MEDIUM): 引号短语不强制exact_phrase → _phrase_terms_set跟踪
This commit is contained in:
34047007@qq.com
2026-07-28 15:12:13 +08:00
parent de1f4a4bb8
commit 5fa2fbec7b
2 changed files with 87 additions and 6 deletions
+43 -2
View File
@@ -279,9 +279,15 @@ class AdvancedSearchEngine:
) )
if has_pubmed_terms: if has_pubmed_terms:
_is_flat_text = False _is_flat_text = False
# P17: 异常保护 — _pubmed_conditions 内部异常时降级到 flat text
try:
conditions = await AdvancedSearchEngine._pubmed_conditions( conditions = await AdvancedSearchEngine._pubmed_conditions(
db, pp, conditions, db, pp, conditions,
) )
except Exception:
logger.exception("_pubmed_conditions failed, falling back to flat text")
_is_flat_text = True
conditions = []
# ─── 传统搜索路径(纯文本 / PubMed 退化) ─── # ─── 传统搜索路径(纯文本 / PubMed 退化) ───
if _is_flat_text and query.strip(): if _is_flat_text and query.strip():
@@ -316,6 +322,38 @@ class AdvancedSearchEngine:
query = re.sub(r'\b(AND|OR|NOT)\b', '', query) query = re.sub(r'\b(AND|OR|NOT)\b', '', query)
query = query.replace('"', '').replace('(', '').replace(')', '') query = query.replace('"', '').replace('(', '').replace(')', '')
query = ' '.join(query.split()) query = ' '.join(query.split())
# P17: query 被原地修改后,缓存键指向脏数据 → 重新计算
_search_cache_key = AdvancedSearchEngine._search_cache_key(
query, field, boolean, exact_phrase,
year_from, year_to, date_from, date_to,
journal_tiers, pub_types, tag_ids,
retracted, negative_result,
is_oa, language, languages, nlm_subsets,
page_size, sort, page,
has_abstract=has_abstract,
is_free_full_text=is_free_full_text,
has_full_text=has_full_text,
has_associated_data=has_associated_data,
species=species, sex=sex, age=age,
medline_only=medline_only,
exclude_preprints=exclude_preprints,
cursor_val=cursor_val,
cursor_id=cursor_id,
)
_facet_cache_key = AdvancedSearchEngine._facet_cache_key(
query, field, boolean, exact_phrase,
year_from, year_to, date_from, date_to,
journal_tiers, pub_types, tag_ids,
retracted, negative_result,
is_oa, language, languages, nlm_subsets,
has_abstract=has_abstract,
is_free_full_text=is_free_full_text,
has_full_text=has_full_text,
has_associated_data=has_associated_data,
species=species, sex=sex, age=age,
medline_only=medline_only,
exclude_preprints=exclude_preprints,
)
# 中文搜索:自动匹配 GlobalTag.name_zh → 注入 tag_ids,跳过 ILIKE # 中文搜索:自动匹配 GlobalTag.name_zh → 注入 tag_ids,跳过 ILIKE
import re as _re import re as _re
_CHINESE_RE = _re.compile(r'[一-鿿㐀-䶿豈-﫿]') _CHINESE_RE = _re.compile(r'[一-鿿㐀-䶿豈-﫿]')
@@ -337,6 +375,8 @@ class AdvancedSearchEngine:
_query_no_quotes = _phrase_pat.sub(' ', query) _query_no_quotes = _phrase_pat.sub(' ', query)
_rest = [t.strip().strip('"').strip("'") for t in _query_no_quotes.split() if t.strip()] _rest = [t.strip().strip('"').strip("'") for t in _query_no_quotes.split() if t.strip()]
terms = [p for p in _phrases if p.strip()] + [t for t in _rest if t not in _phrases] terms = [p for p in _phrases if p.strip()] + [t for t in _rest if t not in _phrases]
# P17: 记录引号短语,后续强制 exact=True
_phrase_terms_set = set(p.lower() for p in _phrases if p.strip())
# 单数字词:优先 PMID 精确匹配(unique index 5ms 返回) # 单数字词:优先 PMID 精确匹配(unique index 5ms 返回)
# 不是 PMID 时才回退到 ILIKE 兜底(DOI 片段等),不做 tsquery 避免 seq scan # 不是 PMID 时才回退到 ILIKE 兜底(DOI 片段等),不做 tsquery 避免 seq scan
numeric_terms = [t for t in terms if re.match(r'^\d{1,15}$', t)] numeric_terms = [t for t in terms if re.match(r'^\d{1,15}$', t)]
@@ -387,9 +427,10 @@ class AdvancedSearchEngine:
_cond_before = len(conditions) _cond_before = len(conditions)
if boolean == "and": if boolean == "and":
for term in text_terms: for term in text_terms:
conditions.append(AdvancedSearchEngine._field_condition(field, term, exact_phrase)) _exact = exact_phrase or term in _phrase_terms_set # P17: 引号短语强制 exact
conditions.append(AdvancedSearchEngine._field_condition(field, term, _exact))
else: else:
or_conds = [AdvancedSearchEngine._field_condition(field, t, exact_phrase) for t in text_terms] or_conds = [AdvancedSearchEngine._field_condition(field, t, exact_phrase or t in _phrase_terms_set) for t in text_terms]
conditions.append(or_(*or_conds)) conditions.append(or_(*or_conds))
# 将文本条件与 ATM 条件 OR 组合 # 将文本条件与 ATM 条件 OR 组合
+41 -1
View File
@@ -1210,7 +1210,7 @@
## 第十六轮:第 16 轮审计修复(4 项修复 + 3 项记录) ## 第十六轮:第 16 轮审计修复(4 项修复 + 3 项记录)
**日期**2026-07-29 **日期**2026-07-29
**提交**`(待推送)` **提交**`de1f4a4`
**数量**:4 项修复 + 3 项记录 **数量**:4 项修复 + 3 项记录
**触发**:用户第 11 次要求全面检查(Round 163 并行 agentNormal 搜索边缘、前端参数、NOT 检测) **触发**:用户第 11 次要求全面检查(Round 163 并行 agentNormal 搜索边缘、前端参数、NOT 检测)
**测试**1007 全部通过 + 前端 build 通过 **测试**1007 全部通过 + 前端 build 通过
@@ -1253,6 +1253,46 @@
--- ---
## 第十七轮:第 17 轮审计修复(3 项)
**日期**2026-07-29
**提交**`(待推送)`
**数量**3 项(1 HIGH + 2 MEDIUM
**触发**:用户第 12 次要求全面检查(Round 173 并行 agent`_is_flat_text` 降级路径、facet 一致性、前端参数映射)
**审计**:facet 一致性和缓存键验证通过,未发现问题
**测试**1007 全部通过 + 前端 build 通过
### P0-1 (HIGH): `_search_cache_key` 在 sub-path A 中被污染
- **文件**`search_engine.py:314-318`
- **根因**`_search_cache_key` 在第 186 行用原始 `query` 预计算。第 314-318 行在 sub-path A 中对 `query` 原地修改(剥离 field tags/布尔符/引号/括号),但缓存键未更新。后续请求命中此缓存时,返回的是剥离后的空查询结果。
- **触发条件**:仅当 `is_pubmed_syntax(query)=True` 但 `parse_pubmed_query(query)` 返回空有效字段时(如 `"NOT"`、`"AND OR"` 等无意义查询)。实际影响极小,但属于正确性 bug。
- **修复**:在 `query` 原地修改后重新调用 `_search_cache_key()` 和 `_facet_cache_key()`,确保缓存键反映剥离后的查询内容。
### P0-2 (MEDIUM): `_pubmed_conditions` 无异常保护
- **文件**`search_engine.py:282-284`
- **根因**`_pubmed_conditions()` 调用无 try/except。内部虽有零散异常处理,但 `AttributeError`/`TypeError` 等会传播到 `search()` 外 → 500 错误(`literature.py:330` 已有全局保护,但高级搜索引擎没有)。
- **修复**:包裹 try/except Exception,异常时 `logger.exception()` 并降级到 flat text 路径:`_is_flat_text = True; conditions = []`
### P0-3 (MEDIUM): 引号短语不强制 exact_phrase
- **文件**`search_engine.py:334-393`
- **根因**flat text 路径从查询中提取引号短语 `"lung cancer"` 作为独立词,但传入 `_field_condition(term, exact_phrase)` 时使用全局 `exact_phrase` 参数。`exact_phrase=False` 时,引号短语被拆散为 `lung AND cancer` 而非保持 `<->` 短语搜索。
- **修复**:记录引号短语集 `_phrase_terms_set`,传入 `_field_condition` 时:引号短语始终 `exact=True`,其他词使用全局 `exact_phrase`。
### 审计结果汇总
| 审计维度 | 结果 |
|---------|------|
| `_is_flat_text` 降级路径 | ✅ 缓存键中毒(P0-1)已修复、异常保护(P0-2)已添加、引号短语(P0-3)已修复 |
| Fragment/缓存完整性 | ✅ `parse_pubmed_query` 异常降级 clean、回退路径比普通搜索更全面 |
| `year_counts` facet 一致性 | ✅ 筛选条件与主查询完全一致、缓存键差异仅限于分页参数(设计意图)、无问题 |
| Facet 缓存键完备性 | ✅ `_facet_cache_key` 包含全部 26 个筛选参数,无缺失 |
| 前端参数映射 | ✅ SearchView.vue 发送全部 28 个参数、HomeView 参数子集缩小属设计意图、`is_oa` 后端功能被 `is_free_full_text` 覆盖、无实际 gap |
---
截至 2026-07-29,剩余 7 项已知限制: 截至 2026-07-29,剩余 7 项已知限制:
| ID | 问题 | 原因 | 影响 | | ID | 问题 | 原因 | 影响 |