Skip to content
ES
排序、分页与高亮

排序、分页与高亮

搜索结果展示三要素:排序规则、分页策略和关键词高亮。


排序(sort)

普通字段排序

GET /bank/_search
{
    "query": { "match_all": {} },
    "sort": [
        { "account_number": { "order": "asc" } },
        { "age": { "order": "desc" } }
    ]
}

多字段排序:按数组顺序优先,第一个相同则比较第二个。

💡 最佳实践:一旦指定了 sort,ES 自动停止相关性打分(_score: null),显著提升查询性能。

排序模式(sort mode)

当排序字段有多个值(数组)时,指定取哪个值排序:

GET /_search
{
    "sort": [
        {
            "ratings": {
                "order": "desc",
                "mode": "avg"
            }
        }
    ]
}
mode 说明
min 取最小值
max 取最大值
avg 取平均值
sum 取总和
median 取中位数

地理距离排序

GET /restaurants/_search
{
    "query": { "match_all": {} },
    "sort": [
        {
            "_geo_distance": {
                "location": { "lat": 40, "lon": -70 },
                "order": "asc",
                "unit": "km",
                "distance_type": "plane"
            }
        }
    ]
}
参数 说明
unit 距离单位:km / m / mi / yd
distance_type arc(默认,球面距离,精确)/ plane(平面距离,较快)

Script 排序

GET /products/_search
{
    "query": { "match_all": {} },
    "sort": [
        {
            "_script": {
                "type": "number",
                "script": {
                    "lang": "painless",
                    "source": "doc['price'].value * (1 - doc['discount'].value / 100)"
                },
                "order": "asc"
            }
        }
    ]
}

保持评分的同时排序

GET /_search
{
    "query": {
        "function_score": {
            "query": { "match": { "title": "手机" } },
            "functions": [{ "field_value_factor": { "field": "sales" } }],
            "boost_mode": "sum"
        }
    },
    "sort": [
        { "_score": "desc" },
        { "price": "asc" }
    ]
}

分页

基础分页(from + size)

GET /bank/_search
{
    "query": { "match_all": {} },
    "from": 10,
    "size": 10
}
参数 说明 默认值
from 跳过的文档数(偏移量) 0
size 返回的文档数 10

🚨 陷阱:ES 对 from + size 上限为 max_result_window默认 10000from: 9990, size: 20 会直接报错。此限制可通过 index.max_result_window 调高,但强烈不推荐——应当使用 search_afterscroll

深度分页的性能问题

ES 是分布式系统,分页执行过程:

1. Coordinating Node 向每个分片请求 from + size 条文档
2. 每个分片排序后返回 top(from+size) 给协调节点
3. 协调节点在内存中对 (分片数 × (from+size)) 条数据重新排序
4. 截取 [from, from+size) 返回

🔬 深入原理:假设 5 个主分片,查 from: 10000, size: 20。每个分片返回 10020 条文档,协调节点需对 50100 条数据进行排序,然后丢弃前 10000 条,只保留 20 条返回。这正是深度分页低效的根源。

search_after — 实时深度分页(推荐)

利用上一页最后一条文档的排序值作为游标,避免 skip 开销。

# 第一页
GET /logs/_search
{
    "size": 10,
    "query": { "match": { "level": "error" } },
    "sort": [
        { "@timestamp": "asc" },
        { "_id": "asc" }     // ⚠️ 必须有 tiebreaker(_id 是唯一的)
    ]
}

# 响应中每条 hit 都有 "sort": [1718123456789, "doc-xxx"]
# 下一页:用上一页最后一条的 sort 值
GET /logs/_search
{
    "size": 10,
    "query": { "match": { "level": "error" } },
    "sort": [
        { "@timestamp": "asc" },
        { "_id": "asc" }
    ],
    "search_after": [1718123456789, "doc-xxx"]
}
特性 search_after from+size scroll
实时性 ✅ 实时 ✅ 实时 ❌ 快照(滞后)
性能 ✅ 高效 ❌ 深度页慢 ✅ 快(快照)
随机跳页 ❌ 不支持 ✅ 支持 ❌ 不支持
依赖 排序值必须唯一 大量堆内存
推荐度 ⭐⭐⭐ 首选 ⭐ 浅分页 ⭐ 批量导出

🚨 陷阱:search_after 要求排序值全局唯一。如果只用时间戳,同一毫秒可能有多条文档导致数据丢失。始终在 sort 数组末尾加 _id_doc 作为 tiebreaker。

scroll — 快照分页(批量导出专用)

创建数据快照,适合一次性批量导出,不适合实时搜索

