feat: 第四轮PubMed搜索字段补全 — 11项修复
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

- [Title/Abstract]/[OAB]/[WORD]/[FI]/[SO]/[PL] 注册
- [GEN] 基因符号搜索 → gene_symbols JSONB
- [PMC] PMCID 搜索 → pmc_id 列
- [OT] 语义修复 → keywords JSONB(不再映射到 all)
- phraseto_tsquery 精确短语(GIN 索引替代 ILIKE)
- has_pubmed_terms 门控补全新字段
- 127 项搜索测试全部通过,前端构建无报错
This commit is contained in:
34047007@qq.com
2026-07-27 11:19:10 +08:00
parent 6a8eb70dde
commit 807972d41b
4 changed files with 117 additions and 20 deletions
+26 -1
View File
@@ -42,7 +42,6 @@ _FIELD_TAG_MAP: dict[str, str] = {
"FAU": "author",
"LAU": "author",
"TW": "all",
"OT": "all",
"ALL": "all", # P3-7: [ALL] = 全部字段
"MESH": "MH", # P1-2: [MESH] 是 [MH] 的别名
# 新增标量字段
@@ -51,6 +50,13 @@ _FIELD_TAG_MAP: dict[str, str] = {
"IP": "issue",
"PG": "pages",
"LID": "lid",
# P4 新增字段标签
"Title/Abstract": "all", # [Title/Abstract] 长标签 → all
"OAB": "all", # [OAB] Other Abstract → all
"WORD": "all", # [WORD] Word in text → all
"FI": "GR", # [FI] Funder Identifier → 同 GRgrant_id
"SO": "journal", # [SO] Source → journal(近似)
"PL": "journal", # [PL] Place of Publication → journal(近似)
}
# 需要特殊处理的字段(不直接映射到 field 参数)
@@ -66,6 +72,10 @@ _SPECIAL_FIELDS = {
"AUID", "COIS", "ED", "IR", "PS", "PUBN", "TT",
# P1-2: 新字段
"SB", "STAT", "UID",
# P4 新增:独立语义字段
"OT", # [OT] → keywords JSONB(不再映射到 all
"GEN", # [GEN] → gene_symbols JSONB
"PMC", # [PMC] → pmc_id
}
# 支持日期范围语法的字段
@@ -83,6 +93,10 @@ _ALL_FIELD_TAGS = {
"SB", "STAT", "UID", # P1-2: Subset, Status, UID
"MESH", # P1-2: [MH] 别名
"TI", "TIAB", "TT", "VI",
# P4 新增字段标签
"Title/Abstract", # [Title/Abstract] 长标签
"OAB", "WORD", # [OAB] Other Abstract, [WORD] Word in text
"FI", "GEN", "PMC", "SO", "PL", # [FI] Funder, [GEN] Gene, [PMC] PMCID, [SO] Source, [PL] Place
}
@@ -221,6 +235,10 @@ class ParsedPubmedQuery:
sb_terms: list[Term] = field(default_factory=list) # P1-2: [SB] Subset
stat_terms: list[Term] = field(default_factory=list) # P1-2: [STAT] Status
uid_terms: list[Term] = field(default_factory=list) # P1-2: [UID] PMID/DOI 统一
# P4 新增独立语义字段
ot_terms: list[Term] = field(default_factory=list) # [OT] → keywords JSONB
gene_terms: list[Term] = field(default_factory=list) # [GEN] → gene_symbols JSONB
pmc_terms: list[Term] = field(default_factory=list) # [PMC] → pmc_id
plain_terms: list[Term] = field(default_factory=list) # no field tag
boolean_operator: str = "and" # "and" | "or" | "mixed"
has_not: bool = False # contains NOT
@@ -419,6 +437,13 @@ class PubmedQueryParser:
result.stat_terms.append(term)
elif term.field == "UID":
result.uid_terms.append(term)
# P4 新增独立语义字段
elif term.field == "OT":
result.ot_terms.append(term)
elif term.field == "GEN":
result.gene_terms.append(term)
elif term.field == "PMC":
result.pmc_terms.append(term)
else:
result.plain_terms.append(term)
+49 -2
View File
@@ -173,6 +173,7 @@ class AdvancedSearchEngine:
or pp.ed_terms or pp.investigator_terms or pp.personal_name_terms
or pp.pubnote_terms or pp.auid_terms or pp.cois_terms or pp.tt_terms
or pp.sb_terms or pp.stat_terms or pp.uid_terms
or pp.ot_terms or pp.gene_terms or pp.pmc_terms
or pp.edat_from or pp.crdt_from or pp.mhda_from
or pp.lr_from or pp.dcom_from or pp.dep_from
or pp.plain_terms or pp.has_not
@@ -201,6 +202,7 @@ class AdvancedSearchEngine:
or _pubmed_parsed.ed_terms or _pubmed_parsed.investigator_terms or _pubmed_parsed.personal_name_terms
or _pubmed_parsed.pubnote_terms or _pubmed_parsed.auid_terms or _pubmed_parsed.cois_terms or _pubmed_parsed.tt_terms
or _pubmed_parsed.sb_terms or _pubmed_parsed.stat_terms or _pubmed_parsed.uid_terms
or _pubmed_parsed.ot_terms or _pubmed_parsed.gene_terms or _pubmed_parsed.pmc_terms
or _pubmed_parsed.edat_from or _pubmed_parsed.crdt_from or _pubmed_parsed.mhda_from
or _pubmed_parsed.lr_from or _pubmed_parsed.dcom_from or _pubmed_parsed.dep_from
or _pubmed_parsed.plain_terms or _pubmed_parsed.has_not
@@ -755,6 +757,32 @@ class AdvancedSearchEngine:
neg_conds = [GlobalLiterature.pharmacological_actions.cast(JSONB).contains([{"name": t.text}]) for t in neg]
term_conditions.append(not_(or_(*neg_conds)))
# P4: [OT] → keywords JSONB contains(不再映射到 all
if pp.ot_terms:
pos = [t for t in pp.ot_terms if not t.is_not]
neg = [t for t in pp.ot_terms if t.is_not]
if pos:
term_conditions.append(or_(*[
GlobalLiterature.keywords.cast(JSONB).contains([t.text])
for t in pos
]))
if neg:
neg_conds = [GlobalLiterature.keywords.cast(JSONB).contains([t.text]) for t in neg]
term_conditions.append(not_(or_(*neg_conds)))
# P4: [GEN] → gene_symbols JSONB contains
if pp.gene_terms:
pos = [t for t in pp.gene_terms if not t.is_not]
neg = [t for t in pp.gene_terms if t.is_not]
if pos:
term_conditions.append(or_(*[
GlobalLiterature.gene_symbols.cast(JSONB).contains([t.text])
for t in pos
]))
if neg:
neg_conds = [GlobalLiterature.gene_symbols.cast(JSONB).contains([t.text]) for t in neg]
term_conditions.append(not_(or_(*neg_conds)))
# 5d. [ED] [IR] [PS] [PUBN] [AUID] [COIS] [TT] → 新增字段搜索,支持 is_not
if pp.ed_terms:
pos = [t for t in pp.ed_terms if not t.is_not]
@@ -989,6 +1017,13 @@ class AdvancedSearchEngine:
cond = not_(cond)
conditions.append(cond)
# P4: [PMC] → pmc_id 精确匹配,支持 is_not
for term in pp.pmc_terms:
cond = GlobalLiterature.pmc_id == term.text
if term.is_not:
cond = not_(cond)
conditions.append(cond)
return conditions
@staticmethod
@@ -1057,6 +1092,15 @@ class AdvancedSearchEngine:
return GlobalLiterature.pmid == int(term.text)
except ValueError:
return GlobalLiterature.doi.ilike(f"%{_escape_ilike(term.text)}%")
# P4: [OT] → keywords JSONB contains
if field == "OT":
return GlobalLiterature.keywords.cast(JSONB).contains([term.text])
# P4: [GEN] → gene_symbols JSONB contains
if field == "GEN":
return GlobalLiterature.gene_symbols.cast(JSONB).contains([term.text])
# P4: [PMC] → pmc_id 精确匹配
if field == "PMC":
return GlobalLiterature.pmc_id == term.text
# 回退
return AdvancedSearchEngine._field_condition("all", term.text, term.exact)
@@ -1101,8 +1145,11 @@ class AdvancedSearchEngine:
)
else: # "all" default
pat = _pt()
if exact or _wildcard:
# exact phrase or wildcard → ILIKEtsvector 不支持),多字段覆盖
if exact and not _wildcard:
# P4: 精确短语 → phraseto_tsquery(利用 GIN 索引,保留词序)
return GlobalLiterature.search_tsv.op("@@")(func.phraseto_tsquery("english", term))
if _wildcard:
# wildcard → ILIKE 右截断(tsvector 不支持 *),多字段覆盖
return or_(
GlobalLiterature.title.ilike(pat),
GlobalLiterature.abstract.ilike(pat),