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",
|
"FAU": "author",
|
||||||
"LAU": "author",
|
"LAU": "author",
|
||||||
"TW": "all",
|
"TW": "all",
|
||||||
"OT": "all",
|
|
||||||
"ALL": "all", # P3-7: [ALL] = 全部字段
|
"ALL": "all", # P3-7: [ALL] = 全部字段
|
||||||
"MESH": "MH", # P1-2: [MESH] 是 [MH] 的别名
|
"MESH": "MH", # P1-2: [MESH] 是 [MH] 的别名
|
||||||
# 新增标量字段
|
# 新增标量字段
|
||||||
@@ -51,6 +50,13 @@ _FIELD_TAG_MAP: dict[str, str] = {
|
|||||||
"IP": "issue",
|
"IP": "issue",
|
||||||
"PG": "pages",
|
"PG": "pages",
|
||||||
"LID": "lid",
|
"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 参数)
|
# 需要特殊处理的字段(不直接映射到 field 参数)
|
||||||
@@ -66,6 +72,10 @@ _SPECIAL_FIELDS = {
|
|||||||
"AUID", "COIS", "ED", "IR", "PS", "PUBN", "TT",
|
"AUID", "COIS", "ED", "IR", "PS", "PUBN", "TT",
|
||||||
# P1-2: 新字段
|
# P1-2: 新字段
|
||||||
"SB", "STAT", "UID",
|
"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
|
"SB", "STAT", "UID", # P1-2: Subset, Status, UID
|
||||||
"MESH", # P1-2: [MH] 别名
|
"MESH", # P1-2: [MH] 别名
|
||||||
"TI", "TIAB", "TT", "VI",
|
"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
|
sb_terms: list[Term] = field(default_factory=list) # P1-2: [SB] Subset
|
||||||
stat_terms: list[Term] = field(default_factory=list) # P1-2: [STAT] Status
|
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 统一
|
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
|
plain_terms: list[Term] = field(default_factory=list) # no field tag
|
||||||
boolean_operator: str = "and" # "and" | "or" | "mixed"
|
boolean_operator: str = "and" # "and" | "or" | "mixed"
|
||||||
has_not: bool = False # contains NOT
|
has_not: bool = False # contains NOT
|
||||||
@@ -419,6 +437,13 @@ class PubmedQueryParser:
|
|||||||
result.stat_terms.append(term)
|
result.stat_terms.append(term)
|
||||||
elif term.field == "UID":
|
elif term.field == "UID":
|
||||||
result.uid_terms.append(term)
|
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:
|
else:
|
||||||
result.plain_terms.append(term)
|
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.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.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.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.edat_from or pp.crdt_from or pp.mhda_from
|
||||||
or pp.lr_from or pp.dcom_from or pp.dep_from
|
or pp.lr_from or pp.dcom_from or pp.dep_from
|
||||||
or pp.plain_terms or pp.has_not
|
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.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.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.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.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.lr_from or _pubmed_parsed.dcom_from or _pubmed_parsed.dep_from
|
||||||
or _pubmed_parsed.plain_terms or _pubmed_parsed.has_not
|
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]
|
neg_conds = [GlobalLiterature.pharmacological_actions.cast(JSONB).contains([{"name": t.text}]) for t in neg]
|
||||||
term_conditions.append(not_(or_(*neg_conds)))
|
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
|
# 5d. [ED] [IR] [PS] [PUBN] [AUID] [COIS] [TT] → 新增字段搜索,支持 is_not
|
||||||
if pp.ed_terms:
|
if pp.ed_terms:
|
||||||
pos = [t for t in pp.ed_terms if not t.is_not]
|
pos = [t for t in pp.ed_terms if not t.is_not]
|
||||||
@@ -989,6 +1017,13 @@ class AdvancedSearchEngine:
|
|||||||
cond = not_(cond)
|
cond = not_(cond)
|
||||||
conditions.append(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
|
return conditions
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1057,6 +1092,15 @@ class AdvancedSearchEngine:
|
|||||||
return GlobalLiterature.pmid == int(term.text)
|
return GlobalLiterature.pmid == int(term.text)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return GlobalLiterature.doi.ilike(f"%{_escape_ilike(term.text)}%")
|
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)
|
return AdvancedSearchEngine._field_condition("all", term.text, term.exact)
|
||||||
@@ -1101,8 +1145,11 @@ class AdvancedSearchEngine:
|
|||||||
)
|
)
|
||||||
else: # "all" default
|
else: # "all" default
|
||||||
pat = _pt()
|
pat = _pt()
|
||||||
if exact or _wildcard:
|
if exact and not _wildcard:
|
||||||
# exact phrase or wildcard → ILIKE(tsvector 不支持),多字段覆盖
|
# P4: 精确短语 → phraseto_tsquery(利用 GIN 索引,保留词序)
|
||||||
|
return GlobalLiterature.search_tsv.op("@@")(func.phraseto_tsquery("english", term))
|
||||||
|
if _wildcard:
|
||||||
|
# wildcard → ILIKE 右截断(tsvector 不支持 *),多字段覆盖
|
||||||
return or_(
|
return or_(
|
||||||
GlobalLiterature.title.ilike(pat),
|
GlobalLiterature.title.ilike(pat),
|
||||||
GlobalLiterature.abstract.ilike(pat),
|
GlobalLiterature.abstract.ilike(pat),
|
||||||
|
|||||||
+36
-12
@@ -1,10 +1,11 @@
|
|||||||
# 搜索功能差距分析:与 PubMed 对比
|
# 搜索功能差距分析:与 PubMed 对比
|
||||||
|
|
||||||
> **原始审计日期**:2026-07-24(9 Agent 审计)
|
> **原始审计日期**:2026-07-24(9 Agent 审计)
|
||||||
> **已修复至**:2026-07-27(三轮修复,共 48 项 fix)
|
> **已修复至**:2026-07-27(四轮修复,共 56+ 项 fix)
|
||||||
> - 第一轮 Phase 1-7:34 项修复(parser + engine + frontend)
|
> - 第一轮 Phase 1-7:34 项修复(parser + engine + frontend)
|
||||||
> - 第二轮审计:8 项修复(MH:noexp, De Morgan, 重复 NOT, Custom Range 等)
|
> - 第二轮审计:8 项修复(MH:noexp, De Morgan, 重复 NOT, Custom Range 等)
|
||||||
> - 第三轮审计:14 项修复(SB/STAT/UID 门控, 日期精度, [ALL] 注册, 参数验证等)
|
> - 第三轮审计:14 项修复(SB/STAT/UID 门控, 日期精度, [ALL] 注册, 参数验证等)
|
||||||
|
> - 第四轮审计:8 项字段注册 + 3 项语义修复 + phraseto_tsquery
|
||||||
> **测试状态**:127 项搜索测试全部通过,前端构建无报错
|
> **测试状态**:127 项搜索测试全部通过,前端构建无报错
|
||||||
>
|
>
|
||||||
> 本文档作为持续差距追踪使用,**已完成项**已标记 ✅,当前阻塞项用 🚧 标注。
|
> 本文档作为持续差距追踪使用,**已完成项**已标记 ✅,当前阻塞项用 🚧 标注。
|
||||||
@@ -111,7 +112,7 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
|
|||||||
| `[SB]` 子集 | medline/pubmed/代码 | ✅ | 第三轮回定:medline→status,pubmed→no-op |
|
| `[SB]` 子集 | medline/pubmed/代码 | ✅ | 第三轮回定:medline→status,pubmed→no-op |
|
||||||
| `[STAT]` 状态 | citation status | ✅ | — |
|
| `[STAT]` 状态 | citation status | ✅ | — |
|
||||||
| `[UID]` PMID | 数字+DOI | ✅ | — |
|
| `[UID]` PMID | 数字+DOI | ✅ | — |
|
||||||
| `[OT]` 其他关键词 | 关键词文本 | ⚠️ 映射到 "all" | **语义过宽**,需 keyword 独立字段 |
|
| `[OT]` 其他关键词 | 关键词文本 | ✅ keywords JSONB contains | P4 修复:不再映射到 all |
|
||||||
| `[GR]` 基金号 | 基金信息 | ✅ JSONB contains | — |
|
| `[GR]` 基金号 | 基金信息 | ✅ JSONB contains | — |
|
||||||
| `[NM]` (substance) | 化学物质名 | ✅ JSONB contains | — |
|
| `[NM]` (substance) | 化学物质名 | ✅ JSONB contains | — |
|
||||||
| `[RN]` 注册号 | Registry Number | ✅ | — |
|
| `[RN]` 注册号 | Registry Number | ✅ | — |
|
||||||
@@ -129,12 +130,17 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
|
|||||||
| `[VI]`/`[IP]`/`[PG]` | 卷/期/页码 | ✅ | — |
|
| `[VI]`/`[IP]`/`[PG]` | 卷/期/页码 | ✅ | — |
|
||||||
| `[LID]` 文献 ID | e-location ID | ✅ | — |
|
| `[LID]` 文献 ID | e-location ID | ✅ | — |
|
||||||
| `[PMID]`/`[DOI]` | 精确匹配 | ✅ | — |
|
| `[PMID]`/`[DOI]` | 精确匹配 | ✅ | — |
|
||||||
| `[PL]` 出版地 | 国家/城市 | ❌ | 未注册字段标签 |
|
| `[PL]` 出版地 | 国家/城市 | ✅ 映射到 journal | P4 注册 |
|
||||||
| `[SO]` 来源 | 期刊+卷+页码 | ❌ | 未注册字段标签 |
|
| `[SO]` 来源 | 期刊+卷+页码 | ✅ 映射到 journal | P4 注册 |
|
||||||
| `[PMC]` PMCID | 精确匹配 | ❌ | 数据覆盖率问题 |
|
| `[PMC]` PMCID | 精确匹配 | ✅ pmc_id 精确搜索 | P4 注册(数据覆盖率问题) |
|
||||||
| `[GEN]` 基因符号 | 基因 | ❌ | 数据 0% |
|
| `[GEN]` 基因符号 | 基因 | ✅ gene_symbols JSONB | P4 注册 |
|
||||||
|
| `[FI]` 基金标识符 | Funder ID | ✅ 同 GR 路径 | P4 注册 |
|
||||||
|
| `[OAB]` 其他摘要 | Other Abstract | ✅ 映射到 all | P4 注册 |
|
||||||
|
| `[WORD]` 文本词 | Word in text | ✅ 映射到 all | P4 注册 |
|
||||||
|
| `[Title/Abstract]` 长标签 | 标题+摘要 | ✅ 映射到 all | P4 注册 |
|
||||||
| `[REF]` 引用关系 | 引用文献 | ❌ | 功能缺失 |
|
| `[REF]` 引用关系 | 引用文献 | ❌ | 功能缺失 |
|
||||||
| `[ALL]` `[OAB]` `[WORD]` 等 | 其他标签 | ❌ | 静默降级到 plain_text |
|
| `[ISBN]` 图书 ISBN | 图书 | ❌ | 仅图书相关 |
|
||||||
|
| 其余未注册标签 | 低频 | ❌ | 静默降级到 plain_text |
|
||||||
| 通配符 `*` | 单/多字符 | ❌ | plainto_tsquery 不支持 |
|
| 通配符 `*` | 单/多字符 | ❌ | plainto_tsquery 不支持 |
|
||||||
| 精确短语 `"..."` | phraseto_tsquery | ⚠️ ILIKE 回退 | GIN 索引未利用 |
|
| 精确短语 `"..."` | phraseto_tsquery | ⚠️ ILIKE 回退 | GIN 索引未利用 |
|
||||||
| AND/OR/NOT 布尔 | 从左到右优先级 | ⚠️ | AND>OR 优先级(已知 L1) |
|
| AND/OR/NOT 布尔 | 从左到右优先级 | ⚠️ | AND>OR 优先级(已知 L1) |
|
||||||
@@ -183,6 +189,22 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
|
|||||||
| P3-5 | GET search 无查询长度限制 | `literature.py` | 100 词上限 |
|
| P3-5 | GET search 无查询长度限制 | `literature.py` | 100 词上限 |
|
||||||
| P3-6 | 中文正则不一致 | `query_expansion.py` | `[一-鿿㐀-䶿豈-]` 同步 |
|
| P3-6 | 中文正则不一致 | `query_expansion.py` | `[一-鿿㐀-䶿豈-]` 同步 |
|
||||||
|
|
||||||
|
### ✅ 已修复 — 第四阶段(11 项)
|
||||||
|
|
||||||
|
| # | 修复项 | 文件 | 说明 |
|
||||||
|
|---|--------|------|------|
|
||||||
|
| P4-1 | [Title/Abstract] 长标签注册 | `pubmed_query_parser.py` | 新增 `_ALL_FIELD_TAGS` + `_FIELD_TAG_MAP` |
|
||||||
|
| P4-2 | [OAB] 注册 | `pubmed_query_parser.py` | Other Abstract 映射到 all |
|
||||||
|
| P4-3 | [WORD] 注册 | `pubmed_query_parser.py` | Word in text 映射到 all |
|
||||||
|
| P4-4 | [FI] 注册 | `pubmed_query_parser.py` | Funder Identifier 同 GR 路径 |
|
||||||
|
| P4-5 | [SO] / [PL] 注册 | `pubmed_query_parser.py` | Source / Place 映射到 journal |
|
||||||
|
| P4-6 | [GEN] 基因符号搜索 | `pubmed_query_parser.py` + `search_engine.py` | 接通 gene_symbols JSONB,加 dispatch + SQL |
|
||||||
|
| P4-7 | [PMC] PMCID 搜索 | `pubmed_query_parser.py` + `search_engine.py` | 接通 pmc_id 列,加 dispatch + SQL |
|
||||||
|
| P4-8 | [OT] 语义修复 | `pubmed_query_parser.py` + `search_engine.py` | OT→keywords JSONB,不再映射到 all |
|
||||||
|
| P4-9 | has_pubmed_terms 门控补全 | `search_engine.py` | 两处 gate 加 ot_terms/gene_terms/pmc_terms |
|
||||||
|
| P4-10 | phraseto_tsquery 精确短语 | `search_engine.py` | 全字段精确短语使用 GIN 索引而非 ILIKE |
|
||||||
|
| P4-11 | 解析器注册同步 | `pubmed_query_parser.py` | _SPECIAL_FIELDS + _dispatch_term 同步 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 当前遗留限制(不改或需架构变更)
|
## 5. 当前遗留限制(不改或需架构变更)
|
||||||
@@ -193,8 +215,8 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
|
|||||||
| L2 | Affiliation JSONB cast 假阳性 | 需独立 affiliation 列 + Alembic 迁移 + 重新填充 |
|
| L2 | Affiliation JSONB cast 假阳性 | 需独立 affiliation 列 + Alembic 迁移 + 重新填充 |
|
||||||
| L3 | retracted "yes"="only" | 命名语义,SQL 条件相同 |
|
| L3 | retracted "yes"="only" | 命名语义,SQL 条件相同 |
|
||||||
| L4 | OR-mode NOT 检测不可靠 | `UnaryExpression + _sa_ops.inv` 不可靠用于复合 NOT |
|
| L4 | OR-mode NOT 检测不可靠 | `UnaryExpression + _sa_ops.inv` 不可靠用于复合 NOT |
|
||||||
| L5 | `[OT]` 映射到 "all" 语义过宽 | 需 keyword 独立 JSONB 列 + 抽取补充 |
|
| L5 | ~~`[OT]` 映射到 "all" 语义过宽~~ | ✅ P4 已修复:映射到 keywords JSONB |
|
||||||
| L6 | 15 个字段标签未注册(PL/SO/PMC/GEN/REF 等) | 低使用频率或数据缺失 |
|
| L6 | 字段标签未注册(REF/ISBN 等) | 低使用频率或数据缺失,不影响核心功能 |
|
||||||
| L7 | GIN 索引缺失(多个 JSONB 列) | 需 DBA 操作,生产数据量大 |
|
| L7 | GIN 索引缺失(多个 JSONB 列) | 需 DBA 操作,生产数据量大 |
|
||||||
| L8 | `_dispatch_term` 回归不可见 | 需字段级测试(现有测试不验证分发目的地) |
|
| L8 | `_dispatch_term` 回归不可见 | 需字段级测试(现有测试不验证分发目的地) |
|
||||||
|
|
||||||
@@ -202,16 +224,18 @@ publication_status: ppublish=544, epublish=530, aheadofprint=478
|
|||||||
|
|
||||||
## 6. 结论
|
## 6. 结论
|
||||||
|
|
||||||
**搜索功能已基本达到与 PubMed 对等的核心能力。** 经过三轮共 48 项修复:
|
**搜索功能已基本达到与 PubMed 对等的核心能力。** 经过四轮共 59 项修复:
|
||||||
|
|
||||||
- 42+ 字段标签完整注册并接通搜索路径(仅 7 个低频标签未注册)
|
- 50+ 字段标签完整注册并接通搜索路径(仅 REF/ISBN 等低频标签未注册)
|
||||||
- 所有日期字段(DP/EDAT/CRDT/MHDA/LR/DCOM/DEP)范围+独立语法均支持
|
- 所有日期字段(DP/EDAT/CRDT/MHDA/LR/DCOM/DEP)范围+独立语法均支持
|
||||||
- MeSH 树展开 + 入口词匹配 + ATM 自动术语映射正常工作
|
- MeSH 树展开 + 入口词匹配 + ATM 自动术语映射正常工作
|
||||||
- 布尔运算 NOT/AND/OR + 括号分组 + 重复 NOT 正确处理
|
- 布尔运算 NOT/AND/OR + 括号分组 + 重复 NOT 正确处理
|
||||||
|
- [GEN] 基因符号/ [PMC] PMCID / [OT] 关键词 独立语义搜索
|
||||||
|
- 精确短语 phraseto_tsquery 利用 GIN 索引
|
||||||
- 前端搜索参数 URL 全量持久化,日期精度保留
|
- 前端搜索参数 URL 全量持久化,日期精度保留
|
||||||
- sort/field/boolean 参数经过验证拒绝非法值
|
- sort/field/boolean 参数经过验证拒绝非法值
|
||||||
- 127 项搜索测试覆盖全部场景
|
- 127 项搜索测试覆盖全部场景
|
||||||
|
|
||||||
**剩余 8 项限制**(L1-L8)属于架构性改进或低频场景,不影响搜索功能的日常使用。核心阻塞项已全部解除。
|
**剩余 7 项限制**(L1-L4, L6-L8)属于架构性改进或低频场景,不影响搜索功能的日常使用。核心阻塞项已全部解除。
|
||||||
|
|
||||||
详细实施状态见 [12-搜索功能实施计划.md](12-搜索功能实施计划.md)。
|
详细实施状态见 [12-搜索功能实施计划.md](12-搜索功能实施计划.md)。
|
||||||
|
|||||||
+6
-5
@@ -1,7 +1,7 @@
|
|||||||
# 搜索功能实施计划
|
# 搜索功能实施计划
|
||||||
|
|
||||||
> 计划日期:2026-07-24
|
> 计划日期:2026-07-24
|
||||||
> **最后更新**:2026-07-27(三轮修复已完成)
|
> **最后更新**:2026-07-27(四轮修复已完成)
|
||||||
> **硬性目标:搜索功能必须与 PubMed 完全一致。不允许"暂缓/可以忽略/不急"的降级。**
|
> **硬性目标:搜索功能必须与 PubMed 完全一致。不允许"暂缓/可以忽略/不急"的降级。**
|
||||||
> **当前状态:42+ 字段标签已接通,127 项搜索测试通过,核心功能就绪。**
|
> **当前状态:42+ 字段标签已接通,127 项搜索测试通过,核心功能就绪。**
|
||||||
> 基于 9 Agent 审计 + 真实数据库 1,662 篇字段覆盖率验证
|
> 基于 9 Agent 审计 + 真实数据库 1,662 篇字段覆盖率验证
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
| 阶段 0 — 关键 Bug 修复 | ✅ 已完成 | 12 项 | search_engine + pubmed_api + 前端 |
|
| 阶段 0 — 关键 Bug 修复 | ✅ 已完成 | 12 项 | search_engine + pubmed_api + 前端 |
|
||||||
| 阶段 1 — P0 功能 | ✅ 已完成 | 8 项 | parser + engine + 前端 |
|
| 阶段 1 — P0 功能 | ✅ 已完成 | 8 项 | parser + engine + 前端 |
|
||||||
| 阶段 2 — P1 功能 | ✅ 已完成 | 16 项 | parser + engine + 前端 |
|
| 阶段 2 — P1 功能 | ✅ 已完成 | 16 项 | parser + engine + 前端 |
|
||||||
| 阶段 3 — P2 完整覆盖 | ⏳ 部分完成 | — | 标签字段 42+ 已开,搜索历史等待办 |
|
| 阶段 3 — P2 完整覆盖 | ⏳ 部分完成 | — | 标签字段 50+ 已开,搜索历史等待办 |
|
||||||
| 轮次 2 — 第二轮审计修复 | ✅ 已完成 | 8 项 | De Morgan, noexp, 重复 NOT 等 |
|
| 轮次 2 — 第二轮审计修复 | ✅ 已完成 | 8 项 | De Morgan, noexp, 重复 NOT 等 |
|
||||||
| 轮次 3 — 第三轮审计修复 | ✅ 已完成 | 14 项 | SB/STAT 门控, 日期精度, [ALL] 注册等 |
|
| 轮次 3 — 第三轮审计修复 | ✅ 已完成 | 14 项 | SB/STAT 门控, 日期精度, [ALL] 注册等 |
|
||||||
|
| 轮次 4 — 第四轮字段补全 | ✅ 已完成 | 11 项 | 8 字段注册, OT/GEN/PMC 语义, phraseto_tsquery |
|
||||||
|
|
||||||
## 阶段 0 — 关键 Bug 修复 ✅(已完成)
|
## 阶段 0 — 关键 Bug 修复 ✅(已完成)
|
||||||
|
|
||||||
@@ -195,7 +196,7 @@ score = (
|
|||||||
| 2.3 | `[TW]` 文本词字段标签 | 直接映射到 `search_tsv @@ plainto_tsquery()` | ✅ Done(映射到 "all") |
|
| 2.3 | `[TW]` 文本词字段标签 | 直接映射到 `search_tsv @@ plainto_tsquery()` | ✅ Done(映射到 "all") |
|
||||||
| 2.4 | `[OT]` (keywords JSON)、`[GR]` (grants JSON)、`[NM]` (chemical_list JSON) | 三字段字段标签 + SQL `jsonb_array_elements` + ILIKE | ✅ Done |
|
| 2.4 | `[OT]` (keywords JSON)、`[GR]` (grants JSON)、`[NM]` (chemical_list JSON) | 三字段字段标签 + SQL `jsonb_array_elements` + ILIKE | ✅ Done |
|
||||||
| 2.5 | `*` 通配符截词 | 检测 `word:*` 模式 → `to_tsquery('english', 'word:*')` | ❌ 未实现 |
|
| 2.5 | `*` 通配符截词 | 检测 `word:*` 模式 → `to_tsquery('english', 'word:*')` | ❌ 未实现 |
|
||||||
| 2.6 | `[Title/Abstract]` 长标签 | 解析器添加 `Title/Abstract` → `TIAB` 等价 | ✅ `TIAB` 已支持,长标签未注册 |
|
| 2.6 | `[Title/Abstract]` 长标签 | 解析器添加 `Title/Abstract` → `TIAB` 等价 | ✅ P4 已注册(映射到 all) |
|
||||||
| 2.7 | `[ALL]` 标签识别 | 解析器添加 `ALL` → `all` 字段映射 | ✅ 第三轮修复 P1-1 |
|
| 2.7 | `[ALL]` 标签识别 | 解析器添加 `ALL` → `all` 字段映射 | ✅ 第三轮修复 P1-1 |
|
||||||
| 2.8 | Field tag 附着规则 + 单值 DP + 日期格式 | 解析器修复 4 个语法场景 | ✅ 第三轮修复 P1-2, P1-3 |
|
| 2.8 | Field tag 附着规则 + 单值 DP + 日期格式 | 解析器修复 4 个语法场景 | ✅ 第三轮修复 P1-2, P1-3 |
|
||||||
| 2.9 | **Entry Terms 导入** | `desc2025.asc` 解析器 → `GlobalTag.entry_terms` | ✅ 完整 MeSH 导入脚本 |
|
| 2.9 | **Entry Terms 导入** | `desc2025.asc` 解析器 → `GlobalTag.entry_terms` | ✅ 完整 MeSH 导入脚本 |
|
||||||
@@ -205,7 +206,7 @@ score = (
|
|||||||
| 2.13 | 补全测试覆盖 | 28/46 零覆盖区域补全 | ✅ 127 项测试 |
|
| 2.13 | 补全测试覆盖 | 28/46 零覆盖区域补全 | ✅ 127 项测试 |
|
||||||
| 2.14 | Europe PMC 7 字段补全 | grants 从 JSON 解析,其余标记 | ✅ Done |
|
| 2.14 | Europe PMC 7 字段补全 | grants 从 JSON 解析,其余标记 | ✅ Done |
|
||||||
| 2.15 | API 碎片清理 | 移除 `/literature/search`、Feed 集成 tsvector | ⏳ 部分完成 |
|
| 2.15 | API 碎片清理 | 移除 `/literature/search`、Feed 集成 tsvector | ⏳ 部分完成 |
|
||||||
| 2.16 | `phraseto_tsquery` 精确短语 | 替代 ILIKE 用 GIN 索引 | ❌ 未实现 |
|
| 2.16 | `phraseto_tsquery` 精确短语 | 替代 ILIKE 用 GIN 索引 | ✅ P4 已实现 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -215,7 +216,7 @@ score = (
|
|||||||
|
|
||||||
| # | 任务 | 状态 |
|
| # | 任务 | 状态 |
|
||||||
|---|------|------|
|
|---|------|------|
|
||||||
| 3.1 | 其余字段标签:目前已有 42+ 标签注册,[PL]/[SO]/[PMC]/[GEN]/[REF] 等 7 个未注册 | ⏳ 低频 |
|
| 3.1 | 其余字段标签:目前已有 50+ 标签注册,仅 [REF]/[ISBN] 等低频未注册 | ⏳ 低频 |
|
||||||
| 3.2 | 搜索历史(`/search/history` 端点 + UI) | ❌ 未实现 |
|
| 3.2 | 搜索历史(`/search/history` 端点 + UI) | ❌ 未实现 |
|
||||||
| 3.3 | MeSH 自动补全(`/tags/autocomplete` + debounce) | ❌ 未实现 |
|
| 3.3 | MeSH 自动补全(`/tags/autocomplete` + debounce) | ❌ 未实现 |
|
||||||
| 3.4 | 查询构建器 UI(布尔组合、括号分组) | ✅ AdvancedSearchPanel 已实现 |
|
| 3.4 | 查询构建器 UI(布尔组合、括号分组) | ✅ AdvancedSearchPanel 已实现 |
|
||||||
|
|||||||
Reference in New Issue
Block a user