# 创建 scroll,设定快照存活时间
POST /bank/_search?scroll=5m
{
    "size": 100,
    "query": { "match_all": {} }
}

# 翻页(每次翻页都会重置存活时间)
POST /_search/scroll
{
    "scroll": "5m",
    "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAA..."
}

# 用完必须清理
DELETE /_search/scroll
{
    "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAA..."
}

# 清理所有 scroll
DELETE /_search/scroll/_all

🚨 陷阱:scroll 占用的堆内存不会在超时前释放。未清理的 scroll 是内存泄漏的常见原因。监控 GET /_nodes/stats/indices/searchopen_contexts 指标。

Point In Time (PIT) — ES 7.10+ 新方式

PIT 是 scroll 的轻量级替代,不锁资源:

# 创建 PIT
POST /bank/_pit?keep_alive=5m
# → { "id": "46+xBAAAA..." }

# 使用 PIT 搜索(省略索引名)
GET /_search
{
    "size": 100,
    "query": { "match_all": {} },
    "pit": {
        "id": "46+xBAAAA...",
        "keep_alive": "5m"
    },
    "sort": [{ "@timestamp": "asc" }, { "_id": "asc" }],
    "search_after": [1718123456789, "doc-xxx"]
}

# 释放 PIT
DELETE /_pit
{ "id": "46+xBAAAA..." }

💡 最佳实践:实时分页用 PIT + search_after(ES 7.10+),批量导出用 scroll(尽快清理),浅分页用 from + size


折叠(Collapse)— 按字段去重

类似 SQL 的 DISTINCT,对某个字段去重,每组只返回最相关的一条。

GET /orders/_search
{
    "query": { "match": { "product": "手机" } },
    "collapse": {
        "field": "user_id",
        "inner_hits": {
            "name": "other_orders",
            "size": 5
        }
    }
}
参数 说明
field 折叠字段,必须为 keyword 类型
inner_hits 获取每组内更多文档(折叠组展开)
max_concurrent_group_searches 最大并发组内搜索数

高亮(highlight)

GET /bank/_search
{
    "query": {
        "match": { "firstname": "Yvette" }
    },
    "highlight": {
        "fields": {
            "firstname": {
                "pre_tags": ["<em>"],
                "post_tags": ["</em>"],
                "number_of_fragments": 1,
                "fragment_size": 100
            },
            "city": {}
        }
    }
}

image-20260610114722223

高亮三种类型

类型 适用场景 性能 配置
unified 默认,通用场景 中等 fragment_sizenumber_of_fragments
plain 简单场景 不支持 fragment_size
fvh (Fast Vector Highlighter) term_vector: with_positions_offsets 的字段 最快 支持多片段、短语高亮

高亮核心参数

参数 说明 默认值
pre_tags / post_tags 高亮标签 <em> / </em>
number_of_fragments 返回片段数,0 = 返回整个字段 5
fragment_size 每个片段的字符数 100
require_field_match 高亮字段必须与搜索字段一致 true
no_match_size 未匹配时返回的字段前缀长度

🚨 陷阱:默认 require_field_match: true,即搜索 firstname 高亮 city 不会生效。跨字段高亮需设为 false

自定义高亮标签

GET /bank/_search
{
    "query": { "match": { "address": "mill lane" } },
    "highlight": {
        "pre_tags": ["<mark class='hl'>"],
        "post_tags": ["</mark>"],
        "fields": {
            "address": {
                "require_field_match": "false",
                "fragment_size": 150,
                "number_of_fragments": 3
            }
        }
    }
}

高亮性能优化

⚡ 性能提示:

  • 高亮文本字段(text)需要读取 _sourceterm_vector 来定位匹配位置
  • 对需要频繁高亮的关键字段,建议在 Mapping 中启用 term_vector: with_positions_offsets
  • 长文本高亮建议限制 fragment_size,避免返回整个字段
// 为高亮优化 Mapping
{
    "mappings": {
        "properties": {
            "content": {
                "type": "text",
                "term_vector": "with_positions_offsets"
            }
        }
    }
}

常见陷阱

陷阱 说明
🚨 from + size > 10000 超过 max_result_window 直接报错
🚨 search_after 排序值不唯一 可能导致数据丢失,末尾加 _id 保底
🚨 scroll 不清理 内存泄漏的常见原因,用完务必 DELETE
🚨 scroll 不感知后续写入 scroll 是快照,新建或被删除的文档不可见
🚨 高亮字段与搜索字段不一致 require_field_match: false 解决
🚨 collapse 字段非 keyword text 字段不可用于折叠去重
🚨 长文本高亮不设 fragment_size 可能返回整个文档内容