Elasticsearch 学习之 Search After 结果分页显示:TaoToken 统一 Key 接入与 settings.json 配置骨架
1. 为什么 fromsize 翻到第 100 页就崩了Elasticsearch 的from size分页在浅层翻页时很舒服写起来也直观但它的代价是每个分片都要取出from size条文档再在协调节点归并排序后丢掉前from条。也就是说你翻到第 100 页、每页 20 条时每个分片实际要捞 2000 条5 个分片就是 10000 条文档参与排序最后只返回 20 条。翻得越深浪费越大这就是深分页的性能塌陷。更麻烦的是index.max_result_window默认只给到 10000。超过这个窗口ES 会直接抛Result window is too large你连翻都翻不动。很多同学第一反应是把max_result_window调大但这只是把墙往后挪内存和 CPU 的消耗并没有消失反而更容易把集群拖垮。Search After 解决的就是这个问题。它不按「第几页」来定位而是按「上一页最后一条的排序值」来定位相当于给你一个 live cursor。ES 只需要从游标位置往后取size条不需要跳过前面所有数据深分页的代价从 O(fromsize) 降到接近 O(size)。代价是它只能顺序向后翻不能随机跳到第 N 页也不能直接回退这在对「下一页」体验要求高的场景里完全够用。这篇就聚焦一件事在本地用 TaoToken 统一 Key 打通请求通道配好settings.json骨架然后跑一次可复现的 Search After 分页验证把 sort 字段、search_after 游标、PIT 参数这三样东西真正用起来。2. TaoToken 前置统一 Key 与 API 通道准备Search After 的验证本身不复杂麻烦的是请求通道。如果你同时要调 ES、调模型做结果摘要、再跑个 coding agent 写脚本每个服务一套 Key、一套地址配置散落在各处排查问题时根本不知道是哪一层挂了。我习惯把这类外部调用统一收口到 TaoToken一个 Key 走 API 通道settings.json里只维护一份配置。TaoToken 在这里扮演的是统一接入层你拿到一个 Key就能通过它的 API 通道访问模型对话、coding plan 等能力地址是https://taotoken.net/api。注意 API 地址不带任何查询参数保持干净。官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content第一次接触可以先从官网了解整体能力。具体到操作你需要先拿到 Key。打开控制台创建 API Key路径是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。创建时建议按用途命名比如es-search-after-demo方便后面在settings.json里对应。Key 只在创建时完整显示一次复制后立刻存到本地环境变量或配置文件别贴在聊天窗口里。如果你后面想用模型对话来辅助分析分页结果可以走https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite如果打算长期跑编码或 Agent 任务Coding Plan 在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite遇到参数不确定时以文档为准。注意Key 属于敏感凭据不要写进会提交到 Git 的明文文件。下面settings.json里我用占位符你替换成自己的值即可。3. 可复制的 settings.json 配置骨架下面这份骨架把 TaoToken 的 Key、API 地址以及 ES 的连接信息、分页参数放在一起。你可以直接复制替换YOUR_TAOTOKEN_KEY、YOUR_ES_HOST等占位符。结构上分成taotoken、elasticsearch、paging三块职责清晰后面排障时一眼能看出是哪一层的问题。{ taotoken: { api_base: https://taotoken.net/api, api_key: YOUR_TAOTOKEN_KEY, timeout_ms: 30000, default_model: gpt-4o-mini }, elasticsearch: { host: http://localhost:9200, index: bank, username: , password: , verify_certs: false }, paging: { mode: search_after, size: 10, keep_alive: 5m, sort: [ { balance: asc }, { _id: desc } ], max_pages: 50 } }几个字段值得展开说。paging.mode固定为search_after方便你在代码里做分支判断未来要切回fromsize只改这一处。paging.size是每页条数Search After 场景下建议不要设太大10 到 50 之间比较稳。paging.keep_alive是 PIT 的存活时间只有用 PIT 时才生效5m表示 5 分钟内游标有效超时后 PIT 会被回收需要重新开。paging.sort是 Search After 的核心。它必须和请求里的sort完全一致而且排序字段组合要能唯一确定一条文档。只用balance排序是不够的因为余额可能重复重复值会导致游标定位歧义翻页时可能漏数据或重复数据。所以我在balance后面补了_id作为 tiebreaker_id在单个索引内唯一这样排序结果稳定游标才可靠。max_pages是我加的保护字段防止脚本因为游标没推进而无限循环。真实项目里这个值按业务上限设比如最多翻 50 页就停。4. 验证请求sort、search_after 游标与 PIT 参数配置就绪后先确认 ES 能通。用 curl 打一个最简单的请求curl -XGET http://localhost:9200/bank/_search \ -H Content-Type: application/json \ -d { size: 2, query: { match_all: {} }, sort: [ { balance: asc }, { _id: desc } ] }预期返回里hits.hits有 2 条文档每条都带一个sort数组比如[1000, 1001]这就是这一页最后一条的游标值。注意_id是字符串balance是数值游标数组里的类型要和sort定义一致顺序也不能乱。拿到游标后发第二页请求把上一页最后一条的sort值填进search_aftercurl -XGET http://localhost:9200/bank/_search \ -H Content-Type: application/json \ -d { size: 2, query: { match_all: {} }, search_after: [1000, 1001], sort: [ { balance: asc }, { _id: desc } ] }这里有个硬性约束用search_after时from必须为 0 或不传。如果你同时写了from: 10和search_afterES 会报错。预期返回是紧接着上一页之后的 2 条hits.hits[0].sort应该大于上一页的游标值且两页之间没有重叠。上面是不带 PIT 的写法适合索引数据不频繁变更的场景。如果翻页过程中有写入或删除游标可能错位这时候用 PIT 更稳。先开一个 PITcurl -XPOST http://localhost:9200/bank/_pit?keep_alive5m返回里会有id字段这就是 PIT ID。然后带着它查询注意此时请求路径不再带索引名索引信息由 PIT 承载curl -XGET http://localhost:9200/_search \ -H Content-Type: application/json \ -d { size: 2, query: { match_all: {} }, pit: { id: YOUR_PIT_ID, keep_alive: 5m }, sort: [ { balance: asc }, { _id: desc } ] }翻下一页时search_after照旧填上一页最后一条的sort值pit.id保持不变。全部翻完后主动关闭 PIT 释放资源curl -XDELETE http://localhost:9200/_pit -H Content-Type: application/json -d {id: YOUR_PIT_ID}实测下来PIT 的价值在于给整个翻页过程一个一致的数据视图游标不会因为中途的写入而漂移。代价是 PIT 会占用资源keep_alive别设太长翻完就关。5. 本篇常见错排查第一个高频错误是Result window is too large。如果你在 Search After 请求里不小心带了from或者代码里沿用了旧的分页逻辑就会撞上这个。检查请求体确保from为 0 或不存在search_after和from不要同时出现。第二个是游标类型不匹配。比如sort里balance是数值你传search_after: [1000, 1001]把数值写成了字符串ES 会报解析错误或返回空结果。对照上一页返回的sort数组逐位检查类型和顺序。第三个是排序字段不唯一导致翻页重复或漏数据。只按balance排序时余额相同的文档顺序不稳定游标可能定位到错误位置。解决办法就是像配置里那样补一个唯一性字段做 tiebreaker_id是最省事的选择。第四个是 PIT 过期。keep_alive设了5m但你的脚本跑得慢超过 5 分钟再翻页PIT 已被回收请求会报search_phase_execution_exception或提示 PIT 不存在。要么调大keep_alive要么在脚本里捕获异常后重新开 PIT 并重置游标。第五个是 Key 或地址配错。如果请求根本没到 ES先看settings.json里elasticsearch.host是否可达再看 TaoToken 的api_base是否写成了带路径的形式。API 地址就是https://taotoken.net/api不要自己拼/v1之类的后缀。Key 无效时通常返回 401对照控制台里创建的 Key 重新复制一次。6. 把通道和分页都收口到一处走到这里你手上应该有一份能跑的settings.json、一组验证过的 curl 请求以及一套排障清单。Search After 的关键就三样稳定的sort组合、正确的search_after游标、需要一致性时加 PIT。把这三样固定进配置深分页就不再是性能黑洞。通道层面我建议把 Key 和地址统一交给 TaoToken 管理settings.json里只留一份taotoken配置。需要新建或轮换 Key 时去https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite操作参数细节查接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite想先用模型对话验证分页结果的分析逻辑走https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite如果要把这套分页脚本纳入长期编码或 Agent 工作流Coding Plan 在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。一个 Key、一份配置翻页和调用都不用来回切换。