P0 (1项): - keyset cursor: lit.title or "__NULL__" 空字符串误判为NULL导致后续翻页全空 P1 (6项): - boolean_operator: 仅扫描depth=0的token,括号内AND/OR不再影响顶层操作符判定 - is_pubmed_syntax: 去除AND/OR/NOT检测的IGNORECASE(PubMed仅识别大写布尔符) - _relevance_query: MeSH-only查询(如breast[MAJR])补充原始回退,避免退化到日期排序 - pmid_terms: 缺少try/except int(),补充ValueError+DOI兜底 - 部分日期展开: 2024-01[DP]等YYYY-MM partial date展开为整月范围(DP/EDAT/CRDT) - 前端#N去重: resolveQuery/expandQuery dedup refs防重复引用误判为循环 P2 (9项): - cache: filter-options加入invalidate_search_cache清理 - worker: daily_ftp_update/daily_citation_update异常时finally保证缓存清理 - cursor_val: 空字符串''通过is None检查,补充not cursor_val - keyset UI: 模板条件sort==='date'改为KEYSET_SORTS.has(sort) - resetAllFilters: 补充showCustomYear=false
213 lines
7.4 KiB
Python
213 lines
7.4 KiB
Python
"""PubMed 查询语法解析器测试"""
|
|
|
|
import pytest
|
|
|
|
from app.services.pubmed_query_parser import (
|
|
ParsedPubmedQuery,
|
|
parse_pubmed_query,
|
|
is_pubmed_syntax,
|
|
tokenise,
|
|
extract_pubmed_query_for_prisma,
|
|
)
|
|
|
|
|
|
class TestIsPubmedSyntax:
|
|
def test_plain_text_not_pubmed(self):
|
|
assert not is_pubmed_syntax("lung cancer")
|
|
|
|
def test_field_tag_detected(self):
|
|
assert is_pubmed_syntax('"lung cancer"[TI]')
|
|
|
|
def test_boolean_and_detected(self):
|
|
assert is_pubmed_syntax("cancer AND therapy")
|
|
|
|
def test_boolean_or_detected(self):
|
|
assert is_pubmed_syntax("cancer OR tumor")
|
|
|
|
def test_boolean_not_detected(self):
|
|
assert is_pubmed_syntax("cancer NOT review")
|
|
|
|
def test_empty_not_pubmed(self):
|
|
assert not is_pubmed_syntax("")
|
|
assert not is_pubmed_syntax(None)
|
|
|
|
def test_lowercase_boolean_detected(self):
|
|
"""小写的 and/or/not 不识别为 PubMed 语法(P11: 仅大写 AND/OR/NOT 才是 PubMed 布尔符)"""
|
|
assert not is_pubmed_syntax("cancer and therapy")
|
|
|
|
|
|
class TestTokenise:
|
|
def test_simple_tokens(self):
|
|
tokens = tokenise('"lung cancer"[TI]')
|
|
texts = [t.value for t in tokens if t.value]
|
|
assert '"lung cancer"' in texts
|
|
assert "[TI]" in texts
|
|
|
|
def test_boolean_tokens(self):
|
|
tokens = tokenise("a AND b OR c NOT d")
|
|
texts = [t.value for t in tokens if t.value]
|
|
assert "AND" in texts
|
|
assert "OR" in texts
|
|
assert "NOT" in texts
|
|
|
|
def test_parentheses(self):
|
|
tokens = tokenise("(a OR b)")
|
|
types = [t.type.name for t in tokens]
|
|
assert "LPAREN" in types
|
|
assert "RPAREN" in types
|
|
|
|
|
|
class TestParsePubmedQuery:
|
|
def test_plain_text_no_field(self):
|
|
"""纯文本查询返回 plain_terms"""
|
|
result = parse_pubmed_query("lung cancer")
|
|
assert len(result.plain_terms) == 2
|
|
assert result.plain_terms[0].text == "lung"
|
|
assert result.plain_terms[1].text == "cancer"
|
|
|
|
def test_title_field(self):
|
|
"""[TI] 映射到 title 字段"""
|
|
result = parse_pubmed_query('"lung cancer"[TI]')
|
|
assert len(result.title_terms) == 1
|
|
t = result.title_terms[0]
|
|
assert t.text == "lung cancer"
|
|
assert t.exact is True
|
|
assert t.field == "title"
|
|
|
|
def test_abstract_field(self):
|
|
result = parse_pubmed_query('"cancer"[AB]')
|
|
assert len(result.abstract_terms) == 1
|
|
assert result.abstract_terms[0].text == "cancer"
|
|
|
|
def test_tiab_field(self):
|
|
result = parse_pubmed_query('"cancer"[TIAB]')
|
|
assert len(result.tiab_terms) == 1
|
|
assert result.tiab_terms[0].text == "cancer"
|
|
|
|
def test_author_field(self):
|
|
result = parse_pubmed_query("smith j[AU]")
|
|
assert len(result.author_terms) == 1
|
|
assert result.author_terms[0].text == "j"
|
|
assert result.author_terms[0].field == "author"
|
|
# smith should be captured as plain term via implicit AND
|
|
assert len(result.plain_terms) >= 1
|
|
|
|
def test_journal_field(self):
|
|
result = parse_pubmed_query('"nature"[TA]')
|
|
assert len(result.journal_terms) == 1
|
|
assert result.journal_terms[0].text == "nature"
|
|
|
|
def test_mesh_field(self):
|
|
result = parse_pubmed_query('"lung neoplasms"[MH]')
|
|
assert len(result.mesh_terms) == 1
|
|
assert result.mesh_terms[0].text == "lung neoplasms"
|
|
assert result.mesh_terms[0].is_not is False
|
|
|
|
def test_majr_field(self):
|
|
result = parse_pubmed_query('"lung neoplasms"[MAJR]')
|
|
assert len(result.majr_terms) == 1
|
|
assert result.majr_terms[0].text == "lung neoplasms"
|
|
assert result.majr_terms[0].is_not is False
|
|
|
|
def test_pub_type(self):
|
|
result = parse_pubmed_query('"review"[PT]')
|
|
assert len(result.pub_types) == 1
|
|
assert result.pub_types[0].text == "review"
|
|
|
|
def test_pmid_field(self):
|
|
result = parse_pubmed_query("12345678[PMID]")
|
|
assert len(result.pmid_terms) == 1
|
|
assert result.pmid_terms[0].text == "12345678"
|
|
|
|
def test_doi_field(self):
|
|
result = parse_pubmed_query('"10.1000/test"[DOI]')
|
|
assert len(result.doi_terms) == 1
|
|
assert "10.1000/test" in result.doi_terms[0].text
|
|
|
|
def test_date_range_dp(self):
|
|
result = parse_pubmed_query("2024:2026[DP]")
|
|
assert result.year_from == 2024
|
|
assert result.year_to == 2026
|
|
|
|
def test_simple_and(self):
|
|
result = parse_pubmed_query("cancer AND therapy")
|
|
assert result.boolean_operator == "and"
|
|
assert len(result.plain_terms) == 2
|
|
|
|
def test_simple_or(self):
|
|
result = parse_pubmed_query("cancer OR tumor")
|
|
assert result.boolean_operator == "or"
|
|
assert len(result.plain_terms) == 2
|
|
|
|
def test_boolean_not(self):
|
|
result = parse_pubmed_query('"lung"[TI] NOT "review"[PT]')
|
|
assert result.has_not is True
|
|
assert len(result.not_terms) == 1
|
|
assert result.not_terms[0].text == "review"
|
|
|
|
def test_complex_nested(self):
|
|
result = parse_pubmed_query(
|
|
'("lung cancer"[TI] OR "breast cancer"[TI]) AND "immunotherapy"[TIAB]'
|
|
)
|
|
# 括号分组后,term 进入 groups 而非直接列在 title_terms
|
|
assert len(result.groups) > 0
|
|
assert len(result.tiab_terms) == 1
|
|
assert result.boolean_operator == "and"
|
|
# P11: 括号内的 OR 不影响顶层布尔操作符判定,顶层只有 AND
|
|
# 确保 groups 中包含括号内的两个 title 词
|
|
group_texts = [t.text for g in result.groups for t in g]
|
|
assert "lung cancer" in group_texts
|
|
assert "breast cancer" in group_texts
|
|
|
|
def test_implicit_and_multiple_terms(self):
|
|
result = parse_pubmed_query("a b c")
|
|
assert len(result.plain_terms) == 3
|
|
|
|
def test_lowercase_field_tag(self):
|
|
result = parse_pubmed_query('"lung cancer"[ti]')
|
|
assert len(result.title_terms) == 1
|
|
|
|
def test_mixedcase_field_tag(self):
|
|
result = parse_pubmed_query('"lung cancer"[Ti]')
|
|
assert len(result.title_terms) == 1
|
|
|
|
def test_quoted_phrase_exact(self):
|
|
result = parse_pubmed_query('"exact phrase"')
|
|
assert len(result.plain_terms) == 1
|
|
assert result.plain_terms[0].exact is True
|
|
|
|
def test_not_with_parens(self):
|
|
result = parse_pubmed_query('("cancer" AND "therapy") NOT "review"')
|
|
assert result.has_not is True
|
|
|
|
def test_empty_query(self):
|
|
result = parse_pubmed_query("")
|
|
assert len(result.plain_terms) == 0
|
|
|
|
def test_invalid_syntax_degrades(self):
|
|
"""非法语法退化到空 ParsedPubmedQuery"""
|
|
result = parse_pubmed_query("invalid[[syntax")
|
|
# Should not crash, return empty plain_terms
|
|
assert isinstance(result, ParsedPubmedQuery)
|
|
|
|
def test_field_with_dot(self):
|
|
result = parse_pubmed_query("john.doe[AU]")
|
|
assert len(result.author_terms) == 1
|
|
|
|
|
|
class TestExtractForPrisma:
|
|
def test_extract_mesh_and_normalize(self):
|
|
normalized, mesh = extract_pubmed_query_for_prisma(
|
|
'("lung cancer"[ti] OR "lung neoplasms"[mh]) AND 2024:2026[dp]'
|
|
)
|
|
# Field tags should be uppercased
|
|
assert "[TI]" in normalized or "[ti]" not in normalized
|
|
|
|
def test_pubmed_query_types(self):
|
|
normalized, mesh = extract_pubmed_query_for_prisma(
|
|
'"cancer"[TI] AND "immunotherapy"[MH]'
|
|
)
|
|
assert "cancer" in normalized
|
|
assert "immunotherapy" in normalized
|
|
assert "immunotherapy" in mesh
|