5.3 结果整理:排序、分页、高亮与建议 本节摘要:命中之后还有四道整理工序:排序决定顺序(按分数、按字段或组合)、分页决定窗口(浅翻页用 from 加 size,深翻页要换 searchafter)、高亮把命中词标红、建议器兜住拼错的查询词。本节附上深分页的三种方案对比与一次真实选型,读完命中之路即告收官。 命中之后的整理台 排序:不只有分数 默认按相关性分数降序。业务要"最近创建优先"或"优先级高的在前"时,改排序键: sort 是数组,先按优先级、同级按创建时间、再按分数——把 score 放末位,等于给同序文档一个相关性兜底。两个细节:排序的字段最好是 keyword、数值或日期形态(text 不能直接排序,走 3.1 的 raw 子字段);
本节摘要:命中之后还有四道整理工序:排序决定顺序(按分数、按字段或组合)、分页决定窗口(浅翻页用 from 加 size,深翻页要换 search_after)、高亮把命中词标红、建议器兜住拼错的查询词。本节附上深分页的三种方案对比与一次真实选型,读完命中之路即告收官。
默认按相关性分数降序。业务要"最近创建优先"或"优先级高的在前"时,改排序键:
GET tickets/_search { "query": { "match": { "title": "退款" } }, "sort": [ { "priority": { "order": "asc" } }, { "created_at": { "order": "desc", "format": "strict_date_optional_time" } }, "_score" ] }
sort 是数组,先按优先级、同级按创建时间、再按分数——把 _score 放末位,等于给同序文档一个相关性兜底。两个细节:排序的字段最好是 keyword、数值或日期形态(text 不能直接排序,走 3.1 的 raw 子字段);按时间排序的典型陷阱是同毫秒并列,导致翻页时同一文档重复出现——补一个唯一键(文档 id)做末级排序即可根治。排序模式下 _score 变成 null?不,只有纯 filter 查询才没有分数;改了 sort 后分数仍计算,只是不用于排序,想省这点计算就加 track_total_hits 与过滤上下文的组合。
from 加 size 是浅水区泳姿:from 跳过多少条、size 取多少条。5.1 讲过它的病根——翻到第 N 页时每个分片要回"from 加 size"条候选。默认结果窗口上限一万(from 加 size 不得超过),这个限制保护的不是理念,是集群的内存。深水区的标准泳姿是 search_after:用上一页最后一条的排序值当游标,下一页从游标之后接着取:
GET tickets/_search { "query": { "match": { "title": "退款" } }, "sort": [ { "created_at": "desc" }, { "_id": "asc" } ], "size": 100, "search_after": [ 1724112000000, "doc-1041" ] }
GET tickets/_point_in_time { "indices": "tickets", "keep_alive": "5m" }
GET tickets/_search { "size": 100, "query": { "match": { "title": "退款" } }, "sort": [ { "created_at": "desc" }, { "_id": "asc" } ], "pit": { "id": "要填上一请求返回的点快照id", "keep_alive": "5m" }, "search_after": [ 1724112000000, "doc-1041" ] }
游标泳姿的关键是排序键唯一且稳定(时间戳加 id 的组合),翻页期间新写入文档会挤进窗口——点快照把索引视图冻结五分钟,翻页期间的世界不再变动。这套组合是导出与无限滚动的正解;老的滚动查询接口仍是海量导出的备选,语义是"快照遍历",同一张票别混用。

高亮在阶段二进行:命中文档取回后,引擎重新定位命中词条,按配置包上标签:
GET tickets/_search { "query": { "match": { "title": "退款" } }, "highlight": { "fields": { "title": { "pre_tags": ["<mark>"], "post_tags": ["</mark>"], "fragment_size": 80, "number_of_fragments": 2 } } } }
命中后响应里的 highlight.title 形如: 用户申请<mark>退款</mark>,处理速度太慢 长文本被切成约80字符的片段 最多两段 前端直接渲染
三个实用档位:fragment_size 控制片段长度(标题类字段设大些甚至关闭分片);number_of_fragments 控制段数;字段名用通配符星号可一次给多字段开高亮,但代价随字段数上升——只给用户真正看的字段开。高亮实现默认要在阶段二重读原文并重定位词条,大响应下成本可观,列表页高亮两三个字段是合理边界。
用户把"退款"敲成"退宽",term 建议器基于编辑距离给出纠正候选:
GET tickets/_search { "suggest": { "my_suggest": { "text": "退宽", "term": { "field": "title" } } } }
响应 options 里给出候选:退款(基于倒排表词频的高频词)score更高 前端可展示 您是不是想搜 退款
term 建议器从字段的真实词条里挑编辑距离近的高频词,"您是不是想搜"就是它的产物;completion 建议器做前缀补全(搜索框下拉),需要建专门的补全字段。两者都是查询侧的体验件,不改动写入路径。
⚠️ 常见坑:导出脚本用 from 加 size 一万一页地翻,翻到上限被拒后把结果窗口调大硬闯——分片与协调节点的内存压力随之而来。正确动作是换游标方案,窗口上限是护栏不是路障。
💡 关键直觉:排序键的唯一性决定翻页的正确性。任何"翻页偶现重复或漏行"的缺陷,先检查排序末级是不是唯一键,再看快照一致性。
| 考核点 | 达标标准 |
|---|---|
| 排序末级键 | 说明为什么任何排序都要补唯一键,缺了会怎样 |
| 翻页选型口诀 | 浅翻页、滚到底、全量导出三种场景对号入座 |
| 高亮边界 | 说出片段长度与段数参数的含义及字段数代价 |
| 建议器分工 | 区分 term 纠错与 completion 补全各自的用途 |
命中之路走完。下一章换一双统计的眼睛:不问"哪几条命中",问"这批数据长什么样"——聚合分析的旅程开始。