fix: 第22轮搜索审计修复 — De Morgan语义/部分日期/has_not/custom URL等8项
Bug-R22-1 (MEDIUM): OR模式特殊字段NOT产生错误De Morgan语义 — 17处修正 Bug-R22-2 (MEDIUM): has_not不反映日期范围NOT — 添加_date_range_markers检查 Bug-R22-3 (MEDIUM): YYYY-MM[DP]未引号部分日期无法解析 — 预处理器标准化 Bug-R22-4 (MEDIUM): date_preset=custom URL恢复丢失年份范围 — 读取URL参数 Bug-R22-5 (MEDIUM): 错误状态下分页总数残留 — 搜索开始时重置total Bug-R22-6 (LOW): 搜索错误状态未在开始时清除 — 搜索开始时重置searchError Bug-R22-7 (LOW): DATE token不在未消耗标记恢复处理器中 — 添加DATE类型 Bug-R22-8 (LOW): 外部NOT组内is_not被忽略 — 保留t.is_not
This commit is contained in:
@@ -445,7 +445,7 @@ class PubmedQueryParser:
|
|||||||
# P12: has_not 同时检查分组内 NOT(如 NOT (a OR b))
|
# P12: has_not 同时检查分组内 NOT(如 NOT (a OR b))
|
||||||
result.has_not = any(t.is_not for t in _ungrouped) or any(
|
result.has_not = any(t.is_not for t in _ungrouped) or any(
|
||||||
t.is_not for g in result.groups for t in g
|
t.is_not for g in result.groups for t in g
|
||||||
)
|
) or any(t.is_not for t in result._date_range_markers)
|
||||||
result.not_terms = [t for t in _ungrouped if t.is_not]
|
result.not_terms = [t for t in _ungrouped if t.is_not]
|
||||||
for t in _ungrouped:
|
for t in _ungrouped:
|
||||||
self._dispatch_term(result, t)
|
self._dispatch_term(result, t)
|
||||||
@@ -453,7 +453,7 @@ class PubmedQueryParser:
|
|||||||
# P2-1: Handle unconsumed tokens (e.g., orphan text after RPAREN)
|
# P2-1: Handle unconsumed tokens (e.g., orphan text after RPAREN)
|
||||||
if self.pos < len(self.tokens) - 1:
|
if self.pos < len(self.tokens) - 1:
|
||||||
for t in self.tokens[self.pos:-1]: # exclude EOF token
|
for t in self.tokens[self.pos:-1]: # exclude EOF token
|
||||||
if t.type in (TokenType.WORD, TokenType.QUOTED, TokenType.NUMBER):
|
if t.type in (TokenType.WORD, TokenType.QUOTED, TokenType.NUMBER, TokenType.DATE):
|
||||||
text = t.value.strip('"') if t.type == TokenType.QUOTED else t.value
|
text = t.value.strip('"') if t.type == TokenType.QUOTED else t.value
|
||||||
result.plain_terms.append(Term(text=text, exact=(t.type == TokenType.QUOTED)))
|
result.plain_terms.append(Term(text=text, exact=(t.type == TokenType.QUOTED)))
|
||||||
|
|
||||||
@@ -1008,6 +1008,12 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
|
|||||||
lambda m: f'{m.group(1)}{m.group(2)}-{int(m.group(3)):02d}-{int(m.group(4)):02d}',
|
lambda m: f'{m.group(1)}{m.group(2)}-{int(m.group(3)):02d}-{int(m.group(4)):02d}',
|
||||||
query,
|
query,
|
||||||
)
|
)
|
||||||
|
# P5: Normalize YYYY-MM (partial month) to YYYY-MM-01 when followed by date field tag
|
||||||
|
query = re.sub(
|
||||||
|
r'(\b\d{4}-\d{2})(?!-\d)(?=\s*\[(?:DP|EDAT|DEP|CRDT|MHDA|LR|DCOM)\])',
|
||||||
|
r'\1-01',
|
||||||
|
query,
|
||||||
|
)
|
||||||
tokens = tokenise(query)
|
tokens = tokenise(query)
|
||||||
parser = PubmedQueryParser(tokens)
|
parser = PubmedQueryParser(tokens)
|
||||||
return parser.parse()
|
return parser.parse()
|
||||||
|
|||||||
@@ -941,7 +941,7 @@ class AdvancedSearchEngine:
|
|||||||
term_conditions.append(or_(*pos_conds))
|
term_conditions.append(or_(*pos_conds))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.pub_types.cast(JSONB).contains([t.text]) for t in neg]
|
neg_conds = [GlobalLiterature.pub_types.cast(JSONB).contains([t.text]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# 5b. [GR] [SH] [RN] [NM] [SI] [PA] → JSONB contains,支持 is_not
|
# 5b. [GR] [SH] [RN] [NM] [SI] [PA] → JSONB contains,支持 is_not
|
||||||
if pp.grant_terms:
|
if pp.grant_terms:
|
||||||
@@ -954,7 +954,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.grants.cast(JSONB).contains([{"grant_id": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.grants.cast(JSONB).contains([{"grant_id": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.subheading_terms:
|
if pp.subheading_terms:
|
||||||
pos = [t for t in pp.subheading_terms if not t.is_not]
|
pos = [t for t in pp.subheading_terms if not t.is_not]
|
||||||
neg = [t for t in pp.subheading_terms if t.is_not]
|
neg = [t for t in pp.subheading_terms if t.is_not]
|
||||||
@@ -965,7 +965,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.mesh_headings.cast(JSONB).contains([{"qualifiers": [t.text]}]) for t in neg]
|
neg_conds = [GlobalLiterature.mesh_headings.cast(JSONB).contains([{"qualifiers": [t.text]}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.registry_terms:
|
if pp.registry_terms:
|
||||||
pos = [t for t in pp.registry_terms if not t.is_not]
|
pos = [t for t in pp.registry_terms if not t.is_not]
|
||||||
neg = [t for t in pp.registry_terms if t.is_not]
|
neg = [t for t in pp.registry_terms if t.is_not]
|
||||||
@@ -976,7 +976,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.chemical_list.cast(JSONB).contains([{"registry_number": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.chemical_list.cast(JSONB).contains([{"registry_number": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.substance_terms:
|
if pp.substance_terms:
|
||||||
pos = [t for t in pp.substance_terms if not t.is_not]
|
pos = [t for t in pp.substance_terms if not t.is_not]
|
||||||
neg = [t for t in pp.substance_terms if t.is_not]
|
neg = [t for t in pp.substance_terms if t.is_not]
|
||||||
@@ -987,7 +987,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.chemical_list.cast(JSONB).contains([{"name": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.chemical_list.cast(JSONB).contains([{"name": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.databank_terms:
|
if pp.databank_terms:
|
||||||
pos = [t for t in pp.databank_terms if not t.is_not]
|
pos = [t for t in pp.databank_terms if not t.is_not]
|
||||||
neg = [t for t in pp.databank_terms if t.is_not]
|
neg = [t for t in pp.databank_terms if t.is_not]
|
||||||
@@ -998,7 +998,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.databank_list.cast(JSONB).contains([{"accession_numbers": [t.text]}]) for t in neg]
|
neg_conds = [GlobalLiterature.databank_list.cast(JSONB).contains([{"accession_numbers": [t.text]}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# 5c. [PA] → pharmacological_actions JSONB contains (by name or ui),支持 is_not
|
# 5c. [PA] → pharmacological_actions JSONB contains (by name or ui),支持 is_not
|
||||||
if pp.pharmaco_terms:
|
if pp.pharmaco_terms:
|
||||||
@@ -1020,7 +1020,7 @@ class AdvancedSearchEngine:
|
|||||||
)
|
)
|
||||||
for t in neg
|
for t in neg
|
||||||
]
|
]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# P4: [OT] → keywords JSONB contains(不再映射到 all)
|
# P4: [OT] → keywords JSONB contains(不再映射到 all)
|
||||||
if pp.ot_terms:
|
if pp.ot_terms:
|
||||||
@@ -1033,7 +1033,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.keywords.cast(JSONB).contains([t.text]) for t in neg]
|
neg_conds = [GlobalLiterature.keywords.cast(JSONB).contains([t.text]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# P4: [GEN] → gene_symbols JSONB contains
|
# P4: [GEN] → gene_symbols JSONB contains
|
||||||
if pp.gene_terms:
|
if pp.gene_terms:
|
||||||
@@ -1046,7 +1046,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.gene_symbols.cast(JSONB).contains([t.text]) for t in neg]
|
neg_conds = [GlobalLiterature.gene_symbols.cast(JSONB).contains([t.text]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# 5d. [ED] [IR] [PS] [PUBN] [AUID] [COIS] [TT] → 新增字段搜索,支持 is_not
|
# 5d. [ED] [IR] [PS] [PUBN] [AUID] [COIS] [TT] → 新增字段搜索,支持 is_not
|
||||||
if pp.ed_terms:
|
if pp.ed_terms:
|
||||||
@@ -1059,7 +1059,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.authors.cast(JSONB).contains([{"type": "editor", "family": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.authors.cast(JSONB).contains([{"type": "editor", "family": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.investigator_terms:
|
if pp.investigator_terms:
|
||||||
pos = [t for t in pp.investigator_terms if not t.is_not]
|
pos = [t for t in pp.investigator_terms if not t.is_not]
|
||||||
neg = [t for t in pp.investigator_terms if t.is_not]
|
neg = [t for t in pp.investigator_terms if t.is_not]
|
||||||
@@ -1070,7 +1070,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.investigators.cast(JSONB).contains([{"family": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.investigators.cast(JSONB).contains([{"family": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.personal_name_terms:
|
if pp.personal_name_terms:
|
||||||
pos = [t for t in pp.personal_name_terms if not t.is_not]
|
pos = [t for t in pp.personal_name_terms if not t.is_not]
|
||||||
neg = [t for t in pp.personal_name_terms if t.is_not]
|
neg = [t for t in pp.personal_name_terms if t.is_not]
|
||||||
@@ -1081,7 +1081,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.personal_name_subjects.cast(JSONB).contains([{"family": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.personal_name_subjects.cast(JSONB).contains([{"family": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.pubnote_terms:
|
if pp.pubnote_terms:
|
||||||
pos = [t for t in pp.pubnote_terms if not t.is_not]
|
pos = [t for t in pp.pubnote_terms if not t.is_not]
|
||||||
neg = [t for t in pp.pubnote_terms if t.is_not]
|
neg = [t for t in pp.pubnote_terms if t.is_not]
|
||||||
@@ -1092,7 +1092,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [cast(GlobalLiterature.publication_notes, String).ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
neg_conds = [cast(GlobalLiterature.publication_notes, String).ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.auid_terms:
|
if pp.auid_terms:
|
||||||
pos = [t for t in pp.auid_terms if not t.is_not]
|
pos = [t for t in pp.auid_terms if not t.is_not]
|
||||||
neg = [t for t in pp.auid_terms if t.is_not]
|
neg = [t for t in pp.auid_terms if t.is_not]
|
||||||
@@ -1103,7 +1103,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.auid_data.cast(JSONB).contains([{"value": t.text}]) for t in neg]
|
neg_conds = [GlobalLiterature.auid_data.cast(JSONB).contains([{"value": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.cois_terms:
|
if pp.cois_terms:
|
||||||
pos = [t for t in pp.cois_terms if not t.is_not]
|
pos = [t for t in pp.cois_terms if not t.is_not]
|
||||||
neg = [t for t in pp.cois_terms if t.is_not]
|
neg = [t for t in pp.cois_terms if t.is_not]
|
||||||
@@ -1114,7 +1114,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.cois_statement.ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
neg_conds = [GlobalLiterature.cois_statement.ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
if pp.tt_terms:
|
if pp.tt_terms:
|
||||||
pos = [t for t in pp.tt_terms if not t.is_not]
|
pos = [t for t in pp.tt_terms if not t.is_not]
|
||||||
neg = [t for t in pp.tt_terms if t.is_not]
|
neg = [t for t in pp.tt_terms if t.is_not]
|
||||||
@@ -1125,7 +1125,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.vernacular_title.ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
neg_conds = [GlobalLiterature.vernacular_title.ilike(f"%{_escape_ilike(t.text)}%") for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# P1-2: [SB] Subset
|
# P1-2: [SB] Subset
|
||||||
# medline[SB] → citation_status='medline'(记录级)
|
# medline[SB] → citation_status='medline'(记录级)
|
||||||
@@ -1164,7 +1164,7 @@ class AdvancedSearchEngine:
|
|||||||
]))
|
]))
|
||||||
if neg:
|
if neg:
|
||||||
neg_conds = [GlobalLiterature.citation_status == t.text.lower() for t in neg]
|
neg_conds = [GlobalLiterature.citation_status == t.text.lower() for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
term_conditions.extend(not_(c) for c in neg_conds)
|
||||||
|
|
||||||
# P1-2: [UID] → PMID 优先,兜底 DOI
|
# P1-2: [UID] → PMID 优先,兜底 DOI
|
||||||
if pp.uid_terms:
|
if pp.uid_terms:
|
||||||
@@ -1201,7 +1201,8 @@ class AdvancedSearchEngine:
|
|||||||
cond = await AdvancedSearchEngine._single_term_condition(db, t)
|
cond = await AdvancedSearchEngine._single_term_condition(db, t)
|
||||||
if cond is not None:
|
if cond is not None:
|
||||||
if _negated:
|
if _negated:
|
||||||
g_neg.append(cond)
|
# R22: preserve inner is_not when group is externally negated
|
||||||
|
g_neg.append(not_(cond) if t.is_not else cond)
|
||||||
elif t.is_not:
|
elif t.is_not:
|
||||||
g_neg.append(not_(cond))
|
g_neg.append(not_(cond))
|
||||||
else:
|
else:
|
||||||
|
|||||||
+69
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
|
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
|
||||||
>
|
>
|
||||||
> **累计**:21 轮,235 项修复,80+ 字段标签注册,1007+ 项测试覆盖,7 项已知限制
|
> **累计**:22 轮,248 项修复,80+ 字段标签注册,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 行)
|
||||||
|
|
||||||
@@ -1588,6 +1588,74 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 第二十二轮:第 22 轮审计修复(8 项)
|
||||||
|
|
||||||
|
**日期**:2026-07-29
|
||||||
|
**提交**:`c68aa06..`(第 21 轮后追加)
|
||||||
|
**数量**:8 项(5 MEDIUM + 3 LOW)
|
||||||
|
**触发**:用户第 17 次要求全面检查(第 22 轮,4 并行审计 agent)
|
||||||
|
**测试**:1007+ 全部通过 + 前端 build 通过
|
||||||
|
|
||||||
|
### Bug-R22-1 (MEDIUM): OR 模式下特殊字段 NOT 产生错误 De Morgan 语义
|
||||||
|
|
||||||
|
- **文件**:`search_engine.py:944-1167`
|
||||||
|
- **根因**:`term_conditions.append(not_(or_(*neg_conds)))` 在 OR 模式下产生 `NOT(A OR B)` = `NOT A AND NOT B`。正确应为 `or_(not_(A), not_(B))` = `NOT A OR NOT B`。
|
||||||
|
- **影响**:`NOT "Review"[PT] OR NOT "Clinical Trial"[PT]` 搜索结果过窄。
|
||||||
|
- **修复**:将全部 17 处 `term_conditions.append(not_(or_(*neg_conds)))` 改为 `term_conditions.extend(not_(c) for c in neg_conds)`。AND 模式下语义等价,OR/mixed 模式语义修正。
|
||||||
|
|
||||||
|
### Bug-R22-2 (MEDIUM): `has_not` 不反映日期范围 NOT
|
||||||
|
|
||||||
|
- **文件**:`pubmed_query_parser.py:446`
|
||||||
|
- **根因**:`_ungrouped` 过滤掉 `_is_range_end=True` 的标记,`has_not` 不检查 `_date_range_markers`。
|
||||||
|
- **修复**:`has_not` 额外检查 `result._date_range_markers`。
|
||||||
|
|
||||||
|
### Bug-R22-3 (MEDIUM): `YYYY-MM[DP]` 未引号部分日期无法解析
|
||||||
|
|
||||||
|
- **文件**:`pubmed_query_parser.py:1010-1015`
|
||||||
|
- **根因**:`2024-01[DP]` 分词为 `NUMBER(2024) WORD(-01) FIELD([DP])`。`-01` 不匹配 `_PARTIAL_DATE_RE`。
|
||||||
|
- **修复**:预处理器将 `YYYY-MM[date_field]` 标准化为 `YYYY-MM-01[date_field]`。
|
||||||
|
|
||||||
|
### Bug-R22-4 (MEDIUM): `date_preset=custom` URL 恢复丢失年份范围
|
||||||
|
|
||||||
|
- **文件**:`SearchView.vue:393-396`
|
||||||
|
- **根因**:`restoreFromQuery` 在 custom 分支无条件清空 yearFromStr/yearToStr。
|
||||||
|
- **修复**:custom 分支读取 URL 中的 `year_from/year_to`。
|
||||||
|
|
||||||
|
### Bug-R22-5 (MEDIUM): 错误状态下分页总数残留
|
||||||
|
|
||||||
|
- **文件**:`SearchView.vue:274-276`
|
||||||
|
- **根因**:catch 块清空 results 但不清除 total。
|
||||||
|
- **修复**:搜索开始时重置 `total.value = 0`。
|
||||||
|
|
||||||
|
### Bug-R22-6 (LOW): 搜索错误状态未在开始时清除
|
||||||
|
|
||||||
|
- **文件**:`SearchView.vue:274-276`
|
||||||
|
- **根因**:searchError 仅成功后清空。
|
||||||
|
- **修复**:搜索开始时设置 `searchError.value = ''`。
|
||||||
|
|
||||||
|
### Bug-R22-7 (LOW): DATE token 不在未消耗标记恢复处理器中
|
||||||
|
|
||||||
|
- **文件**:`pubmed_query_parser.py:456`
|
||||||
|
- **根因**:未消耗标记处理器仅捕获 WORD/QUOTED/NUMBER。
|
||||||
|
- **修复**:添加 `TokenType.DATE`。
|
||||||
|
|
||||||
|
### Bug-R22-8 (LOW): 外部 NOT 组内 `is_not` 被忽略
|
||||||
|
|
||||||
|
- **文件**:`search_engine.py:1203`
|
||||||
|
- **根因**:`NOT (A OR NOT B)` → `not_(or_(A, B))`,内部 NOT B 被吞掉。
|
||||||
|
- **修复**:组内保留 `t.is_not`。
|
||||||
|
|
||||||
|
### 审计结果汇总
|
||||||
|
|
||||||
|
| 审计维度 | 结果 |
|
||||||
|
|---------|------|
|
||||||
|
| R21 回归 | ✅ 无回归 |
|
||||||
|
| 搜索引擎代码 | ✅ De Morgan 语义 17 处修正、negated 组内 is_not |
|
||||||
|
| 解析器/分词器 | ✅ YYYY-MM 预处理器、has_not 日期范围、DATE 恢复 |
|
||||||
|
| 前端集成 | ✅ custom URL 恢复、total/error 清理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 附录:测试覆盖统计
|
## 附录:测试覆盖统计
|
||||||
|
|
||||||
| 测试文件 | 用例数 | 范围 |
|
| 测试文件 | 用例数 | 范围 |
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ const { page, total, goToPage } = usePagination({
|
|||||||
const gen = ++searchGeneration.value
|
const gen = ++searchGeneration.value
|
||||||
loading.value = true
|
loading.value = true
|
||||||
searched.value = true
|
searched.value = true
|
||||||
|
searchError.value = ''
|
||||||
|
total.value = 0
|
||||||
try {
|
try {
|
||||||
if (p === 1 && query.value.trim()) {
|
if (p === 1 && query.value.trim()) {
|
||||||
trackAction('search', 'search', query.value.trim(), { sort: sort.value })
|
trackAction('search', 'search', query.value.trim(), { sort: sort.value })
|
||||||
@@ -392,8 +394,13 @@ function restoreFromQuery() {
|
|||||||
if (route.query.sort && VALID_SORTS.has(String(route.query.sort))) sort.value = String(route.query.sort)
|
if (route.query.sort && VALID_SORTS.has(String(route.query.sort))) sort.value = String(route.query.sort)
|
||||||
if (route.query.date_preset && ['1y','5y','10y','custom'].includes(String(route.query.date_preset))) {
|
if (route.query.date_preset && ['1y','5y','10y','custom'].includes(String(route.query.date_preset))) {
|
||||||
datePreset.value = String(route.query.date_preset)
|
datePreset.value = String(route.query.date_preset)
|
||||||
|
if (datePreset.value === 'custom') {
|
||||||
|
if (route.query.year_from) yearFromStr.value = String(route.query.year_from)
|
||||||
|
if (route.query.year_to) yearToStr.value = String(route.query.year_to)
|
||||||
|
} else {
|
||||||
yearFromStr.value = ''
|
yearFromStr.value = ''
|
||||||
yearToStr.value = ''
|
yearToStr.value = ''
|
||||||
|
}
|
||||||
} else if (route.query.date_from || route.query.date_to) {
|
} else if (route.query.date_from || route.query.date_to) {
|
||||||
datePreset.value = null
|
datePreset.value = null
|
||||||
if (route.query.date_from) {
|
if (route.query.date_from) {
|
||||||
|
|||||||
Reference in New Issue
Block a user