# Architecture
## 项目分层
QuantumAtlas 的核心设计不是“把所有东西都塞进一个存储”,而是明确区分不同层级的 source of truth。
```text
QuantumAtlas app repo 对象存储 (RustFS / S3) PostgreSQL 搜索引擎
应用代码与工具 <-> canonical paper assets <-> registry + OpenAlex corpus <-> 派生查询层
```
建议这样理解:
- 应用仓库负责代码、CLI、API、测试和脚本。
- 对象存储保存 PDF、解析 Markdown、元数据 JSON 和图片等论文资产,是 canonical paper asset store(本地 `RAW_DIR` 仅作 dev fallback)。
- PostgreSQL 是中心化关系库:paper registry(papers / paper_assets / paper_identities,goose 管理)+ OpenAlex works 语料,见 ADR 0006。
- 搜索引擎(`POST /api/search` 的多 provider fan-out)与 ingest 任务状态属于派生或运行时层,不是长期主数据源。
## 为什么要把 registry 和论文资产分开
对象存储负责回答“字节在哪”,PostgreSQL 负责回答“有哪些论文、各自处于什么状态、彼此什么关系”。
这带来几个好处:
- 大文件资产不会污染关系库;registry 行可以独立重建。
- 集合查询(计数、过滤、租约)走关系型数据库擅长的约束、索引和事务。
- 应用代码可以按 release tag 固定,语料与资产可以独立高频更新。
- 搜索只是查询层,不会反向定义数据边界。
## Paper 资产
`qatlasd` 服务端**默认不通过 HTTP API 对外分发** PDF / Markdown 字节。资产
桶里的对象只为内部处理(如解析、索引)服务,用户/客户端只能查询元数据
(OpenAlex 同步进来的 title / authors / 引用关系等)与论文是否已经具备
某项资产(`papers/stats`、`papers/needs-mineru`)。Self-hosted 部署可用
`QATLAS_PAPER_ACCESS_ENABLED=true` 打开字节分发,合规义务随之转给部署方。
如果需要原始 PDF,请直接到 arXiv 等上游获取(`papers.ArxivAbsURL` 数据
字段可以拼出 canonical arxiv URL)。
## 论文元数据索引(PostgreSQL catalog)
> `paperindex` 是历史锚点名:旧版曾把索引放成 RustFS 上的 Parquet。
> 现在的决策是 **RustFS 只做纯 S3 后端**;集合查询、DOI 状态和 MinerU claim
> 租约进入 PostgreSQL,登录态仍由 PocketBase 单独管理。
### 问题:对象存储不会回答"集合性"问题
对象存储 (S3 / RustFS) 的原生接口只有"按 key 取一个对象 (GetObject)"、
"按 key 列出对象 (ListObjects)"、"按 key 删除"几类。它**没有**:
- 跨对象的 query / filter / sort / count / group by
- 二级索引(按非 key 字段查)
- 原子租约(例如"抢一篇还没被 MinerU 处理的论文")
对单个论文做"按 id 取 PDF"这种**点查**是天然合适的(就是 GetObject)。
但真实业务需要集合查询:
| 需求 | 对象存储原生能力 | 当前实现 |
|---|---|---|
| "PDF / Markdown 总数" | `ListObjects(prefix=pdf/)` 全扫 + 计数 | PostgreSQL `count(*) FILTER (...)` |
| "需要 MinerU 的论文(有 PDF 无 MD)" | 双 ListObjects + 内存 diff | PostgreSQL partial index (`paper_assets ... WHERE mineru_md_path IS NULL`) |
| "DOI 是否已有本地贡献" | 无二级索引 | PostgreSQL UNIQUE `papers.paper_doi` |
| "抢一个 MinerU lease" | 无事务锁 | PostgreSQL row lock + `paper_assets.lease_expires_at` |
### 实际方案:PostgreSQL 是非登录态 catalog
`papers` + `paper_assets` 两张表是 paper catalog 的派生索引(ADR 0009):
- **`papers`**(一 work 一行):代理主键 `paper_id`;三个外部 id 列各 UNIQUE
`paper_arxiv_id` / `paper_doi` / `paper_openalex_id`(`paper_openalex_id` 带可空 FK →
`openalex_works`);`paper_title` / `paper_publication_date`;生成式 `paper_ref`
(openalex>arxiv>doi);触发器维护的 `paper_default_asset_id`;精简
`paper_verification_status`
- **`paper_assets`**(一份 PDF 一行:arxiv v1/v2/… + 正式版):`asset_id`、
`paper_id`(FK CASCADE)、`source`(arxiv|published)、`arxiv_version`、对象 key
`pdf_path` / `mineru_md_path` / `mineru_json_path`、`pdf_sha256` / `pdf_size` /
`image_count` / `fetched_at`;MinerU 租约 `lease_id` / `lease_holder` /
`lease_expires_at`
使用 PostgreSQL 特性:
- `INSERT ... ON CONFLICT` 做 upload write-through 和 `papers sync` 重建;
- partial unique index:`paper_assets (paper_id) WHERE source='published'`(正式版每篇一份);
- partial queue index:只索引 `paper_assets ... WHERE mineru_md_path IS NULL`
的 MinerU 队列候选;
- row-level lock (`SELECT ... FOR UPDATE`) 保证同一资产的 lease 只有一个赢家;
- 触发器在 `paper_assets` 增删时重算 `papers.paper_default_asset_id`(正式版优先,否则最新 arxiv)。
### 写入路径
```text
1. PUT s3://qatlas-pdf//.pdf ← 字节先落 RustFS
2. INSERT ... ON CONFLICT papers + paper_assets ← PostgreSQL 派生索引同步
3. PostgreSQL 不可用时:HTTP 仍成功 + X-Catalog-Sync: deferred
4. 之后跑 qatlasd papers sync --full --from-rustfs 从 S3 重建缺失状态
```
RustFS 仍是资产字节的 source of truth;PostgreSQL 是可重建的集合索引与租约层。
这避免了把索引文件、Parquet 或应用级 metadata 放进 RustFS,同时也避免把登录态和
论文业务状态混进 PocketBase。
### 查询路径
```text
GET /api/papers/needs-mineru
→ SELECT ... FROM paper_assets a JOIN papers p ON p.paper_id = a.paper_id
WHERE a.source = 'arxiv'
AND a.pdf_path IS NOT NULL AND a.mineru_md_path IS NULL
AND (a.lease_expires_at IS NULL OR a.lease_expires_at < now())
ORDER BY a.fetched_at DESC
→ JSON 返回
```
跟直接 `ListObjects` 相比,这条链路不会触发全桶 LIST;registry 用的是
关系型数据库擅长的约束、索引和事务。
## Client / Server 边界
QuantumAtlas 既可以作为服务端运行,也可以作为远程客户端使用。
- server 模式负责读写对象存储 / PostgreSQL,并提供搜索、论文资产和摄入能力。
- client 模式通过 HTTP API 使用这些能力,不要求拿到服务器文件系统权限。
应用仓库内不再保留任何 `raw/`、`data/`、`pb_data/` 目录——所有
状态目录都有内置默认值,落到 git checkout 之外:
```ini
# 所有这些都有内置默认;不写就走默认(无需在 .env 里出现):
# QATLAS_RAW_DIR -> ${XDG_DATA_HOME:-$HOME/.local/share}/qatlasd/raw
# QATLAS_DATA_DIR -> ${XDG_DATA_HOME:-$HOME/.local/share}/qatlasd/data
# QATLAS_PB_DATA_DIR -> ${XDG_DATA_HOME:-$HOME/.local/share}/qatlasd/pb_data
#
# 想搬到挂载盘 / FHS 路径时再显式覆盖:
# QATLAS_RAW_DIR=/srv/quantum-atlas/raw
# QATLAS_PB_DATA_DIR=/var/lib/quantum-atlas/pb_data
```
> 旧名 `RAW_DIR` / `DATA_DIR` / `PB_DATA_DIR` 的 alias 已在 v0.17.0 移除,
> 新写法统一 `QATLAS_*` 前缀。从仓库内布局迁移到 XDG 默认的
> 步骤见 [migration-storage-layout.md](../server/migration-storage-layout.md)。
把状态目录放在 git checkout 外的好处:fresh clone 永远干净(不需要维护
长串 `.gitignore` 规则),`go ./...` 不会撞到 FUSE 挂载,并且符合 XDG /
FHS / 12-factor 的常规约定。
## 服务端组件清单(`internal/` / `cmd/`)
按上面分层对应的 qatlasd 主要 Go 包;都是主仓内的代码,不是独立进程:
| 组件 | 职责 |
|---|---|
| `internal/registry` | PostgreSQL paper registry(`papers` / `paper_assets` / `paper_identities`),去重唯一入口 `ResolveOrMint` |
| `internal/downloader` | Robust Downloader:多范式 PDF 抓取策略阶梯 + 统一验证管线(v0.26.0;builtin `downloader` 插件)|
| `cmd/downloaderproxy` | 独立单容器部署的同款梯子服务——放在有出版社 entitlement 的机器(校园出口),qatlasd 经 `downloader.proxy` 委托;自带 Chrome-for-Testing 浏览器 lane(v0.27.0)|
| `internal/search` | 搜索 Provider 抽象 + catalog/arxiv/openalex 内置 + remote 微服务 client + backend 静态目录(`backendmeta.go`)|
| `internal/userkeys` | 每用户第三方搜索 key 的 AES-256-GCM 加密存储(`search_api_keys` collection;密钥派生自 system PAT)|
| `internal/routes` | HTTP 路由层(papers / search(含 multi + backends)/ downloader / admin assets / me / auth …)|
| `internal/ingest` · `internal/lazyload` · `internal/mineru` | 惰性摄入管线与 MinerU 转换调度 |
## 设计上的取舍
- QuantumAtlas 不把浏览器 OAuth 登录流程内置进应用本体。
- QuantumAtlas 不绑定特定反向代理、SSO 或存储产品。
- `RAW_DIR`、`DATA_DIR`、`PB_DATA_DIR`、`QATLAS_POSTGRES_DSN` 是显式边界,而不是隐含在仓库结构里的假设。
## 延伸阅读
- [storage-architecture.md](storage-architecture.md) — 当 RAW 资产体量上 TB / 引入对象存储(RustFS / S3 兼容)时,raw / metadata 各层怎么分工、怎么对账、怎么重建。
- [migration-storage-layout.md](../server/migration-storage-layout.md) — 把 raw / data / pb_data 从仓库内搬到 XDG / 挂载点的实操步骤。
- [data-flow.md](data-flow.md) — 一篇论文从 arXiv 到可检索资产的端到端链路。