qatlas-search — QuantumAtlas 的多源学术检索微服务¶
面向学术检索的独立微服务:客户端发请求,服务端做 multi-source fan-out (arXiv / OpenAlex / Semantic Scholar / Crossref / qatlasd 论文目录 / qatlas-rag 语义检索),按 精准词项匹配 + 引用量排序;可选地再让 LLM 对 top hits 写一段结论。pipeline 刻意简单:不加 agent 就直接返回命中列表。
与向量 RAG(qatlas_rag)并列的另一条检索路径:RAG 走向量相似度,本服务走
严格文本匹配(标题 / 摘要 / 元数据精准词项)+ 年均引用率加权,无需 GPU、
无需向量库。qatlas-rag 服务也可以作为一个 fan-out 源接入本服务(rag backend,
默认关闭,见下文"对 qatlas-rag 的依赖")。
架构¶
客户端 (qatlas-search CLI / qatlas 插件 / web)
│
▼
qatlasd ── 用户 auth、计量、身份锚定都在这里完成
│ Authorization: Bearer <search.remote.token>
▼
qatlas-search (本服务) ── fan-out 到各数据源 → 融合排序,可选 LLM 总结
│ (fused 混合勾选时,网页命中经 reinject 二轮反喂学术源)
│
▼
学术源: arXiv / OpenAlex / Semantic Scholar / Crossref / PubMed /
Europe PMC / DBLP / DOAJ / OpenAIRE / CORE / NASA ADS / Springer /
IEEE Xplore / Scopus / qatlasd 论文目录 (catalog) / qatlas-rag (rag)
网页源: Wikipedia / SearXNG(自托管) / Tavily / Exa / Serper / Brave / Kagi
本服务只接受内部网络请求(compose 示例不做任何 ports 映射),用共享的
service_token 校验调用方;用户级鉴权、配额计量、论文身份锚定均由 qatlasd 负责。
接口协议¶
POST /v1/search
请求: {"query": str = "", "max_results": int = 10,
"sources": [str] | null, "agent": bool = false,
"ranking": "auto" | "scorer" | null,
"scorer": {"language", "filter"?, "score"} | null,
"explain": bool = false,
"mode": "fused" | "multi" = "fused",
"api_keys": {backend: key} | null,
"arxiv_id": str | null, "doi": str | null, "title": str | null}
响应(fused 默认/auto): {"hits": [{title, abstract, authors, year, doi, arxiv_id, url,
venue, citations, source, score, score_detail,
raw_rank, raw_score}],
"conclusion": str | null,
"ranking": {"source": "llm"|"default",
"weights": {...} | null},
"usage": {"llm_tokens": int},
"errors": {backend|intent|agent|reinject: msg}}
响应(multi): {"results": {backend: [hit, ...]}, # 每个平台一份原始
# 命中列表,不做跨源合并/排序
"conclusion": null,
"usage": {"llm_tokens": 0},
"errors": {backend: msg}}
GET /v1/backends → {"backends": [{name, label, category,
requires_key, user_key, available}]}
# backend 目录:qatlasd 据此渲染前端复选框;
# available 只反映服务端 key/配置,不含请求级 key
POST /v1/survey-search → 有界多查询规划及规则过滤(独立接口,不接受 scorer 参数)
GET /v1/scoring/capabilities → 版本化评分语法、特征、函数、资源限额、示例及 generation_available
POST /v1/scoring/generate
请求: {"query": str, "requirements": str} # 严格字符串,各 1..4000 字符且不可空白
成功: {"scorer": {"language", "filter", "score"},
"summary": "Model explanation: …", "warnings": [str],
"scorer_hash": str, "feature_version": str, "usage": {"llm_tokens": int}}
失败: {"detail": {"code": str, "message": str, "field"?: str},
"usage": {"llm_tokens": int}} # 保留已知累计用量,不伪计未知 token
# 显式开启,生成并编译但不执行;确认 scorer 后另调现有 /v1/search
GET /healthz → {"status": "ok", "agent_configured": bool,
"backends": {name: ready bool}}
GET /v1/admin/manifest → 配置清单:分节字段(label/type/description)、
当前值(secret 字段打码)、healthz 同款 status
PUT /v1/admin/config → {"updates": {key: value}},写回实际加载的 YAML
配置文件的 search: 段(agent.* 写回嵌套 agent: 子段),
需重启生效
默认/auto 的
score_detail是score的"账单":五个加权分量(lexical / phrase / citation / recency / agreement),之和恒等于score。打分为绝对分—— 固定查询、权重、合并记录与年份时,不依赖其他候选;引用分量按年均引用率(log 饱和)计, 时效分量为 5 年半衰期衰减。multi 模式的原始命中不打分(score 恒 0)。ranking: "auto"(可选,仅 fused 模式)开启逐请求排序意图:服务端用一次 LLM 调用(复用下文agent:端点配置)从查询里抽取权重 θ,与 fan-out 并行、夹紧后供本次打分;失败静默回退代码默认权重并记errors["intent"], 响应ranking块标明实际生效的权重来源。sources为null时用配置里的default_tools;只会选择当前可用的 backend (服务端有 key/配置,或请求api_keys里带了对应 key)。请求了但不可用(缺 key / 缺服务端配置)的 backend 记入
errors,不再静默丢弃。max_results夹取到 1..100;multi 模式下每个 backend 的预算为min(max_results, max_results_per_tool)。api_keys由 qatlasd 注入(当前用户在网页端配置的第三方 key),只覆盖对应 backend,服务端 YAML 里的同名 key 是兜底;本服务不持久化任何请求级 key。mode: "multi"时忽略agent与合法的ranking: "auto"(没有融合后的 top 集合); 不支持ranking: "scorer",该组合返回 400。身份查询(v0.4.0):
query为空而arxiv_id/doi在场时走精确取回而非 空搜索 fan-out——只调能解析身份的源(arXivid_list、OpenAlexfilter=doi:、 Semantic Scholarpaper/arXiv:…/paper/DOI:…单对象端点),其余源静默跳过 (不发请求、不记 error);query与身份全空但有title时按标题搜索;四者 全空返回 400。query非空时身份字段不参与,行为与从前一致。reinject 二轮检索(v0.5.0,仅 fused):
sources同时含网页源与学术源时, 网页命中不进融合排名,而是被解析成 ≤3 条二轮查询——链接里的 arXiv/DOI 标识符经身份端点精确取回,网页标题做成引号短语——只打一轮勾选的学术源 (sources 白名单不扩大;标识符无归属源时丢弃、名额让给标题通道)。发现的 论文并入结果按用户原始查询统一打分。纯网页勾选不触发二轮(网页命中直接 参与排名);二轮失败记errors["reinject"],不影响一轮结果。开关为reinject.py的REINJECT_ENABLED代码常量。配置了
search.service_token时,/v1/search、/v1/survey-search、/v1/backends、/v1/scoring/capabilities、/v1/scoring/generate与/v1/admin/*校验Authorization: Bearer <token>(与 qatlasd 侧search.remote.token同值), 不符返回 401;/healthz始终开放,供探针使用。/v1/admin/*供 qatlasd 管理页代理调用:manifest 中 secret 字段永远打码, 更新里填打码串 / null / 空串表示"不修改";配置文件缺失或只读挂载时 PUT 返回 500。agent: true且服务端已配置 LLM 时,返回里多一段conclusion;agent 失败不会 拖垮响应,而是记入errors["agent"]。
可编程 Scorer(首版)¶
排序模式明确分为三条:缺省/null 用默认公式;ranking: "auto" 保留原 LLM 配权重路径;
ranking: "scorer" 执行调用方提交的受限程序,不自动调用 LLM 写程序。
第一阶段另提供显式 POST /v1/scoring/generate:默认关闭,只生成并编译检查程序,
不检索或执行;调用方确认后再将返回的 scorer 提交现有 /v1/search。
{
"query": "surface code threshold",
"ranking": "scorer",
"scorer": {
"language": "qatlas-expr-v1",
"filter": "lexical >= 0.6",
"score": "log1p(coalesce(citations, 0))"
},
"explain": true
}
六个特征:
lexical、phrase、citations、year、age_years、source_count。 它们是已绑定的标量变量,可在 filter/score 中直接引用;score: "lexical"合法且不自动加入其他评分项。 生成提示词含经过编译/执行回归的完整示例,明确支持coalesce(year, 0) > 2020等年份过滤。 模型错误拒绝不会被静默替换为示例或默认程序。python -m qatlas_search.scorer_acceptance默认纯离线检查固定需求的168个合成样本; 显式加--live才使用当前配置调用真实模型(有费用,不经过网页额度),再做相同的有限样本 验收。离线成功不等于真实模型成功,也不证明全部自然语言需求;详见 Scorer 指南。支持算术、门槛、条件分支和非线性函数;nullable 特征必须显式处理,不强制词法优先。
scorer 在完整合并候选上执行,再截 top-k;保留身份路由和网页反喂规则。
自定义
score为有限浮点值,不强制 0~1、不舍入,score_detail为 null;explain: true增加有界的真实执行解释。响应 ranking 返回实际程序、哈希、特征版本及参考年份。程序错误明确返回 413/422,不回退默认;模式冲突返回 400。
搜索请求体限 64 KiB、UTF-8 JSON、容器嵌套 64;scorer 对象限 4 KiB、256 AST 节点、 深度 32;自定义评分最多 1,000 个合并候选,解释 JSON 列表预算 64 KiB。
本仓提供微服务 HTTP 和 CLI direct 入口;默认 server CLI 会拒绝 scorer/file/explain 参数。qatlasd/网页的端到端接入需另行验证;本服务不提供独立重排、 候选导出或生成后自动执行/优化循环。
生成请求仅接受严格的
query/requirements,各 1..4000 字符且不可空白,整个 body 限 64 KiB。 成功含scorer、带Model explanation:标签的summary、warnings、scorer_hash、feature_version与usage;失败为安全detail对象及已知累计usage。作者/期刊等不支持的 requirements 直接 fail closed;其他不支持或含糊要求交由模型明确拒绝或 clarification,不得静默丢弃。编译不是自然语言语义证明,程序仍需调用方审核。
生成最多两次 completion(一次修复),默认总超时 30 秒;每次 raw 响应默认限 64 KiB, 抽取的输出文本固定限 16 KiB。总输入+输出预算使用 byte-based 保守预留并结合已知上游用量, 未知 token 不得伪计为实际
usage。进程内生成请求默认上限 2;全部
/v1/search(含默认/auto/scorer/multi)共用另一上限 4。 ASGI 层在读取 body、解析 JSON、同步线程池派发前立即取槽,满额 429,不排队。 每 worker / app 独立,多 worker / 副本使总额度倍增;不是分布式限流,也不覆盖 survey。
完整生成契约、错误码、预算与部署限制见 自然语言生成(第一阶段)。
qatlas-search --list-scoring-capabilities
qatlas-search --direct "surface code threshold" --ranking scorer \
--scorer-file docs/examples/scorers/classic.json --explain --json
完整类型、短路规则、错误码及接入说明见 Scorer 使用指南, 另有 近期论文、高引用经典、 年均引用 程序示例。
配置¶
独立的 YAML(不依赖 qatlas 主包),按顺序取第一个存在的文件:
~/.qatlas/config.yaml(单机 / 本地开发)/etc/qatlas-search/config.yaml(容器挂载点)
与 qatlasd 同机部署时:qatlasd 对
config.yaml里的未知 key 严格报错, 两者不能共用一个文件。把本服务的配置写到独立的~/.qatlas/search.yaml, 挂载为容器的/etc/qatlas-search/config.yaml(见deploy/docker-compose.example.yaml)。
只读顶层的 search: 段;缺文件 / 缺段不报错,用内置默认值:
search:
# --- 第三方数据源(均可选)---
semantic_scholar_api_key: # 提高 S2 速率上限(也可由用户 key 注入)
openalex_api_key: # OpenAlex 免费账号 key(匿名额度已收紧)
openalex_email: you@example.com # 进 OpenAlex polite pool
crossref_email: you@example.com # 进 Crossref polite pool
pubmed_api_key: # 可选;NCBI 限速 3→10 req/s
# --- 需 key 的 backend(服务端兜底 key;qatlasd 正常会注入用户自己的 key)---
core_api_key:
ads_api_key: # NASA ADS
springer_api_key:
ieee_api_key:
scopus_api_key:
tavily_api_key: # 以下为网页引擎
exa_api_key:
serper_api_key:
brave_api_key:
kagi_api_key:
searxng_url: http://searxng:8080 # 自托管 SearXNG(需在实例上开启 json 输出)
# --- catalog backend(qatlasd 论文目录)---
server_url: http://qatlasd:8090
token: <qatlasd-side token>
# --- rag backend(qatlas-rag 语义检索;默认关闭,详见下文"对 qatlas-rag 的依赖")---
rag:
enabled: true
url: http://qatlas-rag:8801
token: <qatlas-rag-side token>
timeout: 30.0 # 可选;缺省回落到 request_timeout
# --- 服务层 ---
service_token: <与 qatlasd 的 search.remote.token 同值>
service_port: 8600
# --- 可选 LLM 总结(OpenAI 兼容端点;也可平铺成 agent_enabled 等字段)---
agent:
enabled: true
base_url: https://llm.example.com/v1
api_key: sk-...
model: my-model
max_tokens: 1024
# --- 独立 Scorer 生成(平铺字段;复用 agent 的 base_url/api_key/model)---
scorer_generation_enabled: false # 不依赖 agent.enabled
scorer_generation_max_concurrency: 2 # 1..32
scorer_search_max_concurrency: 4 # 1..64,全部 /v1/search
scorer_generation_timeout: 30.0 # 1..120 秒,整次生成总超时
scorer_generation_max_tokens: 1024 # 64..4096,每次 completion 输出
scorer_generation_total_tokens: 16384 # 1024..65536,总输入+输出预算
scorer_generation_max_response_bytes: 65536 # 1024..262144,每次 raw 响应
# --- 工具选择 ---
default_tools: arxiv,openalex,semantic_scholar,catalog
max_results_per_tool: 10
request_timeout: 20.0 # 每阶段等待上限;HTTP 调用受剩余预算约束
search_timeout: 50.0 # /v1/search 各联网阶段共享的总等待预算
搜索等待预算与部分结果¶
/v1/search 的首轮 fan-out、可选网页反喂(reinject)和 LLM 总结共享
search_timeout(默认 50 秒)。每阶段最多等待 request_timeout(默认
20 秒),且不得超过整次搜索剩余预算;HTTP 重试、429 的退避、Semantic Scholar
的限速排队以及反喂的多条派生查询均消耗原有预算,不会每次重置。默认预算给 qatlasd
的 60 秒 remote timeout 留出余量。若部署自定义主服务超时,应让它明显大于
search_timeout,预留排序、论文登记和响应传输时间;这不是 CPU/数据库处理的硬超时。
新增 search_timeout 须为 (0, 300] 内有限秒数,旧 request_timeout 的配置兼容性
不变。YAML 中无效的 search_timeout 只回退该字段到 50 秒,不丢弃其余配置及
鉴权 token;admin API 对无效值仍拒绝写入。写入 YAML 后需重启 Search 服务。
慢源不再阻止已有结果返回:超时/工作线程饱和记录在 errors[backend],可选阶段
则记录在 errors.reinject / errors.intent / errors.agent。第二轮后续查询超时
不会丢弃该源已完成的派生查询结果;总结超时仍返回论文列表。HTTP 200 仍可表示部分
成功(甚至全空但有 errors),前端必须展示错误,不应将它当作穷尽结果或自动重试。
这不会绕过第三方 429 限流,也不改变 qatlasd 的配额计量策略。
同步 requests 已开始的网络 I/O 无法强行取消。整个进程共用至多 32 个联网工作
槽,超时返回后仍运行的任务继续占槽,直至真正结束;饱和时拒绝新增工作并显式报错,
不为每次请求无限增建线程。预算到期后不再发起后续重试或派生查询。迟到结果不再合入
已返回的响应;迟到的 LLM 调用仍可能由供应商计费,其未在预算内取得的 token 用量
无法回填到已经返回的响应中。该预算不覆盖 /v1/survey-search 的整次多查询流程;
其每次调用检索引擎受限,但总时限仍由该端点单独管理。
启用 qatlas_search logger 的 INFO 级别可查看 search_stage 的阶段耗时,以及
search_completed 的总耗时、结果数和失败阶段数;这些统计不记录查询文本、API key
或模型提示词。Uvicorn 默认 access log 的 200 OK 本身不能证明在主服务超时前完成。
默认公式的排序权重是代码级配置:打分逻辑与权重(lexical 1.0 / citation 0.6 /
recency 0.2)定义在 src/qatlas_search/scoring.py,不通过 YAML 或
/v1/admin/* 修改——YAML 里写 weight_* 会被当作未知 key 忽略。逐请求的
权重调整用 ranking: "auto"(见上文接口协议)。
Scorer 生成需独立开启 scorer_generation_enabled,且共享的 agent_base_url、
agent_api_key、agent_model 在 strip() 后均非空;不要求 agent_enabled=true。
/v1/admin/manifest 的 scoring section 展示开关、预算及说明,admin 更新验证上述边界并
拒绝 NaN/Inf;写回 YAML 后需重启,不能热更新并发槽。字段均平铺在 search: 下。
DeepSeek 官方 endpoint 的 Scorer 生成自动使用非思考模式:agent_base_url 的 URL hostname 经大小写和末尾点规范化后精确匹配 api.deepseek.com 时,初稿与 repair 的 JSON 顶层均增加 thinking: {"type": "disabled"},不加 reasoning_effort。根地址、/v1 及末尾斜杠均支持,不需改已有 key/model;非官方 OpenAI-compatible 网关/代理 host 保留默认,即使 model 为 deepseek-v4-pro 也不按模型名启用此参数。原 Agentic 总结 payload 不变。依据:DeepSeek 官方 Thinking Mode。紧凑生成提示词仍由 capabilities 投影,完整保留可用 features/functions、拒绝/clarification、nullable 与 domain 约束;默认总超时仍为 30 秒,预算不变,非思考模式和提示词缩短均不是性能保证。
旧 LLM 总结/配权重路径使用 requests;Scorer 生成使用运行时依赖 httpx 的异步客户端
请求 {base_url}/chat/completions。均不依赖 openai / anthropic SDK。
对 qatlas-rag 的依赖¶
rag backend 由独立部署的 qatlas-rag 服务提供(GPU 节点,bge-m3 hybrid 检索本地
论文语料),本服务只作为调用方接入它,不内嵌任何向量检索实现。qatlas-rag 提供的
接口协议为:POST {url}/v1/retrieve(Authorization: Bearer {token},请求体
{"query": str, "max_results": int},响应 {"hits": [...], "errors": {...}},
hit 字段与本服务 /v1/search 的 hit 一致且 source 为 "rag"),另有
GET {url}/healthz 供就绪探测。
接入方法是在配置里声明依赖(见上文配置示例的 rag: 段):enabled: true 加上
url 与 token 三者齐备时 backend 才可用,/healthz 的 backends.rag 就绪位
据此报告;未启用或配置不全时该位为 false,行为与其他可选 backend(如 catalog)
一致。timeout 可单独覆盖 rag 请求的超时,缺省回落到 request_timeout。rag 的
超时、非 2xx 与网络错误都记入响应的 errors["rag"],不影响其他源的命中。注意
default_tools 默认不含 rag:启用后还需把 rag 加进 default_tools,或在
请求的 sources 里显式指定。
版本化说明:本服务自 v0.2.0 起提供 rag backend,并要求 qatlas-rag
侧实施上述 /v1/retrieve 接口协议。各仓独立版本化、独立部署;本仓没有声明支持的
qatlasd / qatlas-rag 软件版本范围,也没有业务调用前的软件版本兼容握手。
qatlasd 当前只检查本服务的 HTTP 健康状态;固定镜像 tag、健康检查通过或服务端
版本号相同都不等于兼容性已验证。升级仍需核对 API/语义、调用方测试及实际集成。
本地开发¶
使用 Python 3.11+(CI / Docker 为 3.12)、uv 0.11.30:
uv sync --locked --extra dev
uv run --no-sync --offline ruff check .
uv run --no-sync --offline python -m pytest
uv lock --check
uv run --no-sync uvicorn qatlas_search.service.app:app --port 8600
首次 sync 可下载锁内依赖;测试执行默认不联网,不读取个人配置 / netrc。
真实外部 API 用例必须显式 uv run --no-sync python -m pytest -m network。
轻量 socket guard 保留 mock、TestClient、localhost 与 Unix socket,不是系统网络沙箱,
不能约束子进程或原生库;新增测试仍应显式 mock 外部调用。
版本与发布开发流程¶
pyproject.toml 的 [project].version 是唯一版本来源;运行时从已安装发行包的
importlib.metadata 取版本(先 sync / 安装项目,不直接依赖未安装的 src)。
Commitizen 的 uv provider 同步 pyproject.toml、uv.lock,version_files 同步根 VERSION。
已发布 tag 仍指向其原始代码 / workflow;后续 CI 的防重发检查不会追溯保护旧 run。
不要通过移动旧 tag、覆盖旧产物或重跑旧版本来应用新的发行流程。
在干净且同步的默认分支 master 上,先预览:
git switch master
git pull --ff-only
uv sync --locked --extra dev
uv run --locked --extra dev cz bump --dry-run
只有获得单独发版授权后才执行:
uv run --locked --extra dev cz bump
git push origin master --follow-tags
日常提交使用 Conventional Commits(feat: / fix: / docs: 等)。cz bump 在本地
增量更新 CHANGELOG、版本与锁文件,跑 Ruff/pytest hook,然后创建版本提交和 annotated
v$version tag;不会自行 push。hook 失败时可能已改文件但尚未 commit/tag:先检查差异
并修复,勿盲目重跑。不要裸跑 cz changelog 重生成历史。保留完整
## vX.Y.Z (YYYY-MM-DD) 标题与非空正文;预发布使用规范 PEP 440(如 0.9.0rc1 /
0.9.0.dev1,不使用 epoch/local),转正需审查是否汇总了预发布的重要说明。
预发布使用下面的 RC 路径代替上面的正式 bump,不能依次执行两套流程:
uv run --locked --extra dev cz bump --prerelease rc --dry-run
# 核对推导出的完整版本与发版授权一致后,才执行并推送:
uv run --locked --extra dev cz bump --prerelease rc
git push origin master --follow-tags
预发布也会真实创建版本提交、tag 并触发镜像上传,不是模拟测试。发布后需检查 Actions 中的测试、镜像与 Release jobs,并核对 GHCR tag / digest、Release 正文及 预发布标记。下载或部署时从 Releases 选择已验证的完整 tag;RC 需显式选择用于测试,不视为稳定版推荐或生产部署授权。
新增 backend(开发者必读)¶
失败契约(v0.5.0 起):
search(query, settings) -> (papers, error)——返回(list[Paper], str | None)元组;任何网络/解析失败返回([], 原因)而非抛 异常。错误只走返回值:实例属性last_error已删除(并发请求共享注册表 单例,错误随返回值走才能保证归属正确)。类属性:
name/label(UI 展示名)/category(academic|web)/requires_key+key_setting(该源 key 在 Settings 里的字段名,请求级api_keys的注入槽)/cost_tier。能按标识符精确取回的源再设supports_identity = True,并在search()开头处理query.identity_only—— 不适用的身份种类返回([], None)静默跳过("帮不上忙"不是"失败")。HTTP 一律经基类
_get/_post(共享重试/退避与 User-Agent);响应解析放_parse。测试约定:mock
base.requests.request断言请求形状;成功路径必须断言err is None。参考tests/test_identity_search.py、tests/test_reinject.py。要加新的执行阶段,参考
reinject.py:目标 backend 由调用方传入(不碰全局 注册表)、同源顺序 + 跨源并行、错误逐查询聚合、engine.run_direct只留一行 接线。
CLI¶
qatlas-search 默认是 server 模式:POST 到 qatlasd 的
/api/search/agentic(由 qatlasd 代理到本服务)。server/token 解析顺序:
--server/--token flag → 插件 CliContext(作为 qatlas search 挂载时,
协议 v2)→ qatlas 客户端 auth(qatlas auth login,惰性 import,
没装 qatlas 也不报错)→ ~/.config/qatlas/config.yaml 顶层 server_url/token。
HTTP 调用优先走 qatlas 客户端的 pluginsupport 公共层(PAT 注入、
版本协商头、TLS 选项)。
qatlas-search "surface code threshold" # server 模式,默认带 agent 总结
qatlas-search "VQE ansatz" --no-agent --max-results 5
qatlas-search --list-tools # 列出 backend 及本地可用性
自动化检索的结果与失败判定¶
普通搜索(server 和 --direct)返回 0 表示有可用结果,或检索正常结束但没有命中;
返回 1 表示请求失败,或结果为空且报告了检索错误;用法/配置错误返回 2。
有结果的部分失败保持 0,调用方仍应检查 errors,不能据此声称已穷尽文献。
这一退出码约定用于普通搜索;survey / multi 保留各自协议。
Direct JSON 除 query / results 外包含 errors、per_backend_counts 和
reinject_counts;每篇保留 abstract、raw_rank 和 raw_score(未知为 null)。
计数来自后端原始返回,可能因合并、过滤和 --top 与输出条数不同。
server JSON 保留服务端原始响应,错误警告也输出到 stderr。
server 文本输出同时显示 qatlas_id=qa_…,可直接用于 qatlas paper get markdown;
direct 模式不生成统一 ID,需先用 qatlas match 或服务端搜索核对身份。
survey / multi 子命令(均走 qatlasd)¶
# 规则化综述检索(POST /api/search/survey):覆盖为 post-filter bounded
qatlas search survey "topological qubits" \
--author "Felix von Oppenheim" --year-from 2020 --min-citations 50
qatlas search survey "surface codes" --query "surface code threshold" \
--query "mbqec" --sort citations --json
# 逐平台原始结果(POST /api/search/multi):各平台自身排序,无融合
qatlas search multi "quantum error correction" --sources arxiv,openalex
--direct 模式¶
qatlas-search --direct '"quantum supremacy" sampling' --tools arxiv,openalex --json
--direct 在本进程内直连各学术数据源(engine.run_direct)。注意其性质:
自带 key、不经过 qatlasd、不计量、不做身份锚定——适合本地开发与调试,
不作为受控环境的入口。
部署¶
镜像由本仓 release 流水线发布到 GHCR(见 .github/workflows/release.yml),不发布 PyPI。
master push / PR / v* tag / 手动运行均先跑锁内 Ruff 和默认离线测试;只有 tag ref
通过测试、精确版本和 CHANGELOG 校验后才发布。手动选择分支只测试。
镜像仅写 ghcr.io/iai-ustc-quantum/qatlas-search:v版本,不写裸版本或 latest,
保留 linux/amd64 + linux/arm64、QEMU、provenance / SBOM 和 GitHub attestation
(私有仓功能权限不足时 attestation 仍为 best-effort)。镜像先于 GitHub Release。
获得部署授权后,部署机只 pull 不 build。将下面占位值替换为 Release 页面中
已经核对过的完整 tag(包含 v),不要照抄历史示例或使用 latest:
cp deploy/docker-compose.example.yaml deploy/docker-compose.yaml
export QATLAS_SEARCH_VERSION='v<已验证的版本号>'
docker compose -f deploy/docker-compose.yaml pull
docker compose -f deploy/docker-compose.yaml up -d
固定 tag 并记录镜像 digest 有助于定位产物,但不保证可回滚。回滚还取决于旧镜像 可获取、配置/数据变更兼容以及实际回滚验证;固定版本也不能代替跨服务兼容性测试。
新版本遵循上面的 Commitizen 流程,不能手改 VERSION 再随手打 tag。 CI 在真实认证后查询 GHCR:仅标准“manifest/name 不存在”响应允许首次 build/push; 已有 tag 会明确停止并给出 digest,网络 / 401 / 5xx / 无法判定均停止,不当成不存在。 同 ref workflow 串行,但 GHCR 不提供原子 compare-and-push;其他发布者也必须遵守 不可覆盖约定,不能并发从外部改写同名 tag。
部分产物失败只恢复、不重发:镜像与 Release 是独立 jobs。镜像成功而 Release 失败时,在同一次 run 选择 Re-run failed jobs,只补 Release,不重建镜像。 不要选择 Re-run all jobs:整次重跑会被已有 tag 的 preflight 拦住。镜像已存在而 run 不可恢复时,先核对原始 tag、镜像 digest 与对应 CHANGELOG,只人工补缺失的 Release; 不要删除/移动 tag、覆盖镜像,或重建同版本冒充原产物。需要改产物则走新的授权版本。 发布与部署分别授权,发布完成不自动更新生产。
示例 compose 把服务接入外部 shared-infra 网络,不做 ports 映射(只允许
网络内访问,流量一律经 qatlasd 进入),并以只读方式挂载
本服务独立的 ~/.qatlas/search.yaml 到 /etc/qatlas-search/config.yaml。
本地开发时仍可以 docker build -t qatlas-search:local . 自建镜像,但这不是
发布路径——线上只允许来自 GHCR 的镜像。Docker 用 uv 0.11.30 按 uv.lock 安装
build group(hatchling 及传递依赖),禁用隔离构建并离线构建当前项目 wheel;随后
sync 仅生产依赖并安装该 wheel,运行阶段保留非 root 用户、不带开发/构建工具。
这不宣称普通 uv build 的隔离依赖天然受锁约束;离开该流程时须另行约束构建工具。
基础 Python / uv 镜像 tag 仍可能被上游更新,锁文件不等于整张镜像逐字节可复现。
已发布镜像的只读验证¶
将 verify-release.yml 合入默认分支后,在 Actions 的 Verify released image 页面
选择 master,输入已存在的完整版本 tag;也可执行:
gh workflow run verify-release.yml --ref master -f tag='v<已发布版本号>'
该流程只用仓库 GITHUB_TOKEN 的包读取权限,拉取 amd64 镜像,校验 index 包含
linux/amd64 与 linux/arm64,然后按记录的 digest 在断网、只读容器中检查安装包版本。
结果和 digest 写入 run summary;不会重建/发布镜像或改写 tag/Release。这只是 CPU
包 metadata smoke,不代表 arm64 运行、真实搜索 API、GPU 或端到端兼容性测试通过。
测试¶
uv sync --locked --extra dev
uv run --no-sync --offline python -m pytest -q
离线覆盖:各 backend 的 _parse(固定 fixture)、身份查询(arXiv id_list /
OpenAlex DOI / S2 paper 端点形状与 identity-only fan-out 收窄)、reinject
(混合门控 / 白名单 / 路由 / 错误聚合 / 契约守卫)、(papers, error) 元组契约
(成功路径断言 err is None)、catalog 与 rag 的请求形状(mock requests)、
rag 的故障隔离与就绪位、排序与去重、config 加载(含嵌套 agent/rag 段拍平)、
agent(mock LLM 端点)、FastAPI 服务(TestClient + 假 backend:命中形状 /
agent 开关 / token 401 / healthz)。带 network 标记的 live 用例默认跳过。
Bounded survey search¶
POST /v1/survey-search accepts goal, an optional queries list (up to 6),
rules (authors, venues, year_from, year_to, min_citations, sort),
sources, max_results, and agentic. The trusted qatlasd gateway may inject
api_keys; keys are never echoed in the plan. sort is relevance/newest/citations.
When agentic is enabled, the configured LLM proposes additional keywords and
bounded scoring weights. Explicit rules remain hard constraints; the LLM cannot
relax them or invent returned papers. Results are filtered from bounded provider
responses, deduplicated and ranked using the existing scoring module. The
response includes the effective plan, per-query counts/errors, actual LLM token
usage and coverage.filter_mode="post_filter_bounded", exhaustive=false.
Missing metadata does not satisfy a filter. This is not exhaustive author/journal
pagination or a global citation-count ranking.
与 Scorer 共存的边界:survey 不接受顶层 ranking、scorer、explain,即使值为
null/false也返回400;需要自定义评分请用 /v1/search,不会忽略程序后使用默认排序。
两条检索路由共享令牌预鉴权、64KiB实际请求体、UTF-8及JSON深度保护;survey验证失败
返回安全的422结构,不回显请求级Key或原始输入。原有survey规划和“先过滤后合并”语义不变。
完整请求、错误及兼容性说明见 Survey 整合指南。
Semantic Scholar request starts (including retries) are paced at least 1 second apart per key across threads in one process. Run one process per S2 key, or use a shared limiter/sticky routing when scaling across processes/replicas. Identity lookups and keyword searches share this gate; key fingerprints are not exposed. The gate applies to all S2 callers (ordinary search, scorer, survey and reinject), not just survey. Queue waiting is outside an individual HTTP operation but now consumes the active retrieval stage/search budget; lock acquisition and pacing stop when that budget expires. Key-slot eviction is not implemented. Concurrent requests sharing a key may therefore return a budget error rather than wait indefinitely. Other providers, including OpenAlex, do not use this gate.
Public clients use qatlasd /api/search/survey. Its agentic mode uses the existing
per-user daily quota and token accounting. Verify the chosen service and gateway
versions together, including existing routes; publishing a service release does
not establish end-to-end compatibility or authorize a deployment.
Author rules normalize name strings (including hyphens/Unicode); they are not ORCID/author-ID disambiguation. Likewise, a journal name may require explicit aliases. Retain researcher-homepage/source review before promoting search hits to a research frontier.