fix: 第七轮深度搜索审计修复 — 20项
- HIGH: isdecimal() → re.match() for PMID (non-ASCII digit crash) - HIGH: boolean_operator 应用到字段分组和无标签词(AND/OR 错误) - HIGH: Cursor 分页 pub_date 优先(article_date 导致数据错位) - Parser: parse 异常处理器清理字段标签/布尔符/引号 - Parser: _parse_range date:year 反向交换(第4分支) - Parser: MAX_TERMS 保护、TITLE/ABSTRACT 大写键补充 - Engine: best_match 排序剥离字段标签 - Engine: year_from/year_to is not None(年份=0 被 falsy 跳过) - Engine: _single_term_condition SB 字段全量分发 - Engine: journal_tiers/nlm_subsets 空条件预警(0结果保障) - Engine: ATM 展开和 _expand_mesh_tag_ids pass → logger.exception - Engine: 降级 regex [\w/: -] 兼容 [MH:noexp] 冒号 - Frontend: syncSearchToUrl 移到 finally 块 - Frontend: 年份滑块/resetAllFilters 清除 URL 日期 - Frontend: Null cursor 日期安全守卫 - Docs: 新增 P7-1~P7-20 记录,测试数更新至 276
This commit is contained in:
@@ -621,6 +621,8 @@ class PubmedQueryParser:
|
||||
start_val, end_val = end_val, start_val
|
||||
elif _start_is_digit and not _end_is_digit and int(start_val) > int(end_val[:4]):
|
||||
start_val, end_val = end_val, start_val
|
||||
elif not _start_is_digit and _end_is_digit and int(start_val[:4]) > int(end_val):
|
||||
start_val, end_val = end_val, start_val
|
||||
# 确定两端是否是 4 位年份
|
||||
_start_is_year = start_val.isdigit() and len(start_val) == 4
|
||||
_end_is_year = end_val.isdigit() and len(end_val) == 4
|
||||
@@ -717,10 +719,15 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
|
||||
parser = PubmedQueryParser(tokens)
|
||||
return parser.parse()
|
||||
except (ParseError, IndexError, ValueError):
|
||||
# P0-1: 降级时返回原始查询作为 plain_terms,不丢失用户输入
|
||||
# P0-1: 降级时清理查询中的 [field] 标签、布尔符、引号和括号
|
||||
degraded = ParsedPubmedQuery()
|
||||
for t in query.strip().split():
|
||||
degraded.plain_terms.append(Term(text=t))
|
||||
import re as _degrade_re
|
||||
_clean = _degrade_re.sub(r'\[[\w/: -]+\]', '', query)
|
||||
_clean = _degrade_re.sub(r'\b(AND|OR|NOT)\b', '', _clean)
|
||||
_clean = _clean.replace('"', '').replace('(', '').replace(')', '')
|
||||
for t in _clean.split():
|
||||
if t.strip():
|
||||
degraded.plain_terms.append(Term(text=t.strip()))
|
||||
return degraded
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async def _find_mesh_tags(db: AsyncSession, query: str) -> list[UUID]:
|
||||
stmt = select(GlobalTag.id).where(
|
||||
GlobalTag.source.in_(["mesh", "manual"]),
|
||||
GlobalTag.name_zh.ilike(like_pattern),
|
||||
)
|
||||
).limit(100)
|
||||
rows = await db.execute(stmt)
|
||||
for (tid,) in rows:
|
||||
if tid not in seen:
|
||||
|
||||
@@ -219,7 +219,7 @@ class AdvancedSearchEngine:
|
||||
or _pubmed_parsed.year_from or _pubmed_parsed.year_to)
|
||||
]):
|
||||
# 解析失败但检测到 PubMed 语法 — 擦除 [field] 标签、布尔符、引号
|
||||
query = re.sub(r'\[[\w/-]+\]', '', query) # P5: [\w/-] 覆盖 [Title/Abstract]
|
||||
query = re.sub(r'\[[\w/: -]+\]', '', query) # P5: [\w/: -] 覆盖 [Title/Abstract] 和 [MH:noexp]
|
||||
query = re.sub(r'\b(AND|OR|NOT)\b', '', query)
|
||||
query = query.replace('"', '').replace('(', '').replace(')', '')
|
||||
query = ' '.join(query.split())
|
||||
@@ -245,8 +245,8 @@ class AdvancedSearchEngine:
|
||||
terms = [p for p in _phrases if p.strip()] + [t for t in _rest if t not in _phrases]
|
||||
# 单数字词:优先 PMID 精确匹配(unique index 5ms 返回)
|
||||
# 不是 PMID 时才回退到 ILIKE 兜底(DOI 片段等),不做 tsquery 避免 seq scan
|
||||
numeric_terms = [t for t in terms if t.isdecimal() and len(t) <= 15]
|
||||
text_terms = [t for t in terms if not (t.isdecimal() and len(t) <= 15)]
|
||||
numeric_terms = [t for t in terms if re.match(r'^\d{1,15}$', t)]
|
||||
text_terms = [t for t in terms if not re.match(r'^\d{1,15}$', t)]
|
||||
if numeric_terms:
|
||||
num_conds = []
|
||||
if exact_phrase:
|
||||
@@ -287,6 +287,7 @@ class AdvancedSearchEngine:
|
||||
try:
|
||||
_atm_cond = await _expand_atm(db, _atm_query)
|
||||
except Exception:
|
||||
logger.exception("ATM expansion failed (flat text): %s", _atm_query[:100])
|
||||
_atm_cond = None
|
||||
|
||||
_cond_before = len(conditions)
|
||||
@@ -335,8 +336,9 @@ class AdvancedSearchEngine:
|
||||
subq = select(GlobalJournal.issn).where(GlobalJournal.tier.in_(journal_tiers))
|
||||
result = await db.execute(subq)
|
||||
issns = [r for (r,) in result.all()]
|
||||
if issns:
|
||||
conditions.append(GlobalLiterature.journal_issn.in_(issns))
|
||||
if not issns:
|
||||
logger.warning("journal_tiers filter matched zero journals: %s", journal_tiers)
|
||||
conditions.append(GlobalLiterature.journal_issn.in_(issns))
|
||||
|
||||
# 标签筛选(含子标签递归)
|
||||
if tag_ids:
|
||||
@@ -386,8 +388,9 @@ class AdvancedSearchEngine:
|
||||
subq = select(GlobalJournal.issn).where(GlobalJournal.nlm_subsets.overlap(nlm_subsets))
|
||||
result = await db.execute(subq)
|
||||
issns = [r for (r,) in result.all()]
|
||||
if issns:
|
||||
conditions.append(GlobalLiterature.journal_issn.in_(issns))
|
||||
if not issns:
|
||||
logger.warning("nlm_subsets filter matched zero journals: %s", nlm_subsets)
|
||||
conditions.append(GlobalLiterature.journal_issn.in_(issns))
|
||||
|
||||
# ── PubMed 筛选器 ──
|
||||
|
||||
@@ -494,7 +497,7 @@ class AdvancedSearchEngine:
|
||||
|
||||
# 排序(PubMed 查询时跳过 ts_rank,避免语法标签噪音)
|
||||
_relevance_query = query
|
||||
if _pubmed_parsed and sort == "relevance":
|
||||
if _pubmed_parsed and sort in ("relevance", "best_match"):
|
||||
# 用纯文本词做相关性排序,去掉 [field] 标签
|
||||
plain_parts = [t.text for t in _pubmed_parsed.plain_terms]
|
||||
plain_parts += [t.text for t in _pubmed_parsed.title_terms]
|
||||
@@ -609,6 +612,7 @@ class AdvancedSearchEngine:
|
||||
term_conditions: list = []
|
||||
|
||||
# 1. 字段级搜索 [TI] [AB] [TIAB] [AU] [TA] [LA] [VI] [IP] [PG] [LID]
|
||||
field_combine = or_ if pp.boolean_operator == "or" else and_
|
||||
field_map = {
|
||||
"title": pp.title_terms,
|
||||
"abstract": pp.abstract_terms,
|
||||
@@ -631,7 +635,7 @@ class AdvancedSearchEngine:
|
||||
if term.is_not:
|
||||
cond = not_(cond)
|
||||
field_conds.append(cond)
|
||||
term_conditions.append(and_(*field_conds) if len(field_conds) > 1 else field_conds[0])
|
||||
term_conditions.append(field_combine(*field_conds) if len(field_conds) > 1 else field_conds[0])
|
||||
|
||||
# 2. 纯文本词(无字段标签)— P0-2: 对无标签词补充 ATM MeSH 展开
|
||||
if pp.plain_terms:
|
||||
@@ -648,14 +652,15 @@ class AdvancedSearchEngine:
|
||||
try:
|
||||
atm_cond = await _expand_atm_inline(db, combined)
|
||||
except Exception:
|
||||
logger.exception("ATM expansion failed (pubmed plain_terms): %s", combined[:100])
|
||||
atm_cond = None
|
||||
if atm_cond is not None:
|
||||
text_cond = and_(*plain_conds) if len(plain_conds) > 1 else plain_conds[0]
|
||||
text_cond = field_combine(*plain_conds) if len(plain_conds) > 1 else plain_conds[0]
|
||||
term_conditions.append(or_(atm_cond, text_cond))
|
||||
else:
|
||||
term_conditions.append(and_(*plain_conds) if len(plain_conds) > 1 else plain_conds[0])
|
||||
term_conditions.append(field_combine(*plain_conds) if len(plain_conds) > 1 else plain_conds[0])
|
||||
else:
|
||||
term_conditions.append(and_(*plain_conds) if len(plain_conds) > 1 else plain_conds[0])
|
||||
term_conditions.append(field_combine(*plain_conds) if len(plain_conds) > 1 else plain_conds[0])
|
||||
|
||||
# 3. [MH] → tree_number 展开,支持 is_not 和 _noexp
|
||||
if pp.mesh_terms:
|
||||
@@ -964,9 +969,9 @@ class AdvancedSearchEngine:
|
||||
# 6. [DP] → 年份/日期范围
|
||||
dp_negated = "DP" in getattr(pp, 'negated_date_ranges', set())
|
||||
dp_conds = []
|
||||
if pp.year_from:
|
||||
if pp.year_from is not None:
|
||||
dp_conds.append(GlobalLiterature.pub_year >= pp.year_from)
|
||||
if pp.year_to:
|
||||
if pp.year_to is not None:
|
||||
dp_conds.append(GlobalLiterature.pub_year <= pp.year_to)
|
||||
if pp.date_from:
|
||||
from datetime import date as _dt_date
|
||||
@@ -1095,10 +1100,18 @@ class AdvancedSearchEngine:
|
||||
if field == "TT":
|
||||
return GlobalLiterature.vernacular_title.ilike(f"%{_escape_ilike(term.text)}%")
|
||||
if field == "SB":
|
||||
subq = select(GlobalJournal.issn).where(
|
||||
GlobalJournal.nlm_subsets.overlap([term.text.upper()])
|
||||
)
|
||||
return GlobalLiterature.journal_issn.in_(subq)
|
||||
val = term.text.upper()
|
||||
if val == "PUBMED":
|
||||
return text("TRUE") # no-op: 所有记录都是 PubMed
|
||||
elif val == "MEDLINE":
|
||||
return GlobalLiterature.citation_status == "medline"
|
||||
elif len(val) == 1 and val.isalpha():
|
||||
subq = select(GlobalJournal.issn).where(
|
||||
GlobalJournal.nlm_subsets.overlap([val])
|
||||
)
|
||||
return GlobalLiterature.journal_issn.in_(subq)
|
||||
else:
|
||||
return GlobalLiterature.citation_status == val.lower()
|
||||
if field == "STAT":
|
||||
return GlobalLiterature.citation_status == term.text.lower()
|
||||
if field == "UID":
|
||||
@@ -1239,7 +1252,7 @@ class AdvancedSearchEngine:
|
||||
for (tid,) in rows:
|
||||
mesh_tag_ids.add(tid)
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("MeSH tag lookup failed for mesh_names=%s", mesh_names[:5])
|
||||
|
||||
if not mesh_tag_ids:
|
||||
return None
|
||||
@@ -1263,7 +1276,7 @@ class AdvancedSearchEngine:
|
||||
)).scalars().all()
|
||||
mesh_tag_ids.update(children)
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("Tree number expansion failed for mesh_names=%s", mesh_names[:5])
|
||||
|
||||
uids = list(mesh_tag_ids)
|
||||
if major_only:
|
||||
|
||||
Reference in New Issue
Block a user