- [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 项搜索测试全部通过,前端构建无报错
14 KiB
14 KiB
搜索功能实施计划
计划日期:2026-07-24 最后更新:2026-07-27(四轮修复已完成) 硬性目标:搜索功能必须与 PubMed 完全一致。不允许"暂缓/可以忽略/不急"的降级。 当前状态:42+ 字段标签已接通,127 项搜索测试通过,核心功能就绪。 基于 9 Agent 审计 + 真实数据库 1,662 篇字段覆盖率验证
总体进度
| 阶段 | 状态 | 修复数 | 涵盖范围 |
|---|---|---|---|
| 阶段 0 — 关键 Bug 修复 | ✅ 已完成 | 12 项 | search_engine + pubmed_api + 前端 |
| 阶段 1 — P0 功能 | ✅ 已完成 | 8 项 | parser + engine + 前端 |
| 阶段 2 — P1 功能 | ✅ 已完成 | 16 项 | parser + engine + 前端 |
| 阶段 3 — P2 完整覆盖 | ⏳ 部分完成 | — | 标签字段 50+ 已开,搜索历史等待办 |
| 轮次 2 — 第二轮审计修复 | ✅ 已完成 | 8 项 | De Morgan, noexp, 重复 NOT 等 |
| 轮次 3 — 第三轮审计修复 | ✅ 已完成 | 14 项 | SB/STAT 门控, 日期精度, [ALL] 注册等 |
| 轮次 4 — 第四轮字段补全 | ✅ 已完成 | 11 项 | 8 字段注册, OT/GEN/PMC 语义, phraseto_tsquery |
阶段 0 — 关键 Bug 修复 ✅(已完成)
目标:解除搜索阻塞 + 修复严重 Bug,不引入新功能。修改全部测试通过后提交。
| # | 修复项 | 文件 | 修改说明 | 状态 |
|---|---|---|---|---|
| 0.1 | _expand_mesh_tag_ids 移除 INNER JOIN |
search_engine.py:518 |
select(GlobalTag.id).join(GlobalTagTreeNumber) → select(GlobalTag.id).where(...) |
✅ |
| 0.2 | recent_subq 条件化 |
search_engine.py:314-319 |
有搜索词/历史查询时禁用 | ✅ |
| 0.3 | OR 布尔运算符修复 | search_engine.py:394-414 |
同 field 按 boolean_operator 用 or_() 组合 |
✅ |
| 0.4 | 多 [MH] 词 AND 组合 | search_engine.py:516-519 |
逐 term 独立 subq,按 boolean 组合 | ✅ |
| 0.5 | PMC/trial_reg XPath 上下文 | pubmed_api.py:440,447 |
article.findall → article_elem.findall |
✅ |
| 0.6 | 构造函数 5 字段遗漏 | pubmed_api.py:914-944 |
补 pubmed_revised/citation_status/date_completed/article_date/suppl_mesh_list |
✅ |
| 0.7 | ArticleTitle itertext |
pubmed_api.py:409 |
.text → "".join(itertext()) |
✅ |
| 0.8 | PMC_ID 格式统一 | pubmed_api.py:264 |
(art.get("pmcid") or "").lstrip("PMC") |
✅ |
| 0.9 | 搜索端点 try/except | features.py:68 |
加异常处理返回 400 | ✅ |
| 0.10 | 前端 field:all 硬编码 | SearchView.vue:83 |
替换为动态 field.value |
✅ |
| 0.11 | journal_iso 导入修复 |
pubmed_api.py |
覆盖率 0% → 100%,增补 ISOAbbreviation 解析 | ✅ |
| 0.12 | keywords 导入修复 |
pubmed_api.py |
覆盖率 0% → ~50%,增补 KeywordList 解析 | ✅ |
阶段 0 验证命令:
cd backend && python -m pytest tests/ -v --no-cov -k "not wechat"
# 运行 pipeline 回填已有数据
curl -X POST localhost:8000/api/v1/admin/pipeline/run \
-H "Authorization: Bearer $(admin_token)"
# 验证 journal_iso 数据
psql -U scilit_dev -d scilit_dev -c \
"SELECT pmid, pmid, journal_iso FROM global_literature WHERE journal_iso IS NOT NULL LIMIT 10"
# 验证 [MH] 搜索
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "lung neoplasms[MH]"}'
阶段 1 — P0 功能 ✅(大部分已完成)
目标:[MH]/[MAJR] 搜索恢复正常,tree_numbers 填充,EDAT 搜索可用,ATM 第一版。
| # | 任务 | 涉及文件 | 状态 |
|---|---|---|---|
| 1.1 | 导入完整 MeSH 树号(NLM mtrees2025.bin) |
scripts/import_mesh_tags.py |
✅ Done |
| 1.2 | 解析 PubmedData/History → entrez_date |
pubmed_api.py + models/literature.py + 迁移 |
✅ Done |
| 1.3 | [AD](机构)字段标签映射 |
search_engine.py + pubmed_query_parser.py |
✅ Done |
| 1.4 | [LA](语言)字段标签映射 |
search_engine.py + pubmed_query_parser.py |
✅ Done |
| 1.5 | [EDAT](入库日期)字段标签 |
search_engine.py + pubmed_query_parser.py |
✅ Done(含 CRDT/MHDA/LR/DCOM/DEP) |
| 1.6 | tsvector setweight() 迁移 |
迁移脚本 | ⏳ 待办 |
| 1.7 | ATM 引擎 v1:整合 SynonymExpander | services/query_expansion.py |
✅ Done |
| 1.8 | best_match 权重调优 + setweight 配合 |
search_engine.py:556-558 |
⏳ 待 setweight 后调优 |
1.1 MeSH 树号导入
操作步骤:
- 从 NLM 下载
mtrees2025.bin:wget https://nlmpubs.nlm.nih.gov/projects/mesh/MESH_FILES/meshtrees/mtrees2025.bin - 修改
scripts/import_mesh_tags.py,添加树号导入逻辑(解析DescriptorName;TreeNumber行) - 为现有 99 个标签补充 tree_number 关联
- 导入非 C04 树号(当前仅 manual 标签)
1.2 PubmedData/History → entrez_date
模型修改:
# models/literature.py 新增字段
entrez_date: datetime | None = None # PubMed 入库日期 → [EDAT]
解析修改(pubmed_api.py _parse_pubmed_xml()):
# 解析 PubmedData/History/PubMedPubDate[@PubStatus="entrez"]
history = article_elem.find(".//PubmedData/History")
if history is not None:
for pd in history.findall("PubMedPubDate"):
if pd.get("PubStatus") == "entrez":
# 解析年月日...
entrez_date = datetime(yr, mo, dy, tzinfo=UTC)
1.3-1.5 字段标签映射
解析器修改(pubmed_query_parser.py):
# _FIELD_TAG_MAP 添加
"AD": "affiliation", # 机构
"LA": "language", # 语言
"EDAT": "edat", # 入库日期
SQL 映射(search_engine.py _field_condition()):
"affiliation": GlobalLiterature.authors.cast(JSONB),
"language": GlobalLiterature.language,
"edat": GlobalLiterature.entrez_date,
1.6 tsvector setweight 迁移
-- 新迁移脚本:重建 search_tsv 带权重
ALTER TABLE global_literature DROP COLUMN IF EXISTS search_tsv;
ALTER TABLE global_literature ADD COLUMN search_tsv tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(abstract, '')), 'B') ||
setweight(to_tsvector('english',
coalesce((SELECT string_agg(value->>'family', ' ') FROM jsonb_array_elements(authors::jsonb)), '')
), 'A') ||
setweight(to_tsvector('english',
coalesce((SELECT string_agg(value->>'affiliation', ' ') FROM jsonb_array_elements(authors::jsonb)), '')
), 'B')
) STORED;
CREATE INDEX IF NOT EXISTS ix_global_literature_search_tsv ON global_literature USING GIN(search_tsv);
1.7 ATM 引擎 v1
新建文件 services/query_expansion.py:
class QueryExpander:
"""PubMed ATM 简化版:MeSH 翻译 + Journal 翻译 + 同义词展开"""
EXPANSION_LIMIT = 10 # 每个词最多展开为 10 个 OR 选项
async def expand(self, db: AsyncSession, query: str) -> str:
"""对 query 中的每个词执行 ATM 展开"""
words = query.split()
expanded = []
for w in words:
# 1. MeSH Translation Table — 查 exact 匹配的 descriptor
mesh = await self._lookup_mesh(db, w)
if mesh:
expanded.append(f"({w}[MH] OR {' OR '.join(mesh)})")
continue
# 2. Journal Translation — 查缩写/全称
journal = await self._lookup_journal(db, w)
if journal:
expanded.append(f"({w}[TA] OR {' OR '.join(journal)})")
continue
# 3. 同义词展开(整合 SynonymExpander)
syns = synonym_expander.expand(w)
if syns:
expanded.append(f"({w} OR {' OR '.join(syns)})")
continue
expanded.append(w)
return " ".join(expanded)
1.8 best_match 权重调优
# 修改后权重公式(配合 setweight)
score = (
func.ts_rank(GlobalLiterature.search_tsv, tsq) * 10.0 # ts_rank 放大到 ~0-10
+ func.ln(func.coalesce(GlobalLiterature.cited_by_count, 0) + 1) * 2.0 # 引用 ~0-14
+ case((GlobalLiterature.pub_year >= 2020, 1), else_=0) # 近期 +1
)
阶段 2 — P1 功能 ✅(大部分已完成)
目标:字段标签覆盖率达到 ~80%,通配符/精确短语/中文搜索支持。
| # | 任务 | 说明 | 状态 |
|---|---|---|---|
| 2.1 | [MH:NoExp]/[MAJR:NoExp] 支持 |
解析器 + SQL 增加 NoExp 标志,阻止 tree_number 展开 |
✅ 第二轮修复 F1 |
| 2.2 | [TA] journal_iso 支持 + 全称回退 |
_field_condition("journal") 增加 journal_iso ILIKE |
✅ Done |
| 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.5 | * 通配符截词 |
检测 word:* 模式 → to_tsquery('english', 'word:*') |
❌ 未实现 |
| 2.6 | [Title/Abstract] 长标签 |
解析器添加 Title/Abstract → TIAB 等价 |
✅ P4 已注册(映射到 all) |
| 2.7 | [ALL] 标签识别 |
解析器添加 ALL → all 字段映射 |
✅ 第三轮修复 P1-1 |
| 2.8 | Field tag 附着规则 + 单值 DP + 日期格式 | 解析器修复 4 个语法场景 | ✅ 第三轮修复 P1-2, P1-3 |
| 2.9 | Entry Terms 导入 | desc2025.asc 解析器 → GlobalTag.entry_terms |
✅ 完整 MeSH 导入脚本 |
| 2.10 | precision_mode 后端+前端 |
后端 filter + 前端 SearchView 暴露 | ❌ 已移除(第二轮 F6/F7) |
| 2.11 | 多 affiliation 捕获 | find → findall + ` |
` 连接 |
| 2.12 | 中文搜索 | 非英文 query 改用 simple 词典 |
✅ Done |
| 2.13 | 补全测试覆盖 | 28/46 零覆盖区域补全 | ✅ 127 项测试 |
| 2.14 | Europe PMC 7 字段补全 | grants 从 JSON 解析,其余标记 | ✅ Done |
| 2.15 | API 碎片清理 | 移除 /literature/search、Feed 集成 tsvector |
⏳ 部分完成 |
| 2.16 | phraseto_tsquery 精确短语 |
替代 ILIKE 用 GIN 索引 | ✅ P4 已实现 |
阶段 3 — P2 完整覆盖 ⏳(部分完成)
目标:完整字段标签、搜索历史、自动补全、索引全面、facets。
| # | 任务 | 状态 |
|---|---|---|
| 3.1 | 其余字段标签:目前已有 50+ 标签注册,仅 [REF]/[ISBN] 等低频未注册 | ⏳ 低频 |
| 3.2 | 搜索历史(/search/history 端点 + UI) |
❌ 未实现 |
| 3.3 | MeSH 自动补全(/tags/autocomplete + debounce) |
❌ 未实现 |
| 3.4 | 查询构建器 UI(布尔组合、括号分组) | ✅ AdvancedSearchPanel 已实现 |
| 3.5 | best_match 排序前端暴露 |
✅ SearchView 已有 |
| 3.6 | [AU] JSONB 回退 + [DOI] 精确匹配 |
✅ Done |
| 3.7 | is_pubmed_syntax() 引号检测 + regex 缓存 |
✅ Done |
| 3.8 | 重复 query 构建消除(提取 _build_query()) |
⏳ 可优化 |
| 3.9 | CommentsCorrections 全面处理(18 种 RefType) | ⏳ 仅撤稿 |
| 3.10 | 缺失索引(GIN 索引:pub_types/grants/chemical_list 等) | ❌ 需 DBA |
| 3.11 | DISMISS_THRESHOLD Feed 已读/忽略过滤 |
❌ 未实现 |
| 3.12 | 搜索响应 faceted counts + spell correction + highlight | ❌ 未实现 |
| 3.13 | GlobalTagTreeNumber 模型导出 |
✅ Done |
| 3.14 | JSON 列 json → jsonb 迁移 | ✅ 已完成 |
验证流程
回归测试
# 搜索专项测试(127 项,~30 秒)
cd backend && python -m pytest tests/test_pubmed_query_parser.py tests/test_pubmed_search_integration.py tests/test_comprehensive_verify.py -v --no-cov 2>&1 | tail -30
# 前端构建
cd frontend && npm run build
端到端搜索质量检查
# 纯文本搜索(基础能力)
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "lung cancer"}' | python -m json.tool | head -20
# MeSH 搜索 + 树展开
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "lung neoplasms[MH] AND immunotherapy[TI]"}' | python -m json.tool | head -20
# MeSH NoExp(不展开)
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "asthma[MH:noexp]"}' | python -m json.tool | head -20
# 精确短语 + best_match 排序
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "immune checkpoint inhibitor", "sort": "best_match", "boolean": "and"}'
# 布尔混合 NOT + 括号
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "NOT (lung AND cancer)"}'
# 双重否定
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "NOT NOT cancer"}'
# [SB]/[STAT] 搜索
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "medline[SB]"}' | python -m json.tool | head -20
# 日期字段独立语法
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "2025[DP]"}' | python -m json.tool | head -10
# 日期范围
curl -X POST localhost:8000/api/v1/features/search/advanced \
-H "Content-Type: application/json" \
-d '{"query": "cancer", "year_from": 2024, "year_to": 2026, "sort": "date"}'
依赖关系
阶段 0(12 个 Bug 修复)
│
├→ 阶段 1.1(MeSH 树号导入)← 需先完成 0.1
├→ 阶段 1.2(entrez_date 解析)← 独立
├→ 阶段 1.3-1.5(字段标签)← 独立
├→ 阶段 1.6(setweight 迁移)← 独立
├→ 阶段 1.7(ATM 引擎)← 需先完成 2.9 Entry Terms
└→ 阶段 1.8(best_match 权重)← 需先完成 1.6
阶段 0 可并行完成,阶段 1-3 按顺序推进。