ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Solr 深度分页实战:用 cursorMark 替代 start 翻页的配置与验证

Solr 深度分页实战:用 cursorMark 替代 start 翻页的配置与验证 1. 从一次翻页超时说起start 深翻到底慢在哪如果你维护过基于 Solr 的搜索服务大概率遇到过这种反馈前几页秒开翻到几百页之后接口开始变慢上千页直接超时。问题往往不在数据量本身而在start这个参数的工作方式。Solr 深度分页的典型场景是后台导出、全量巡检、数据对账这类需要遍历海量结果集的业务用户不会只看前 10 条而是要把几百万条命中结果一条条走完。start1000000rows10这种查询Solr 并不是「跳到第 100 万条」再取 10 条而是要先在内部把前 1000010 条记录排好序然后丢掉前面 1000000 条只返回最后 10 条。排序字段的检索开销可以优化但前 100 万条的排序过程省不掉。更麻烦的是分布式模式每个分片都要返回自己前 1000010 条的排序字段汇聚节点再合并排序网络传输和内存聚合的代价随start线性上涨。这就是为什么单机还能忍集群一上深度分页就崩。cursorMark换了个思路。它不记录「第几条」而是记录「上一页最后一条的位置」下次查询从这个位置继续往后取。服务端不保存任何游标状态游标本身就是一个编码后的排序位置标记客户端拿着它就能接着翻。这样每次查询都只需要在索引里定位到游标位置再顺序取rows条代价和翻第几页无关。本文就围绕cursorMark的配置骨架、查询写法以及用 curl 对比start与cursorMark的实测动作展开帮你把这套机制真正跑通。2. 前置准备TaoToken 接入与 Solr 环境确认在动手改查询之前先把两件事理清楚一是 Solr 侧要确认版本和字段二是如果你打算用统一的模型/编码辅助来生成和调试这些查询脚本可以先把 TaoToken 的接入配好。TaoToken 是一个聚合多家大模型能力的 API 平台适合在写 Solr 排障脚本、生成对比测试代码时当辅助工具用官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。Solr 侧的前置条件其实就三条缺一条cursorMark都会报错第一排序必须包含唯一键字段通常是id并且是严格升序或降序。原因是 Lucene 内部文档 id 在分布式环境下不保证全局唯一顺序如果排序字段有大量相等值游标定位会漂移导致漏数据或重复。所以排序里必须带一个能唯一定位的字段。第二任何带cursorMark的请求start必须为 0。这不是建议是硬性要求传了非 0 的start会直接抛异常。第三第一次请求传cursorMark*之后每次用上一次返回的nextCursorMark作为新的cursorMark。你可以先用一条简单查询确认环境正常比如访问http://localhost:8983/solr/你的core/select?q*:*rows1sortidasc能返回结果说明 Solr 在跑。接下来我们进入配置和查询写法。3. 可复制配置solrconfig.xml 骨架与查询参数写法3.1 solrconfig.xml 里的 requestHandler 骨架cursorMark本身不需要在solrconfig.xml里开开关它是/select请求处理器原生支持的参数。但为了控制深度分页的行为建议显式配置一个专用的 requestHandler把默认rows、sort和超时约束固定下来避免客户端乱传。下面是一个可复制的骨架requestHandler name/deepPage classsolr.SearchHandler lst namedefaults str nameechoParamsexplicit/str int namerows1000/int str namesortid asc/str str namewtjson/str int nametimeAllowed30000/int /lst lst nameinvariants str namestart0/str /lst /requestHandler这里有几个点值得说明。invariants里把start固定为 0客户端就算传了别的值也会被覆盖从配置层面杜绝误用。defaults里的sort给了默认排序id asc但客户端仍可覆盖只要保证排序里含唯一键即可。timeAllowed设 30 秒防止某个分页请求卡死拖垮整个节点。rows默认 1000深度遍历时单页大一点能减少请求次数但别超过内存承受范围。3.2 查询参数sort 与 cursorMark 的配套写法查询的核心是sort和cursorMark必须配套。排序字段要写成字段名 asc或desc多个字段用逗号分隔最后一个字段建议是id。比如按时间倒序再按 id 升序sortcreate_time desc,id asc注意id asc放在最后保证整体排序唯一。如果只写create_time desc同一秒内的大量记录顺序不确定游标就会出问题。第一次请求q*:*sortid ascrows1000cursorMark*返回结果里会多出一个nextCursorMark字段形如AoEjRGV...这样的一串编码。下一次请求把它原样带上q*:*sortid ascrows1000cursorMarkAoEjRGV...如此循环直到返回的nextCursorMark和传入的cursorMark相同或者返回行数小于rows说明已经到底。这里有个容易忽略的细节cursorMark的值里可能包含、/、这些字符放进 URL 时必须做 URL 编码否则会被截断或解析错误。用 curl 时建议用--data-urlencode交给它处理。4. 验证请求curl 对比 start 与 cursorMark 耗时光看文档不够得实测。下面用 curl 做两组对比一组用start深翻一组用cursorMark顺序遍历观察耗时差异。4.1 start 深翻的耗时测量先测一个较大的start比如 500000取 10 条curl -s -o /dev/null -w start深翻耗时: %{time_total}s\n \ http://localhost:8983/solr/你的core/select?q*:*start500000rows10sortidascwtjson多跑几次取平均。你会看到耗时明显高于首页查询而且随着start增大耗时基本线性上升。如果换成分布式集群这个数字会更夸张因为汇聚节点要合并各分片返回的大量排序字段。4.2 cursorMark 遍历的耗时测量cursorMark没法直接「跳到第 50 万条」它的优势在于顺序遍历时每页耗时稳定。写一个循环脚本连续翻 500 页每页 1000 条记录总耗时#!/bin/bash CORE你的core ROWS1000 CURSOR* TOTAL0 PAGES500 for ((i1; iPAGES; i)); do RESP$(curl -s http://localhost:8983/solr/$CORE/select \ --data-urlencode q*:* \ --data-urlencode sortid asc \ --data-urlencode rows$ROWS \ --data-urlencode cursorMark$CURSOR \ --data-urlencode wtjson) NEXT$(echo $RESP | grep -o nextCursorMark:[^]* | cut -d -f4) if [ $NEXT $CURSOR ]; then echo 已到末尾共 $i 页 break fi CURSOR$NEXT done实测下来cursorMark每页耗时基本持平不会因为翻到后面而变慢。这就是无状态游标的价值每次查询的代价只和rows有关和已经翻了多少页无关。4.3 校验结果不重不漏性能之外正确性更重要。校验方法是用cursorMark遍历时把每页返回的id收集起来最后统计总数和去重后的数量是否一致。如果两者相等说明没有重复再和q*:*的numFound对比如果相等说明没有遗漏。# 收集所有 id 到文件 all_ids.txt CURSOR* while true; do RESP$(curl -s http://localhost:8983/solr/$CORE/select \ --data-urlencode q*:* \ --data-urlencode sortid asc \ --data-urlencode rows1000 \ --data-urlencode flid \ --data-urlencode cursorMark$CURSOR \ --data-urlencode wtjson) echo $RESP | grep -o id:[^]* | cut -d -f4 all_ids.txt NEXT$(echo $RESP | grep -o nextCursorMark:[^]* | cut -d -f4) [ $NEXT $CURSOR ] break CURSOR$NEXT done echo 总条数: $(wc -l all_ids.txt) echo 去重后: $(sort -u all_ids.txt | wc -l)两个数字一致且等于numFound就说明这套游标遍历是可靠的。如果去重后变少通常是排序字段不唯一导致的如果总数少于numFound检查是不是中途cursorMark编码出错或提前终止。5. 本篇常见错排查5.1 报错 CursorMark requires a sort with a unique key这是最常见的报错原因是sort里没有包含唯一键字段。Solr 要求排序能唯一定位每条记录否则游标无法确定「下一条」在哪。解决办法是在sort末尾加上id asc确保整体排序唯一。如果你的业务字段本身就能唯一比如订单号也可以用它但建议还是带上id兜底。5.2 报错 start parameter must be 0 when using cursorMark带cursorMark的请求里start必须为 0。很多客户端框架默认会带上start或者分页组件自动计算了start导致冲突。检查请求参数把start去掉或显式设为 0。前面在solrconfig.xml的invariants里固定start0就是为了防这个。5.3 翻页结果重复或遗漏如果校验时发现 id 有重复八成是排序字段存在大量相等值且没有唯一键兜底。比如只按create_time desc排序同一毫秒的记录顺序在两次查询间可能变化游标定位就会漂移。加上id作为最后排序字段即可解决。另外如果遍历过程中索引有写入结果集本身在变也可能导致重复或遗漏深度遍历建议在索引相对稳定的时段做。5.4 cursorMark 值被 URL 截断cursorMark是 Base64 编码的字符串含、/、。直接拼进 URL 时会被解析成空格可能被截断。用 curl 的--data-urlencode或者客户端做 URL 编码。如果手动拼接记得对游标值做 encodeURIComponent 处理。5.5 分布式环境下 nextCursorMark 为空正常情况下每页都会返回nextCursorMark。如果为空检查是不是查询本身没有命中结果或者rows设成了 0。还有一种情况是请求被路由到了错误的 core确认 URL 里的 core 名称正确。6. 把游标遍历接进你的业务链路cursorMark的价值不只是单次查询快而是它让「全量遍历」这件事变得可预期。你可以把它接进数据导出任务、对账脚本、离线巡检流程每次拿nextCursorMark继续任务中断了也能从上次的游标恢复因为服务端不存状态游标本身就是断点。如果你在写这些脚本时需要辅助生成代码或排查报错可以用 TaoToken 的模型对话能力快速验证思路入口在 https://taotoken.net/api 配合 API Keys 页面 https://taotoken.net/api-keys 拿到密钥即可调用。对于长期跑编码任务和 Agent 流程的场景Coding Plan https://taotoken.net/coding-plan 会更合适接入文档在 https://taotoken.net/doc 可以查到具体参数。回到 Solr 本身最后给一个实用建议深度遍历时把rows设大一点比如 1000 到 5000减少请求次数同时用fl只返回必要字段降低单页传输量。排序字段尽量选索引良好的字段避免在游标遍历时触发昂贵的排序计算。这套组合下来几百万条结果的遍历可以从「不敢跑」变成「定时跑」。
返回列表