Files
backend/docs/12-搜索功能实施计划.md
34047007@qq.com 73f9468384
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
fix: 第8轮搜索深度审计修复 — 缓存失效、Redis重试、中文标签、翻页稳定性等12项
CRITICAL:
- invalidate_search_cache 清理 atm:* 缓存(MeSH ATM扩展不再用过期结果)
- Pro 方案 api_quota_per_day 1000→10000(修复低于 Free 的数据错误)
- CacheService/RateLimitMiddleware Redis 连接失败60秒自动重试(原永久降级)
- 普通搜索中文输入自动匹配 GlobalTag.name_zh(如"肺癌"通过MeSH标签关联文献)
- AdvancedPubSearchView resolveQuery 添加 seen Set 检测交替 #N 循环引用

HIGH:
- 限速器 _burst_windows 每500请求清理过期条目(防止内存泄漏)
- cron daily_ftp_update 末尾调用 invalidate_search_cache()(自动管道不再用过期缓存)

MEDIUM:
- _apply_order_by ASC 排序加 id tiebreaker(title/journal/first_author翻页跳行/重复)
- _keyset_condition 所有 is_(None) 加 id tiebreaker + __NULL__ 哨兵值
- _field_condition("all") 默认tsvector路径加 journal/journal_iso ILIKE 兜底
- SearchView restoreFromQuery date_preset/year_from/year_to 优先顺序修复

docs: 更新 12/13 搜索文档,移除 CLAUDE.md 陈旧 SQLite 提及
2026-07-28 11:02:29 +08:00

14 KiB
Raw Permalink Blame History

搜索功能实施计划

计划日期:2026-07-24 最后更新:2026-07-28(八轮修复已完成) 硬性目标:搜索功能必须与 PubMed 完全一致。不允许"暂缓/可以忽略/不急"的降级。 当前状态:1007 项测试全部通过,前端构建成功,搜索缓存失效、Redis 重试、中文标签、翻页稳定性等全面加固。 基于 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.findallarticle_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 树号导入

操作步骤

  1. 从 NLM 下载 mtrees2025.bin
    wget https://nlmpubs.nlm.nih.gov/projects/mesh/MESH_FILES/meshtrees/mtrees2025.bin
    
  2. 修改 scripts/import_mesh_tags.py,添加树号导入逻辑(解析 DescriptorName;TreeNumber 行)
  3. 为现有 99 个标签补充 tree_number 关联
  4. 导入非 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/AbstractTIAB 等价 P4 已注册(映射到 all
2.7 [ALL] 标签识别 解析器添加 ALLall 字段映射 第三轮修复 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 捕获 findfindall + ` ` 连接
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.2entrez_date 解析)← 独立
  ├→ 阶段 1.3-1.5(字段标签)← 独立
  ├→ 阶段 1.6(setweight 迁移)← 独立
  ├→ 阶段 1.7(ATM 引擎)← 需先完成 2.9 Entry Terms
  └→ 阶段 1.8best_match 权重)← 需先完成 1.6

阶段 0 可并行完成,阶段 1-3 按顺序推进。