fix: PubMed搜索合规 — 34项修复 + query_expansion UnboundLocalError + HomeView precision_mode残留
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

Batch1 — 解析器 (pubmed_query_parser.py)
- P0-1: 未知字段标签降级为 WORD token 而非 ParseError
- P2-1: 增加未消费 token 检查
- P2-2: PRISMA 字段标签正则 [\w:+] → [\w:]+
- P2-3: Unicode NFKC 规格化输入
- P2-4: re.ASCII 防止 Unicode 数字匹配
- P2-9: 移除重复 dataclass 字段

Batch2 — 搜索引擎 (search_engine.py)
- P1-1: 批量 PMID 查询替代 N+1 循环
- P1-2: isdigit() → isdecimal()
- P1-5: 移除 precision_mode 参数
- P2-5: 移除死代码
- P2-6: 统一 _CHINESE_RE 正则

Batch3 — ATM 引擎 (query_expansion.py)
- P0-4: name_zh ILIKE 中文回退 + _find_mesh_tags 中文降级
- P1-3: name_en ILIKE 加 LIMIT 100

Batch4 — API 层 (features.py)
- P1-5: 移除 precision_mode 请求字段
- P1-11: 增加 logging
- P2-8: NLM_SUBSET_LABELS f-string 安全注释
- P2-12: split 校验器近似性注释

Batch5 — SearchView.vue
- P0-5a/b/c: 日期修复(UTC 方法、互斥逻辑、restoreFromQuery 合并)
- P2-10: 筛选模态关闭时重搜
- P3-1: 搜索框 aria-label

Batch6 — HomeView.vue
- P1-10: URL date → date_from/date_to
- P2-11: clearSearch 清空 feedItems 并重加载

Batch7 — LiteratureCard.vue
- P1-6: 字段标签正则 [\w-]+ → [\w:-]+
- P1-7: terms 切片限制 20 项防 ReDoS

后修复:
- query_expansion.py _find_partial_mesh_tags UnboundLocalError(单非中文词未初始化 tag_ids)
- HomeView.vue handleAdvancedSearch precision_mode 残留引用
This commit is contained in:
34047007@qq.com
2026-07-27 09:45:17 +08:00
parent 43392438c8
commit 723c4fc5c9
8 changed files with 129 additions and 81 deletions
+24 -13
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
import re
import unicodedata
from dataclasses import dataclass, field
from enum import Enum, auto
@@ -127,7 +128,7 @@ _TOKEN_PATTERNS: list[tuple[TokenType, str]] = [
_TOKEN_RE = re.compile(
'|'.join(f'(?P<{t.name}>{p})' for t, p in _TOKEN_PATTERNS),
re.IGNORECASE,
re.IGNORECASE | re.ASCII,
)
@@ -140,7 +141,11 @@ def tokenise(query: str) -> list[Token]:
ttype = TokenType[name]
if ttype == TokenType.UNKNOWN_FIELD:
fname = value.strip('[]').upper()
raise ParseError(f"不认识字段标签 [{fname}],降级为简单文本搜索")
# P0-1: 不认识字段标签时降级为 WORD,不终止解析
tokens.append(Token(TokenType.WORD, value.strip('[]')))
if len(tokens) > MAX_TERMS:
raise ParseError(f"查询词过多(超过 {MAX_TERMS} 个),降级为简单文本搜索")
continue
tokens.append(Token(ttype, value))
if len(tokens) > MAX_TERMS:
raise ParseError(f"查询词过多(超过 {MAX_TERMS} 个),降级为简单文本搜索")
@@ -169,11 +174,6 @@ class ParsedPubmedQuery:
tiab_terms: list[Term] = field(default_factory=list) # [TIAB]
author_terms: list[Term] = field(default_factory=list) # [AU]
journal_terms: list[Term] = field(default_factory=list) # [TA]
mesh_terms: list[str] = field(default_factory=list) # [MH]
majr_terms: list[str] = field(default_factory=list) # [MAJR]
pub_types: list[str] = field(default_factory=list) # [PT]
doi_terms: list[str] = field(default_factory=list) # [DOI]
pmid_terms: list[int] = field(default_factory=list) # [PMID]
affiliation_terms: list[Term] = field(default_factory=list) # [AD]
language_terms: list[Term] = field(default_factory=list) # [LA]
volume_terms: list[Term] = field(default_factory=list) # [VI]
@@ -275,10 +275,8 @@ class PubmedQueryParser:
"""入口:解析完整的查询字符串。"""
result = ParsedPubmedQuery()
self._depth = 0 # 括号嵌套深度计数器
try:
terms = self._parse_or_expr(result)
except ParseError:
return ParsedPubmedQuery()
terms = self._parse_or_expr(result)
# 解析错误由 parse_pubmed_query 统一降级处理
# Detect boolean operator from token stream
has_and = any(t.type == TokenType.AND for t in self.tokens)
@@ -296,6 +294,13 @@ class PubmedQueryParser:
for t in _ungrouped:
self._dispatch_term(result, t)
# P2-1: Handle unconsumed tokens (e.g., orphan text after RPAREN)
if self.pos < len(self.tokens) - 1:
for t in self.tokens[self.pos:-1]: # exclude EOF token
if t.type in (TokenType.WORD, TokenType.QUOTED, TokenType.NUMBER):
text = t.value.strip('"') if t.type == TokenType.QUOTED else t.value
result.plain_terms.append(Term(text=text, exact=(t.type == TokenType.QUOTED)))
return result
def _dispatch_term(self, result: ParsedPubmedQuery, term: Term) -> None:
@@ -574,6 +579,8 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
return ParsedPubmedQuery()
try:
# P2-3: Unicode normalization — strip zero-width chars, normalize fullwidth digits
query = unicodedata.normalize('NFKC', query)
# 将 YYYY/MM/DD 格式的日期分隔符统一为 YYYY-MM-DD,使 tokeniser 正确识别为 DATE
import re as _re
query = _re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', query)
@@ -581,7 +588,11 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
parser = PubmedQueryParser(tokens)
return parser.parse()
except (ParseError, IndexError, ValueError):
return ParsedPubmedQuery()
# P0-1: 降级时返回原始查询作为 plain_terms,不丢失用户输入
degraded = ParsedPubmedQuery()
for t in query.strip().split():
degraded.plain_terms.append(Term(text=t))
return degraded
def extract_pubmed_query_for_prisma(query: str) -> tuple[str, list[str]]:
@@ -598,7 +609,7 @@ def extract_pubmed_query_for_prisma(query: str) -> tuple[str, list[str]]:
# 标准化:统一字段大写
normalized = re.sub(
r'\[(\w+)\]',
r'\[([\w:]+)\]',
lambda m: f'[{m.group(1).upper()}]',
query,
)