Elasticsearch 学习笔记(四):进阶查询与优化

写在前面

本文是 Elasticsearch 学习笔记系列的第四篇,介绍进阶查询技巧、分词器、中文分词、搜索调优和索引性能优化。前置知识:聚合分析(第三篇)。


一、深度分页问题

1.1 from/size 的问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from + size 的工作方式:
  假设 5 个分片,请求 from=9990, size=10

  1. 协调节点向 5 个分片各请求前 10000 条
  2. 每个分片返回 10000 条(共 50000 条)
  3. 协调节点合并排序 50000 条
  4. 取第 9991-10000 条返回

  从 = 9990 时,实际处理了 50000 条数据
  from 越大,性能越差

  ES 默认限制 from + size <= 10000

1.2 解决方案

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
方案1:search_after(推荐,实时翻页)
  适合:UI 上的"加载更多"/"下一页"
  特点:基于排序值定位,性能恒定,不能跳页

方案2:scroll(适合批量导出)
  适合:全量数据导出、数据迁移
  特点:创建快照,不适合实时查询

方案3:增大 max_result_window(不推荐)
  PUT /products/_settings
  { "index.max_result_window": 50000 }
  只是提高上限,深分页性能问题没解决

1.3 search_after 详解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 第1次查询
GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "desc" },
    { "_id": "asc" }
  ]
}
// 返回结果中最后一条的 sort 值:["2026-03-15", "42"]

// 第2次查询
GET /products/_search
{
  "size": 100,
  "sort": [
    { "created_at": "desc" },
    { "_id": "asc" }
  ],
  "search_after": ["2026-03-15", "42"]
}
// 基于上一页最后的排序值继续查

// 注意:sort 字段必须唯一(用 _id 兜底)

二、分词器详解

2.1 分词流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
输入文本 → Character Filters → Tokenizer → Token Filters → 倒排索引

Character Filters(字符过滤器):
  - 去除 HTML 标签
  - 字符替换
  - 正则替换

Tokenizer(分词器):
  - 按规则拆分为 Token(词元)
  - standard:按单词边界切分
  - whitespace:按空格切分

Token Filters(词元过滤器):
  - 小写转换
  - 去停用词(the, a, is)
  - 词干提取(running → run)
  - 同义词替换

2.2 内置分词器

1
2
3
4
5
6
standard      — 标准分词器(默认),按单词边界切分 + 小写
simple        — 按非字母字符切分 + 小写
whitespace    — 按空格切分(不做小写)
keyword       — 不分词,整体作为一个 Token
pattern       — 按正则表达式切分
language      — 特定语言分词(english、french 等)

2.3 测试分词效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 使用 _analyze API 测试
GET /_analyze
{
  "analyzer": "standard",
  "text": "华为 Mate 60 Pro 手机"
}
// 返回分词结果:["华为", "mate", "60", "pro", "手", "机"]

// 指定索引的某个字段分析器
GET /products/_analyze
{
  "field": "name",
  "text": "华为 Mate 60 Pro"
}

// 查看索引字段的分词结果
GET /products/_doc/1/_termvectors?fields=name

三、中文分词

3.1 问题:standard 对中文效果差

1
2
3
4
5
standard 分词器处理中文:
  "华为旗舰手机" → ["华", "为", "旗", "舰", "手", "机"]

  问题:每个字作为一个词,无法按词搜索
  搜索 "旗舰" 时,"旗" 和 "舰" 分别在不同的文档中也会匹配

3.2 IK 分词器

1
2
3
4
5
安装(ES 8.15):
  ./bin/elasticsearch-plugin install \
    https://github.com/infinilabs/analysis-ik/releases/download/v8.15.0/elasticsearch-analysis-ik-8.15.0.zip

  安装后重启 ES

3.3 IK 两种模式

1
2
3
4
5
ik_max_word   — 最细粒度切分(索引时用)
  "华为旗舰手机" → ["华为", "旗舰", "手机"]

ik_smart      — 最粗粒度切分(搜索时用)
  "华为旗舰手机" → ["华为", "旗舰", "手机"]

3.4 使用 IK 分词器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建索引时指定
PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "description": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

// analyzer          — 索引时使用的分词器(细粒度,多建倒排索引)
// search_analyzer   — 搜索时使用的分词器(粗粒度,精确匹配)

3.5 自定义词库

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 扩展词典(添加新词)
// config/IKAnalyzer.cfg.xml
<properties>
  <entry key="ext_dict">custom.dic</entry>
</properties>

// custom.dic(每行一个词)
Mate60
鸿蒙
麒麟芯片

3.6 IK 分词测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "华为旗舰手机搭载麒麟芯片"
}
// ["华为", "旗舰", "手机", "搭载", "麒麟", "芯片"]

GET /_analyze
{
  "analyzer": "ik_smart",
  "text": "华为旗舰手机搭载麒麟芯片"
}
// ["华为", "旗舰", "手机", "搭载", "麒麟芯片"]

四、相关性评分调优

4.1 评分机制(BM25)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ES 默认使用 BM25 算法计算相关性评分

