feat: 第四轮PubMed搜索字段补全 — 11项修复
- [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:
@@ -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 → 同 GR(grant_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)
|
||||
|
||||
|
||||
@@ -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 → ILIKE(tsvector 不支持),多字段覆盖
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user