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 提及
189 lines
10 KiB
Markdown
189 lines
10 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Development Commands
|
||
|
||
```bash
|
||
# Start dev environment (backend + frontend)
|
||
cd backend && python scripts/seed_data.py && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||
cd frontend && npm install && npm run dev
|
||
|
||
# Run tests (611 tests, ~3min)
|
||
cd backend && python -m pytest tests/ -v --no-cov
|
||
|
||
# Build frontend (type-check + prod bundle, ~5s)
|
||
cd frontend && npm run build
|
||
|
||
# Database migrations (Alembic)
|
||
cd backend && python -m alembic -c alembic/alembic.ini upgrade head # 应用迁移
|
||
cd backend && python -m alembic -c alembic/alembic.ini downgrade -1 # 回滚一步
|
||
cd backend && python -m alembic -c alembic/alembic.ini history # 查看历史
|
||
cd backend && python -m alembic -c alembic/alembic.ini current # 查看当前版本
|
||
# 自动生成迁移(修改模型后执行):
|
||
cd backend && python -m alembic -c alembic/alembic.ini revision --autogenerate -m "变更说明"
|
||
|
||
# Docker deployment
|
||
docker compose up -d # development(自动运行迁移)
|
||
docker compose -f docker-compose.prod.yml up -d # production(自动运行迁移)
|
||
|
||
# PubMed pipeline (real data)
|
||
curl -X POST localhost:8000/api/v1/admin/pipeline/run -H "Authorization: Bearer $(admin_token)"
|
||
# FTP baseline import (first-time full data)
|
||
cd backend && python scripts/pubmed_baseline.py --dir /path/to/pubmed/baseline/ --demo
|
||
# 批量刷新被引次数(PubMed elink,免费,3 req/s。全库约N分钟/N秒)
|
||
curl -X POST localhost:8000/api/v1/admin/pipeline/refresh-citations -H "Authorization: Bearer $(admin_token)"
|
||
curl -X POST "localhost:8000/api/v1/admin/pipeline/refresh-citations?limit=500" -H "Authorization: Bearer $(admin_token)"
|
||
```
|
||
|
||
## Architecture Overview
|
||
|
||
### Database Migrations (Alembic)
|
||
|
||
Alembic 管理数据库 schema 版本。初始迁移 `alembic/versions/e56e408b2208_initial_schema.py` 包含所有 32 个模型的建表语句。
|
||
|
||
**工作流:**
|
||
1. 修改 Python 模型(增/删/改字段)
|
||
2. 运行 `alembic revision --autogenerate -m "描述"` 自动生成迁移脚本
|
||
3. 检查生成的脚本,确认无误
|
||
4. 运行 `alembic upgrade head` 应用到数据库
|
||
|
||
**部署:** Docker Compose 启动时会自动执行 `alembic upgrade head`。
|
||
|
||
**模型修改后务必同步文档:** 每次通过 migration 增/删/改字段后,必须同步更新 `docs/03-数据库设计.md` 中对应表的 `CREATE TABLE` 定义和索引,保持与模型代码一致。
|
||
|
||
### Multi-Tenant Isolation
|
||
|
||
Two levels: `ContextVar` (application) + PostgreSQL RLS (database, production only). Every tenant-scoped request sets `tenant_ctx` via `get_current_user` (not middleware — avoids connection pool races). The JWT access token carries `tid` (tenant_id) and `is_superuser`. Admin routes (`/admin/*`) enforce `require_superuser` via router-level dependency — demo users get 403.
|
||
|
||
### Middleware Stack (inside→out)
|
||
|
||
`CORS → RateLimit → PerformanceMonitor → SecurityHeaders → CSRFProtection → CSRFCookie → Trace`
|
||
|
||
Rate limiter caches plan quotas 5 minutes. CSRF exempts Bearer tokens and `/auth/*`, `/public/*`, `/captcha/*`.
|
||
|
||
### Backend Layer Pattern
|
||
|
||
```
|
||
api/v1/ → schemas/ → services/ → models/ (DB)
|
||
↑
|
||
core/ (security, permissions, tenant_ctx, middleware)
|
||
```
|
||
|
||
Services never import from api/. Models never import from services/. `core/plans.py` defines Free/Pro/Team/Enterprise feature matrix — use `get_tenant_plan()` to check feature access.
|
||
|
||
### Database
|
||
|
||
39 SQLAlchemy 2.0 models. All `TIMESTAMPTZ` columns use `server_default=func.now()`. Pure date fields (pub_date, approval_date) use `DATE`.
|
||
|
||
`user_feed` is the only table needing partitioning — `PARTITION BY RANGE (pushed_at)` monthly.
|
||
|
||
### Feed Engine
|
||
|
||
`generate_feeds_for_literature()`: new article → match tags against active subscriptions → compute priority (must_read/recommended/related) → insert `user_feed`. Called after every PubMed pipeline run.
|
||
|
||
### Frontend Route Architecture
|
||
|
||
Three layouts: `PublicLayout` (no auth, no sidebar), `AuthLayout` (centered card), `AppLayout` (sidebar + header). Admin uses its own `AdminLayout`. All pages lazy-loaded via `() => import(...)`. Auth guard in `router.beforeEach()` checks `auth.isAuthenticated`.
|
||
|
||
### PubMed Pipeline
|
||
|
||
**FTP 每日更新文件为主数据源**(2026-07-25 决策)。取代旧 E-utilities API 多路搜索策略。
|
||
|
||
日常运行:
|
||
1. **FTP 下载** `pubmed25updateNNNN.xml.gz`(每日 ~10MB,含全部新增/修改/删除记录)
|
||
2. **解析**:`_extract_article()`(复用 `pubmed_baseline.py` 的 lxml 解析器)
|
||
3. **过滤**:`_is_oncology()` 按 MeSH 肿瘤科筛选(复用 `pubmed_baseline.py`)
|
||
4. **处理**:`<PubmedArticle>` → `_update_lit_from_article()` upsert;`<DeleteCitation>` → 标记 `retracted=True`
|
||
5. **打标**: MeSH UI → `global_tags` lookup → `global_literature_tags` INSERT
|
||
6. **检查点**:`pipeline_runs.processed_date` 记录已处理的 EDAT 日期
|
||
|
||
降级:FTP 不可用时,回退到 `pubmed_api.py`(NCBI E-utilities,3 req/s)的 `reldate=1&datetype=edat` 查询。
|
||
|
||
### ARQ Scheduled Tasks (`backend/app/tasks/worker.py`)
|
||
|
||
定时任务使用 ARQ (Redis-backed),所有时间均为 **UTC**。
|
||
|
||
| 任务 | 函数 | Cron (UTC) | 北京时间 | 说明 |
|
||
|---|---|---|---|---|
|
||
| 每日 FTP 增量 | `daily_ftp_update` | `03:07` 每天 | 11:07 | FTP 下载更新文件,处理新增/修改/删除(取代精搜+宽搜+retagger) |
|
||
| 引用更新 | `daily_citation_update` | `05:13` 每天 | 13:13 | 刷新最近文献被引次数 |
|
||
| 摘要邮件 | `daily_digest_task` | `22:30` 每天 | 06:30 (次日) | 每日摘要推送 |
|
||
|
||
> **旧任务已移除:** `daily_pubmed_pipeline`(MAJR 精搜)、`weekly_broad_pipeline`(Title/Abstract 宽搜)和 `mesh_retagger.py`(回查)已被 `daily_ftp_update` 完全取代。FTP 每日更新文件包含所有新/改/删记录,覆盖 MeSH in-process→medline 过渡,不再需要多路搜索和回查。
|
||
|
||
手动触发:
|
||
```bash
|
||
# FTP 增量更新
|
||
curl -X POST localhost:8000/api/v1/admin/pipeline/run -H "Authorization: Bearer $(admin_token)"
|
||
# 批量刷新被引次数
|
||
curl -X POST localhost:8000/api/v1/admin/pipeline/refresh-citations -H "Authorization: Bearer $(admin_token)"
|
||
curl -X POST "localhost:8000/api/v1/admin/pipeline/refresh-citations?limit=500" -H "Authorization: Bearer $(admin_token)"
|
||
```
|
||
|
||
### Citation Counts (PubMed elink)
|
||
|
||
每条文献入库时**自动查询被引次数**(`pubmed_api.py:fetch_citedby_counts()`,通过 NCBI elink 免费接口,200 篇/req)。每日定时任务 `daily_citation_update` 刷新最近文献的被引。管理后台也支持手动全量刷新。
|
||
|
||
**技术要点:**
|
||
- 接口:`elink.fcgi?dbfrom=pubmed&linkname=pubmed_pubmed_citedin&id=PMID1,PMID2,...`
|
||
- 限速:无 API Key 3 req/s,有 key 10 req/s
|
||
- 批量 200 篇/次,全库 1 万篇约需 15-20 分钟免费模式
|
||
- 更新 `global_literature.cited_by_count` + `updated_at`(所以 🔄 徽标会联动)
|
||
- 新文献引用数可能为 0(刚发表,随后每次定时刷新自动更新)
|
||
|
||
**NLM 行为要点:**
|
||
- `DateRevised` **不可靠**——NLM 明确说不应依赖它判断 revision(如 MeSH 年更大批改记录但不一定赋值)。判 new/revised 的唯一稳妥方式是 PMID 撞库
|
||
- `_update_lit_from_article()` 对已存在 PMID 直接覆盖所有字段,不做"是否变了"的检查
|
||
- 每年 11 月中–12 月中是 MeSH 年更期,indexed 记录暂停灌入,只发 in-process/publisher。此时宽搜补充尤为重要
|
||
|
||
### PMC OA TDM (Full Text)
|
||
|
||
为未来深度学习 NLP(药物-靶点关系抽取、临床试验结构化提取)做数据准备。
|
||
|
||
**PMC OA 覆盖:** 约 15% 的 PubMed 文献有 PMC 全文,但高影响力期刊 OA 比例远高于此,对 oncology 领域数据挖掘足够。
|
||
|
||
**数据流:** PubMed efetch 已返回 `pmc_id` + `is_oa` → `pmc_oa.py` 用 OA Service API 下载 XML → `jats_parser.py` 解析成结构化 sections。
|
||
|
||
**存储策略:** 存 parsed sections JSON(`full_text_sections` 字段),不存原始 XML。sections 结构:
|
||
```json
|
||
{"sections": [{"heading": "Introduction", "text": "..."}, {"heading": "Methods", "text": "..."}],
|
||
"chemicals": [{"name": "Trastuzumab", "registry_number": "..."}],
|
||
"references": [{"pmid": 12345, "title": "..."}],
|
||
"tables": [{"caption": "Table 1", "data": "..."}]}
|
||
```
|
||
|
||
**实现(Phase 1):** `app/services/pmc_oa.py` — OA Service API 客户端;`app/services/jats_parser.py` — JATS XML 解析;`GlobalLiterature.full_text_sections` JSON 字段(Alembic 迁移);在 `_run_pipeline()` 中新增 OA 全文抓取阶段。Phase 2 回填现有 OA 文献。Phase 3 按需上 NLP 模型。
|
||
|
||
**技术要点:**
|
||
- API:`https://www.ncbi.nlm.nih.gov/pmc/utils/oa/oa.fcgi?id=PMCID` → XML 响应
|
||
- PMCID ↔ PMID 映射已在 PubMed efetch 阶段完成(`pmc_id` 字段)
|
||
- JATS XML 按 `<sec>` 标签组织,`<sec-title>` + 段落文本即可提取结构化 sections
|
||
- `<chemical>` 标签含 Registry Number,可对接 PubChem
|
||
- 限速:共用 NCBI E-utilities 池(3 req/s 无 key,10 req/s 有 key)
|
||
- 与 `_run_pipeline()` 集成:在打标 + Feed 生成后,对 `is_oa=True` 且无 `full_text_sections` 的文献执行
|
||
|
||
### Key Design Decisions
|
||
|
||
- **No multi-specialty in one deployment.** One codebase, one YAML config, one Docker stack per specialty. Oncology is the first.
|
||
- **Personal → Team upgrade is zero-data-migration.** Personal user's `tenant_id` stays the same; only `is_personal` flips and `plan_type` changes.
|
||
- **Dev mode password reset** returns the reset link directly in API response (no SMTP needed).
|
||
- **`user["sub"]` is a string.** Always convert to `uuid.UUID()` before passing to SQLAlchemy queries.
|
||
- **搜索功能必须与 PubMed 完全一致。** 这是硬性要求,不是"未来优化"。所有 PubMed 字段标签必须全量支持,已存储数据的立即接通搜索路径,缺失数据的补充 XML 抽取和存储。不允许任何字段退化到纯文本搜索。
|
||
|
||
## 代码托管
|
||
|
||
仓库:http://123.207.9.209:3000/scilit/backend
|
||
|
||
**当前阶段(单人开发)**:直接 push 到 main,不设审核。等多人协作时再开启 PR + Review。
|
||
|
||
### 常用命令
|
||
```bash
|
||
git remote -v # 查看 remote
|
||
git push origin main # 直接推送(当前模式)
|
||
git checkout -b feat/xxx # 建分支
|
||
git push origin feat/xxx # 推送分支
|
||
```
|
||
|
||
后期切换 PR 模式时,在 Gitea 上设置 Branch Protection 即可。
|