影响因素:
  1. 词频(TF)     — 搜索词在文档中出现的次数越多,分数越高
  2. 文档频率(IDF) — 搜索词在整个索引中越罕见,分数越高
  3. 字段长度       — 字段越短,匹配到的词越重要

  搜索 "华为":
  文档A:"华为 Mate 60 Pro"(短字段,"华为" 占比高)→ 高分
  文档B:"这是一款华为生产的旗舰手机,搭载麒麟芯片..."(长字段)→ 低分

4.2 function_score(自定义评分)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 搜索 "手机",价格低的排名靠前
GET /products/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "name": "手机" }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "rating",
            "factor": 2,
            "modifier": "sqrt"
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

// modifier 选项:
//   none      — 不处理
//   log       — 取对数(压缩范围)
//   sqrt      — 平方根
//   square    — 平方
//   reciprocal — 1/x

4.3 boosting(提升/降低权重)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 搜索 "手机",华为品牌加权,小品牌降权
GET /products/_search
{
  "query": {
    "boosting": {
      "positive": {
        "bool": {
          "must": [
            { "match": { "name": "手机" } }
          ],
          "should": [
            { "term": { "brand": { "value": "华为", "boost": 2 } } }
          ]
        }
      },
      "negative": {
        "term": { "brand": "山寨" }
      },
      "negative_boost": 0.5
    }
  }
}
// positive 匹配的正常评分
// negative 匹配的评分 × 0.5

4.4 constant_score

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 不计算相关性评分,所有匹配的文档评分都一样(适合纯过滤)
GET /products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "category": "手机" }
      },
      "boost": 1.2
    }
  }
}

五、索引性能优化

5.1 Bulk 批量写入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
最佳实践:
  - 每批 1000-5000 条文档
  - 批量大小 5-15MB
  - 多线程并发写入

// 批量写入示例
POST /_bulk
{"index":{"_index":"products","_id":"1"}}
{"name":"华为 Mate 60","price":6999}
{"index":{"_index":"products","_id":"2"}}
{"name":"小米 14","price":3999}

5.2 Refresh 和 Flush

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
写入流程:
  1. 写入 Index Buffer
  2. Refresh(默认 1 秒)→ Buffer 写入新的 Segment → 可搜索
  3. Flush → Segment 持久化到磁盘 + 清除 Translog

调优:
  - 提高刷新间隔(批量写入时)
    PUT /products/_settings
    { "index.refresh_interval": "30s" }

  - 批量写入完成后恢复
    PUT /products/_settings
    { "index.refresh_interval": "1s" }

  - 批量导入时可以设为 -1(关闭自动刷新)
    导入完成后手动刷新:POST /products/_refresh

5.3 Translog 配置

1
2
3
4
5
6
7
PUT /products/_settings
{
  "index.translog.durability": "async",
  "index.translog.sync_interval": "30s"
}
// async:异步刷盘,性能更好但有丢失风险
// request:每次请求都刷盘(默认,安全但慢)

5.4 索引设计优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1. 合理设置分片数
   - 小索引(< 1GB):1 个分片
   - 中等索引(1-50GB):3-5 个分片
   - 大索引(> 50GB):按 30-50GB/分片计算

2. 避免过多字段
   - 字段过多 → Mapping 膨胀 → 性能下降
   - 不需要搜索的字段设置 "index": false
   - 不需要聚合的字段设置 "doc_values": false

3. 使用 _source 过滤
   - 不需要返回的大字段可以用 store + _source excludes

4. 索引按时间滚动
   - logs-2026.05.01, logs-2026.05.02
   - 方便删除过期数据(直接删整个索引)

5.5 查询优化

1
2
3
4
5
6
1. 用 filter 代替 query(不评分,会缓存)
2. 避免深分页(用 search_after)
3. 只返回需要的字段(_source 过滤)
4. 避免用 wildcard/regexp 前缀通配符(如 *华为)
5. 控制返回的桶数量(terms 的 size)
6. 避免在高基数字段上做聚合

六、Kibana 使用技巧

6.1 Dev Tools

1
2
3
4
5
Kibana Dev Tools 快捷键:
  Ctrl + Enter    — 执行当前请求
  Ctrl + /        — 注释/取消注释
  Ctrl + Space    — 自动补全
  Ctrl + ↑/↓      — 跳转到上/下一个请求

6.2 Discover(数据浏览)

1
2
3
4
5
1. 选择索引模式(如 logs-*)
2. 设置时间范围
3. 用 KQL 或 Lucene 语法过滤
4. 添加字段列显示
5. 保存搜索条件

6.3 Dashboard(仪表盘)

1
2
3
4
5
6
7
8
1. 创建可视化(Visualization)
   - 柱状图、饼图、折线图、数据表格
   - 基于聚合结果展示

2. 组合到 Dashboard
   - 多个可视化组件排布
   - 全局时间过滤
   - 共享给团队

七、小结

本文学习了进阶查询与优化:

  • 深度分页问题(search_after、scroll)
  • 分词器详解(Analyzer、Tokenizer、Token Filter)
  • 中文分词(IK 分词器、自定义词库)
  • 相关性评分调优(BM25、function_score、boosting)
  • 索引性能优化(Bulk、Refresh、Flush、分片设计)
  • 查询优化技巧
  • Kibana 使用技巧

下一篇将学习集群运维:节点角色、分片分配、ILM 和监控排查。