### 1.2. Paper bulk search `GET /graph/v1/paper/search/bulk` 批量取回论文的基础数据:文本查询可选、支持布尔逻辑,每次最多 1,000 篇,用 `token` 续页。 | 参数 | 必填 | 说明 | |---|---|---| | `query` | 是 | 布尔查询,语法见下 | | `token` | 否 | 上一页返回的续页游标 | | `sort` | 否 | `paperId` / `publicationDate` / `citationCount`,可加 `:asc` / `:desc`,可多键 | | `fields` | 否 | 逗号分隔的字段投影,见 §3 | | `limit` | 否 | 默认 1000、上限 1000 | | 过滤参数 | 否 | 与 1.1 相同 | ```bash curl "https://fetchpaper.com/graph/v1/paper/search/bulk?query=neural+machine+translation+AND+attention&fields=paperId,title,year&sort=citationCount:desc" ``` ```json {"total": 1254, "token": "eyJvIjoxMDAwLCJoIjoiYWRkMGVhZDExNGQ1N2M4NiJ9", "data": [{"paperId": "204e3073870fae3d05bcbc2f6a8e263d9b72e776", "title": "Attention is All you Need", "year": 2017}, {"paperId": "bddc714813a5331505eaf308a10a8a6cafe273d4", "title": "Show, Attend and Tell: Neural Image Caption Generation with Visual Attention", "year": 2015}, …]} ``` **布尔语法**:`+` AND、`|` OR、`-` NOT、`()` 分组、`"…"` 短语、`word*` 前缀;相邻词按 AND。 `~N` 模糊后缀被忽略(按精确词处理)。 **切词边界决定「被丢弃」还是「匹配不到」**:子句内部标点(`.` `,` `-` 乃至 NUL)是词元分隔符, 分出来的**单字符词元按分词规则直接丢掉**;空格分开的是两个子句,某子句一个可查词元都没有时 不算被忽略、算「这一支匹配不到」,整条 AND 就是 0 篇。实测(`year=2015`):`networ` 6 篇、 `networ.k` 6 篇(`.` 后的 `k` 被丢)、`networ\0k` 6 篇(NUL 同样只是分隔符),但 `networ k` 与 `networ+k` 都是 **0 篇**(`k` 自成一子句)。**空格和标点在这里不等价。** **停用词不进索引**(`the` / `of` / `method` / `also` / `between` 都在表里),规则两条: - 混在布尔表达式里时这个叶子**直接被丢掉**:`state of the art` 就是 `state + art`(436,407 篇), `graph - the` 等于 `graph`(382,637 篇),`graph | of` 也等于 `graph`。 - 整条查询只剩停用词时(`method`、`-the`、`the | of`)等同于**空查询 = 全库**,一定撞 `Search returned too many hits (… of 10000000)` 那条 400——它说的是「没有可查的词」, 不是「词太常见」。同一个 `query=method` 在 `search` 端回 `total: 0`,两端对纯停用词查询口径相反。 **短语是先 AND 取候选、再逐篇校验词序**,校验与否只看这些词 AND 后有多少候选: **超过 5,000 就静默等价于 AND**。年份/引用/venue 过滤在校验之后才叠加,**加过滤救不回来** (`model collapse` AND = 11,919 > 5,000,`year=2026` 下带不带引号主体都是 AND)。 实测常见短语带引号的 `total` 只比不带引号**略小**(`year=2026` 下 `"model collapse"` 4,696 篇 vs `model collapse` 5,002 篇)。要让短语真按词序收紧, 只能让短语本身的词更稀有:2024-2026 下 `"graph rag"` 194 篇 vs `graph rag` 1,654 篇、 `"key-value cache"` 206 篇 vs `key value cache` 1,429 篇。 常见词的短语基本都退化:`"graph embeddings"` 29,306 vs 29,418、`"deep networks"` 307,506 vs 307,704、`"neural networks"` 708,203 vs 708,430、 `"attention mechanism"` 83,489 vs 83,704。**加引号不是开关,退化才是常态**; 拿不准时对比带/不带引号的 `total`,差值很小时就说明已退化成 AND。 **一条查询的规模有三道上界**(本服务自己的界,官方是否等价未实测),超限一律 400 + JSON,不半帧、不 500: | 上界 | 文案 | 说明 | |---|---|---| | 括号嵌套 64 层 | `Query nested too deeply (max 64 parenthesis levels)` | 64 层够任何真实写法(第 64 层照常返回精确 `total`) | | 折叠后分支 256 个 | `Query has too many terms (300, max 256); split it into several requests` | 数折叠后的真实工作量:同层一模一样的分支先塌(`graph\|graph\|…` 写 1,000 遍是一个分支),同类嵌套先摊平(`a\|(b\|c)` = `a\|b\|c`);同词在不同分支(`a\|(b&a)`)各算一次 | | 累计 postings 20,000,000 条 | 与 §1.1 逐字相同(只差条数) | 两个端点共用一个数。前一道管「分支多但每个小」,这道管「分支少但每个巨大」 | **分页靠 `token`**:把返回的 `token` 原样带回 `?token=…` 就是下一页。`limit` 默认 1000、上限 1000, token 存「下一页起始下标」,步长等于**这一页用的 `limit`**;中途改 `limit` 不报错,但页与页会重叠或跳过。 **`token` 绑查询语义,不绑书写形式**: - 同查询换写法照用:`venue=A,B` 的 token 在 `venue=B,A`、`venue=A,B,A`、`venue=A&venue=B`、 换大小写或塞空格后仍能续页;`publicationTypes`、`fieldsOfStudy` 同理(这三个过滤都是集合语义, 顺序/重复不改结果;`venue=` 空值与不写 `venue` 也是同一查询)。实测 `query=graph neural network&year=2019-2022&venue=,`(精确 `total` 489, 页宽 100):第 1 页 token 在上述每种写法下都给同样那 100 行,整趟翻完逐行相同。 - 语义真换了才拒:改 `query` 的词、改 `year` 区间、增删过滤值、加 `minCitationCount` / `openAccessPdf` / `publicationTypes`、从缺省到显式给 `sort`、**改排序方向** (`citationCount:desc` → `asc`)或**换多键排序的键序**,一律 400 `{"error": "Invalid token"}`。 排序不参与折叠——键序和方向本身就是语义。 - `fields` 不在指纹里:改投影不改「哪几行属于这一页」,同一 token 继续用。 **`token` 的形状也是契约**:`{"o": 下标, "h": 指纹}` 的 base64url。`o` 必须是非负整数、`h` 必须是字符串—— 写成 `"100"` / `1.5` / `true` / `-1`、缺 `o` 或缺 `h`、整份 base64 解出来不是 JSON 对象,都回 400 `Invalid token`,不会按猜出来的行号悄悄发一页。下标**越过结果集末尾不报错**:手工写成 5000 / 10⁹ / 2⁶³ 都是 200 + 空 `data` + `token: null`,`total` 照报——按「这一页没有 token」停机即可,不用预判总页数。 **整片下载请把翻页当一个整体核**,别抽查某一页。实测(同一切片各翻两遍,页宽差 10 倍): `query=attention&year=2020` 的 32,724 篇,`limit=1000`(33 页)与 `limit=100`(328 页)的 `paperId` 序列**逐位相同**,条数正好等于 `total`、无重复;其余小切片(如 `graph rag&year=2026` 的 833 篇)同样如此。 并列键也稳:`citationCount:desc` 在同引用数成千上万的切片上连翻两遍序列一致,并列区内部按 `corpusId` 升序兜底。全量翻页耗时与第几页无关:`query=graph` 的 382,637 篇翻 383 页,每页成本基本恒定 (查询求值与排序每页重算,不随下标增长)。 **排序** `sort=paperId|publicationDate|citationCount[:asc|:desc]`(默认 `paperId:asc`),四个口径: - `sort=paperId` 是**服务出去的 `paperId` 字符串字典序**(不是数字序):那 2,450 篇无主 sha 的论文用 corpusid 字符串排在同一字典序里。`paperId` 在库里不重复,所以这一列没有并列,结果与 token 翻页都确定。 - **可以多键**:`sort=citationCount:desc,publicationDate:asc`——前一键相同才看后一键。字段名与方向忽略 大小写,省略方向即 `asc`,`sort=` 空串或只有逗号按默认 `paperId:asc`;未知字段回 400 `Invalid sort field '…'; supported: paperid, publicationdate, citationcount`,重复键回 400 `Duplicate sort field '…'`。 - **只有年份、没有具体日期**的论文(`publicationDate` 为 `null`)在 `sort=publicationDate` 的**升序和降序里 都排最后**,不被当成 `0000-00-00` 顶到最前。实测 `query=neural networks&year=1970-1985`:273 篇里 150 篇 没日期,两个方向都正好占末尾 150 格。大切片同样:`query=attention` 的 350,751 篇里 40,478 篇(11.5%) 只有年份,两个方向缺失段都从下标 310,273(= 350,751 − 40,478)开始。 - **所有键都相同**时按入库顺序兜底(等于 `corpusId` 升序),不是按 `paperId` 字符串。想要调用方看得懂的 次级顺序就显式加 `paperId:asc` 作后一键(实测同分簇内 `paperId` 严格升序)。 - 换 `sort` 不改 `total`,只改顺序及每页装哪些条目;token 与排序绑定,翻页中途改方向或键序直接 400。 多键排序是本地能力,官方只见过单键例子(未实测),别当 parity。 `total` 是过滤后的**精确**值;命中超过 1,000 万回 400。**过滤与排序列缺数据时按「用到与否」分**: 缺哪张表,只有需要它的查询回 500(文案点名哪张表和重建命令,见 §0);不需要的照常 200。 默认(按 `paperId`)排序依赖排序列,缺它时 `sort=publicationDate:desc` / `citationCount:desc` 仍能答; `openAccessPdf` 只依赖开放获取那张。年份/引用/日期三列缺了整个 `bulk` 端点才废——这条路径没有备用方案,宁缺勿错。