ElasticSearch 9.x 从入门到精通实战教程

Cosolar 10 阅读 技术架构

版本: ElasticSearch 9.4 (2026 年最新版)
更新日期: 2026-07-05
涵盖主题: 索引优化、查询 DSL 编写、ES|QL、向量搜索、性能调优、生产级实战

第一章 ElasticSearch 9.x 新特性与架构概览

1.1 ElasticSearch 9.4 带来了什么

ElasticSearch 9.4 于 2026 年 5 月发布,是 9.x 分支的重要里程碑版本。这个版本围绕三条主线展开:将 ES|QL 打造为一等查询引擎、通过升级 DiskBBQ 算法大幅提升向量搜索吞吐、以及通过 TSDB 合成 ID 降低指标存储开销。

对于开发者而言,9.4 最值得关注的变化包括:

类别 关键特性 实际影响
查询引擎 ES|QL Views、PromQL 支持、时间序列命令 可像查询物理索引一样查询虚拟视图
向量搜索 DiskBBQ 算法升级,过滤查询延迟提升 3 倍以上 语义检索成本大幅下降
存储优化 TSDB 合成 ID,OTLP 指标存储减少 40% 大规模监控场景存储成本显著降低
GPU 加速 GPU 向量索引正式 GA(集成 NVIDIA cuVS) 索引吞吐提升 12 倍,force merge 快 7 倍
量化技术 bbq_disk 成为默认量化模式,支持 1/2/4/7 位配置 新索引自动获得更优的存储与查询平衡
ML 推理 TEXT_EMBEDDING 和 RERANK 正式 GA 语义搜索管线可稳定用于生产环境

向量搜索的改进尤其突出。9.4 中 DiskBBQ 算法在带限制性过滤的场景下,查询延迟改善达到 3 倍以上;通过集成 NVIDIA cuVS,GPU 加速向量索引正式 GA,自管理集群可期待 12 倍的索引吞吐提升和 7 倍的 force merge 加速。同时,semantic_text 字段类型默认采用 BFLOAT16 量化与 bbq_disk 存储,无需额外映射配置即可获得优化。

1.1.1 ES|QL Views 详解

9.4 引入了 Views 功能——将一段 ES|QL 查询封装为虚拟索引,外部可以像查询物理索引一样引用它。你可以在 FROM 子句中引用视图,就像引用物理索引一样,还可以在同一个查询中混合使用视图、索引和通配符。

-- 创建视图:标准化原始日志
CREATE VIEW clean_logs AS
  FROM raw-logs-*
  | RENAME @timestamp AS ts, host.name AS host
  | EVAL level = UPPER(log.level)
  | WHERE level != "DEBUG";

-- 像查询索引一样查询视图
FROM clean_logs
| STATS count = COUNT(*) BY host
| SORT count DESC;

安全限制:当底层索引启用了文档级安全(DLS)或字段级安全(FLS)时,视图无法被查询。这个限制在 ES 安全层强制执行,不是视图定义层面能绕过的。

1.1.2 PromQL 支持

9.4 在 ES|QL 中引入了 PROMQL 源命令(Tech Preview),允许直接在 ES|QL 管道中编写 PromQL 表达式,从存储 Prometheus 格式指标的 Elasticsearch 索引中拉取数据。这不是一个简单的代理——结果会流入 ES|QL 计算引擎,可以通过 SORTLIMITWHERE 等命令进一步处理。

PROMQL index=k8s-downsampled start="2026-02-17T08:00:00Z" end="2026-02-17T09:00:00Z" step=30m
  avg_bytes=(avg(rate(network.total_bytes_in[30m])))
| SORT avg_bytes DESC, step;

在写入端,新增的 POST /_prometheus/api/v1/write 端点接受标准的 Prometheus remote write 二进制协议,因此 Elasticsearch 可以作为 Prometheus 的存储后端直接使用。额外的端点覆盖了即时查询(/query)、范围查询(/query_range)、系列发现(/series)和标签枚举(/labels),全部返回标准的 Prometheus JSON 格式。

1.1.3 TSDB 合成 ID 的存储收益

TSDB 索引在时间序列模式下现在使用合成 ID 而不是索引 _id 字段。实际效果是 OTLP 指标工作负载的存储减少高达 40%,并且在段合并期间 CPU 开销降低,因为不再维护 _id 的倒排索引。

写入时的重复检测由布隆过滤器处理——轻量且快速。以前使用 _id 的查找被委托给其他索引字段,如时间戳或维度字段。查询和检索行为保持不变;该更改对现有查询是透明的。

1.1.4 向量搜索性能提升

DiskBBQ 算法获得了实质性升级。在限制性预过滤场景下,搜索性能提升 3 倍以上。新格式增加了可配置的量化位深度(1、2、4 和 7 位)、原生 SIMD 代码改进,以及通过专家 API 条件化非 IID 向量的方法。bbq_disk(DiskBBQ)现在是新索引的默认量化模式。

// semantic_text 现在默认使用 bfloat16 + bbq_disk
// 无需映射更改;新索引自动获得此优化
PUT my-index
{
  "mappings": {
    "properties": {
      "content": { "type": "semantic_text" }
    }
  }
}

对于 ARM 和 x86,原生内核获得了专门的优化:AVX-512 int8 内核带级联展开、ARM NEON 上 int7u/int8 的 vdotq_s32、BBQ Int4 的 SVE 函数,以及新的 bfloat16 专用和字节向量评分器。零拷贝 SIMD 向量评分现在也在冻结层(可搜索快照)上启用。

1.2 整体架构

ElasticSearch 是一个分布式 RESTful 搜索与分析引擎,基于 Apache Lucene 构建。理解其架构层次是掌握后续所有优化技巧的基础。

image.png

集群中的节点按角色分工:

  • 主节点 (Master Node):负责管理集群状态和元数据(索引创建、分片分配、节点上下线)。生产环境建议配置 3 个专用主节点保证选举多数。
  • 数据节点 (Data Node):存储实际数据并执行增删改查。可以根据数据温度进一步细分为 hot/warm/cold 节点。
  • 协调节点 (Coordinator Node):接收客户端请求,将查询分发到对应分片并合并结果。可以专用节点(node.roles: [])或由数据节点兼任。

在小型集群中,一个节点可以同时承担多个角色,但生产环境建议角色分离,避免资源争抢。

1.3 倒排索引:搜索的基石

ElasticSearch 的全文搜索能力建立在 Lucene 的倒排索引之上。与传统数据库的 B-Tree 按"行→列"存储不同,倒排索引按"词→文档列表"存储,这使得基于关键词的检索复杂度与文档总量无关,只与匹配结果数量相关。

倒排索引的构建过程:
image.png

原始文档:
  Doc 1: "ElasticSearch 是搜索引擎"
  Doc 2: "搜索引擎负责全文检索"
  Doc 3: "ElasticSearch 支持向量搜索"

          ↓ 分析器处理(分词 + 归一化)

词元列表:
  Doc 1: [elasticsearch, 是, 搜索, 引擎]
  Doc 2: [搜索, 引擎, 负责, 全文, 检索]
  Doc 3: [elasticsearch, 支持, 向量, 搜索]

          ↓ 构建倒排索引

倒排索引:
  elasticsearch → [Doc1, Doc3]
  搜索          → [Doc1, Doc2, Doc3]
  引擎          → [Doc1, Doc2]
  向量          → [Doc3]
  全文          → [Doc2]
  ...

分析器在写入时将文本拆分为词元并归一化(小写化、词干提取、停用词过滤等),查询时对查询文本做同样的处理,确保两侧匹配。理解这一点对索引映射设计和查询性能优化至关重要——错误的分词策略会导致"搜得到"但"搜不准"。

1.3.1 倒排索引的内部结构

一个完整的倒排索引由以下几部分组成:

组成部分 说明 示例
Term Dictionary(词典) 所有词元的有序列表,存储在内存中 [elasticsearch, 搜索, 引擎, ...]
Term Index(词索引) 词典的索引结构(FST),快速定位词元在词典中的位置 类似 Trie 树的前缀压缩结构
Posting List(倒排表) 每个词元对应的文档 ID 列表,使用 Roaring Bitmap 压缩 elasticsearch → [1, 3]
Positions(位置信息) 词元在文档中的位置,用于短语查询 搜索 → Doc1:pos2, Doc2:pos0
Offsets(偏移信息) 词元在原文中的起止字符位置,用于高亮 搜索 → Doc1:[6,8]
Payloads(负载) 每个词元的附加信息,如自定义权重 可选,不常用

Roaring Bitmap 压缩:Posting List 中的文档 ID 不是简单的数组,而是使用 Roaring Bitmap 进行压缩。它将文档 ID 分为高 16 位和低 16 位,高 16 位作为容器索引,低 16 位根据密度选择 Array Container(稀疏)或 Bitmap Container(密集)存储。这种设计在保持快速随机访问的同时大幅减少内存占用。

1.3.2 BKD Tree 与 Doc Values

除了倒排索引,Lucene 还维护几种重要数据结构:

BKD Tree:用于数值、地理空间和 IP 字段的范围查询与排序。BKD Tree 是一种多维空间索引结构,特别适合范围查询和最近邻搜索。数值字段(integer、long、float、double、date)默认使用 BKD Tree 索引。

Doc Values:列式存储,用于排序、聚合和脚本访问字段值。倒排索引擅长"词→文档"的正向查找,但不擅长"文档→字段值"的反向查找。Doc Values 弥补了这一缺陷,按文档 ID 顺序存储每个字段的值,使得聚合和排序操作不需要扫描倒排索引。Doc Values 默认对除 text 外的所有字段开启,关闭不需要的字段可节省磁盘空间和堆内存。

// 关闭不需要排序/聚合的字段的 doc_values
{
  "mappings": {
    "properties": {
      "session_id": {
        "type": "keyword",
        "doc_values": false  // 仅用于过滤,不需要聚合
      }
    }
  }
}

1.3.3 FST(Finite State Transducer)词典压缩

Lucene 的 Term Dictionary 使用 FST(有限状态转换器)结构存储。FST 是一种有向无环图,能够高效压缩大量字符串同时支持前缀查询。它的优势在于:

  • 内存效率:百万级词元只需几十 MB 内存
  • 前缀查询:天然支持前缀匹配,是 prefix 查询和 completion 建议器的基础
  • 共享前缀/后缀:相似词元自动共享路径,压缩率极高

第二章 核心概念与快速入门

2.1 安装与启动

2.1.1 Docker 方式(推荐用于开发测试)

# 创建 docker 网络
docker network create elastic

# 启动 ElasticSearch 9.4
docker run -d \
  --name es \
  --net elastic \
  -p 9200:9200 \
  -e discovery.type=single-node \
  -e xpack.security.enabled=false \
  -e ES_JAVA_OPTS="-Xms2g -Xmx2g" \
  docker.elastic.co/elasticsearch/elasticsearch:9.4.3

# 启动 Kibana
docker run -d \
  --name kibana \
  --net elastic \
  -p 5601:5601 \
  -e ELASTICSEARCH_HOSTS=http://es:9200 \
  docker.elastic.co/kibana/kibana:9.4.3

启动后访问 http://localhost:9200 确认集群状态,Kibana 在 http://localhost:5601 提供 Dev Tools 控制台用于交互式查询。

2.1.2 生产环境部署(APT 仓库)

# 1. 导入 GPG 密钥
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

# 2. 添加仓库
echo "deb https://artifacts.elastic.co/packages/9.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-9.x.list

# 3. 安装
sudo apt-get update && sudo apt-get install elasticsearch

# 4. 配置 JVM 堆内存(/etc/elasticsearch/jvm.options)
# -Xms16g
# -Xmx16g

# 5. 配置 elasticsearch.yml
cat > /etc/elasticsearch/elasticsearch.yml << 'EOF'
cluster.name: my-cluster
node.name: node-1
network.host: 0.0.0.0
discovery.seed_hosts: ["host1", "host2", "host3"]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
EOF

# 6. 启动
sudo systemctl enable elasticsearch
sudo systemctl start elasticsearch

生产环境务必开启安全认证xpack.security.enabled=true)并配置 TLS。

2.1.3 集群健康检查

# 查看集群健康状态
curl -X GET "localhost:9200/_cluster/health?pretty"

# 响应示例
{
  "cluster_name": "my-cluster",
  "status": "green",           // green=健康, yellow=副本缺失, red=主分片缺失
  "timed_out": false,
  "number_of_nodes": 3,
  "number_of_data_nodes": 3,
  "active_primary_shards": 15,
  "active_shards": 30,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 0,
  "delayed_unassigned_shards": 0
}

2.2 核心概念速览

概念 关系型数据库类比 说明
Index(索引) Database / Table 文档的集合,7.x 后一个索引对应一种类型
Document(文档) Row JSON 格式的数据单元,通过 _id 唯一标识
Field(字段) Column 文档中的键值对,有明确的数据类型
Mapping(映射) Schema 定义字段的类型、分析器等配置
Shard(分片) Partition 索引的水平拆分单元,分主分片和副本
Segment(段) - Lucene 中的不可变存储单元,倒排索引的物理载体
Replica(副本) Read Replica 主分片的拷贝,提供高可用和读取负载均衡
Alias(别名) View 指向一个或多个索引的软链接,用于零停机重建索引

关键区分:Shard 是 ElasticSearch 层面的路由单元(决定文档存哪个节点),Segment 是 Lucene 层面的存储单元(实际的倒排索引文件)。一次写入先进入内存缓冲区,refresh 后生成新的 Segment,merge 后合并为更大的 Segment。

2.2.1 分片的工作机制

每个分片本质上是一个独立的 Lucene 索引,拥有完整的倒排索引、Doc Values 等数据结构。文档写入时,ElasticSearch 根据以下公式路由文档到对应分片:

shard = hash(routing) % number_of_primary_shards

其中 routing 默认是文档的 _id,也可以自定义。这意味着主分片数量在索引创建后无法修改(只能 reindex 到新索引),因为修改后路由结果会变化,原有文档将无法被找到。

2.2.2 写入流程详解

image.png

客户端写入请求
      │
      ▼
协调节点接收 ──→ 根据 routing 计算目标分片
      │
      ▼
主分片所在节点
      │
      ├──→ 1. 写入 Indexing Buffer(内存缓冲区)
      ├──→ 2. 写入 Translog(事务日志,持久化到磁盘)
      │
      ▼  (refresh_interval 到期,默认 1 秒)
      │
  Refresh 操作
      │
      ├──→ 3. Indexing Buffer 生成新的 Segment(内存中)
      ├──→ 4. Segment 刷新到磁盘(但尚未 fsync)
      │
      ▼  新文档变为可搜索
      │
      ▼  (flush 操作,默认每 30 分钟或 translog 达到 512MB)
      │
  Flush 操作
      │
      ├──→ 5. Segment fsync 到磁盘(确保持久化)
      ├──→ 6. 清空 Translog
      │
      ▼  (同时,主分片将写入同步到副本分片)
      │
副本分片所在节点
      │
      └──→ 同样执行写入流程

关键时间点:

  • refresh(1 秒):新文档变为可搜索。这是 ElasticSearch "近实时"的来源。
  • flush(30 分钟或 512MB translog):确保数据持久化到磁盘。
  • translog:在 flush 之前,通过 translog 保证崩溃恢复时不丢失数据。

2.3 第一次索引与查询

2.3.1 创建索引并定义映射

PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "refresh_interval": "1s"
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "category": { "type": "keyword" },
      "tags": { "type": "keyword" },
      "create_time": { "type": "date" },
      "description": { "type": "text", "analyzer": "ik_max_word" }
    }
  }
}

字段说明

  • name 使用 text 类型支持全文搜索,同时通过 fields.keyword 子字段支持精确匹配和聚合排序
  • price 使用 scaled_float 而非 double,内部以 long 存储(乘以 scaling_factor),避免浮点精度问题且节省空间
  • categorytags 使用 keyword 类型,不分词,适合精确过滤和聚合
  • create_time 使用 date 类型,支持范围查询和日期直方图聚合

2.3.2 批量写入文档

POST /_bulk
{ "index": { "_index": "products", "_id": "1" } }
{ "name": "ElasticSearch 实战指南", "price": 89.90, "category": "图书", "tags": ["技术", "搜索"], "create_time": "2026-06-01" }
{ "index": { "_index": "products", "_id": "2" } }
{ "name": "无线蓝牙耳机 Pro", "price": 599.00, "category": "数码", "tags": ["音频", "无线"], "create_time": "2026-06-15" }
{ "index": { "_index": "products", "_id": "3" } }
{ "name": "机械键盘 RGB 背光", "price": 329.00, "category": "数码", "tags": ["外设", "机械"], "create_time": "2026-07-01" }

Bulk API 是最高效的批量写入方式,一次请求可以包含多个 index/update/delete 操作。每行是一个操作指令,下一行是对应的数据体。

2.3.3 执行全文搜索查询

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "无线 耳机" } }
      ],
      "filter": [
        { "range": { "price": { "lte": 1000 } } }
      ]
    }
  },
  "sort": [
    { "_score": "desc" },
    { "create_time": "desc" }
  ],
  "size": 10
}

这个查询展示了 ElasticSearch 查询的核心模式:bool 组合查询中 must 用于全文匹配并计算相关性得分,filter 用于精确过滤且不参与打分。filter 上下文的结果会被节点级缓存,相同条件的后续查询直接命中缓存,延迟显著降低。

2.3.4 常用 REST API 速查

操作 方法 路径 说明
创建索引 PUT /{index} 创建索引并可选定义映射
删除索引 DELETE /{index} 删除索引及其所有数据
查看映射 GET /{index}/_mapping 查看索引的字段映射
修改设置 PUT /{index}/_settings 修改索引的动态设置
写入文档 POST /{index}/_doc 自动生成 _id
写入文档 PUT /{index}/_doc/{id} 指定 _id
获取文档 GET /{index}/_doc/{id} 按 _id 获取单个文档
更新文档 POST /{index}/_update/{id} 部分更新
删除文档 DELETE /{index}/_doc/{id} 按 _id 删除
批量操作 POST /_bulk 批量 index/update/delete
搜索 GET/POST /{index}/_search 执行查询
聚合统计 GET /{index}/_count 文档计数
强制合并 POST /{index}/_forcemerge 合并 Segment
刷新 POST /{index}/_refresh 强制 refresh

第三章 索引设计与映射

映射是性能的起点——错误的字段类型选择会在集群规模扩大后变成无法修复的债务。

3.1 字段类型选择策略

ElasticSearch 提供了丰富的字段类型,选择正确的类型直接影响存储效率、查询性能和功能可用性。

3.1.1 核心类型决策表

场景 推荐类型 原因
全文搜索(商品名称、文章标题) text + keyword 子字段 text 支持分词检索,keyword 支持精确匹配和聚合排序
精确值(状态、分类、标签) keyword 不分词,倒排索引体积小,聚合快
金额价格 scaled_float 用 long 存储,避免浮点精度问题,比 double 节省空间
整数计数器(有范围查询需求) integer / long 支持 range 查询,BKD Tree 索引
自增 ID(无范围查询需求) keyword keyword 查询比 numeric 更快
时间戳 date 支持范围查询和日期直方图聚合
布尔值 boolean 存储为单字节,查询效率高
IP 地址 ip 支持 CIDR 范围查询
嵌套对象(需要独立查询) nested 保持对象间的关系,避免扁平化导致的交叉匹配
大对象(不需索引) object + enabled: false 仅存储不索引,节省空间
地理坐标点 geo_point 支持距离查询和边界框查询
地理形状 geo_shape 支持复杂多边形查询
二进制数据 binary Base64 存储,不索引

3.1.2 数值类型详解

┌─────────────────┬──────────┬────────────────────────────────────────┐
│ 类型             │ 大小     │ 适用场景                                │
├─────────────────┼──────────┼────────────────────────────────────────┤
│ byte            │ 1 字节   │ -128 到 127                            │
│ short           │ 2 字节   │ -32768 到 32767                        │
│ integer         │ 4 字节   │ 通用整数,计数器、年龄                  │
│ long            │ 8 字节   │ 大整数,时间戳(内部存储)              │
│ float           │ 4 字节   │ 单精度浮点,对精度要求不高              │
│ double          │ 8 字节   │ 双精度浮点                             │
│ scaled_float    │ 8 字节   │ 金额、评分(long + scaling_factor)     │
│ unsigned_long   │ 8 字节   │ 0 到 2^64-1,无符号长整型               │
└─────────────────┴──────────┴────────────────────────────────────────┘

为什么金额用 scaled_float 而不是 double

scaled_float 内部以 long 存储,值 = 原始值 × scaling_factor。例如价格 89.90 元,scaling_factor=100,存储为 8990。好处是:

  1. long 比 double 节省空间(相同位数下 long 不需要存储指数部分)
  2. 整数比较比浮点数比较快
  3. 避免浮点精度问题(0.1 + 0.2 ≠ 0.3 的问题)
  4. BKD Tree 对整数索引效率更高

3.1.3 dynamic mapping 的陷阱

ElasticSearch 默认开启动态映射,字符串字段会被自动映射为 text + keyword 双字段。这意味着每个字符串字段都同时建立了倒排索引和 BKD Tree,在字段数量多的场景下(如日志)会导致映射爆炸和磁盘浪费。

// 默认行为:每个字符串字段都被双重索引
PUT /logs-default
{ "mappings": { "dynamic": true } }

POST /logs-default/_doc
{
  "service": "api-gateway",      // 被映射为 text + keyword
  "env": "production",           // 被映射为 text + keyword
  "request_id": "abc123",        // 被映射为 text + keyword
  "user_agent": "Mozilla/5.0..." // 被映射为 text + keyword
}
// 结果:4 个字符串字段 × 2 种索引 = 8 份索引数据
// 大部分字段只需要 keyword 精确匹配,text 索引是浪费

解决方案

// 方案 1:严格模式,拒绝未知字段
PUT /logs-strict
{
  "mappings": {
    "dynamic": "strict"  // 写入未定义字段会报错
  }
}

// 方案 2:动态模板,精确控制类型推断
PUT /logs-template
{
  "mappings": {
    "dynamic": "true",
    "dynamic_templates": [
      {
        "strings_as_keyword": {
          "match_mapping_type": "string",
          "mapping": { "type": "keyword", "ignore_above": 256 }
        }
      }
    ]
  }
}

3.2 分析器配置

分析器(Analyzer)是全文搜索的核心组件,负责将文本拆分为词元(Token)并归一化。一个分析器由三部分组成:

Character Filter(字符过滤器)
  │  处理原始文本,如去除 HTML 标签
  ▼
Tokenizer(分词器)
  │  将文本拆分为词元
  ▼
Token Filter(词元过滤器)
  │  对词元进行归一化处理
  │  如小写化、词干提取、停用词过滤
  ▼
最终词元列表

3.2.1 内置分析器

分析器 行为 示例输入 → 输出
standard 基于 Unicode 文本分割,小写化 “Hello World” → [hello, world]
simple 按非字母分割,小写化 “Hello-World 123” → [hello, world]
whitespace 按空白分割 “Hello World” → [Hello, World]
keyword 不分词,整体作为一个词元 “Hello World” → [Hello World]
stop standard + 停用词过滤 “The quick fox” → [quick, fox]
pattern 按正则表达式分割 “foo,bar;baz” → [foo, bar, baz]
language 特定语言分析器(english 等) “running” → [run](词干提取)

3.2.2 中文分词:IK 分词器

中文全文搜索需要借助 IK 分词器或 jieba 分词器。IK 提供两种分词模式:

  • ik_max_word(最细粒度分词):尽可能多地切分词元,适合索引时使用,提高召回率
  • ik_smart(智能分词):减少歧义切分,适合查询时使用,提高精确率
输入文本: "ElasticSearch 搜索引擎实战"

ik_max_word: [elasticsearch, 搜索, 搜索引擎, 引擎, 实战]
ik_smart:    [elasticsearch, 搜索引擎, 实战]

自定义分析器:索引时细粒度,查询时智能

PUT /articles
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ik_index": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["lowercase", "asciifolding"]
        },
        "ik_search": {
          "type": "custom",
          "tokenizer": "ik_smart",
          "filter": ["lowercase", "asciifolding"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_index",
        "search_analyzer": "ik_search"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_index",
        "search_analyzer": "ik_search"
      }
    }
  }
}

索引时使用 ik_max_word 尽可能多地切分词元,提高召回率;查询时使用 ik_smart 减少歧义,提高精确率。这种"索引细、查询粗"的策略是中文搜索的标准实践。

3.2.3 验证分析器输出

使用 _analyze API 可以查看分析器对特定文本的处理结果,是调试分词问题的重要工具:

// 查看分析器输出
POST /articles/_analyze
{
  "analyzer": "ik_max_word",
  "text": "ElasticSearch 搜索引擎实战"
}

// 响应
{
  "tokens": [
    { "token": "elasticsearch", "start_offset": 0, "end_offset": 13, "type": "ENGLISH", "position": 0 },
    { "token": "搜索", "start_offset": 14, "end_offset": 16, "type": "CN_WORD", "position": 1 },
    { "token": "搜索引擎", "start_offset": 14, "end_offset": 18, "type": "CN_WORD", "position": 2 },
    { "token": "引擎", "start_offset": 16, "end_offset": 18, "type": "CN_WORD", "position": 3 },
    { "token": "实战", "start_offset": 18, "end_offset": 20, "type": "CN_WORD", "position": 4 }
  ]
}

3.2.4 自定义同义词过滤器

搜索场景中经常需要处理同义词,如"手机"和"智能手机"、“耳机"和"耳麦”:

PUT /products-synonym
{
  "settings": {
    "analysis": {
      "filter": {
        "my_synonym": {
          "type": "synonym",
          "synonyms": [
            "手机,智能手机,移动电话",
            "耳机,耳麦,耳塞",
            "电脑,计算机,PC"
          ]
        }
      },
      "analyzer": {
        "ik_with_synonym": {
          "type": "custom",
          "tokenizer": "ik_max_word",
          "filter": ["lowercase", "my_synonym"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_with_synonym"
      }
    }
  }
}

配置后,搜索"手机"时也会匹配到包含"智能手机"或"移动电话"的文档。

3.3 动态模板

当日志类数据字段不固定时,动态模板可以按规则自动推断字段类型,避免映射爆炸:

PUT /logs-app
{
  "mappings": {
    "dynamic": "true",
    "dynamic_templates": [
      {
        "strings_as_keyword": {
          "match_mapping_type": "string",
          "mapping": { "type": "keyword", "ignore_above": 256 }
        }
      },
      {
        "longs_as_long": {
          "match_mapping_type": "long",
          "mapping": { "type": "long" }
        }
      },
      {
        "doubles_as_double": {
          "match_mapping_type": "double",
          "mapping": { "type": "double" }
        }
      },
      {
        "dates_as_date": {
          "match": ".*_time$",
          "mapping": { "type": "date" }
        }
      },
      {
        "booleans_as_boolean": {
          "match_mapping_type": "boolean",
          "mapping": { "type": "boolean" }
        }
      }
    ]
  }
}

这个模板将所有自动发现的字符串映射为 keyword(截断超过 256 字符的值),以 _time 结尾的字段映射为 date。对于日志场景,大部分字符串字段只需要精确匹配,不需要分词全文搜索,这样可以显著减少倒排索引体积。

动态模板的匹配规则

匹配方式 说明 示例
match_mapping_type 按 JSON 类型匹配 "string", "long", "double", "object", "boolean"
match 按字段名正则匹配 ".*_time$" 匹配所有以 _time 结尾的字段
unmatch 排除匹配的字段 排除特定模式
path_match 按完整路径匹配 "user.*.name" 匹配嵌套字段
match_pattern 匹配模式类型 "regex""simple"

3.4 nested 与 join 类型

3.4.1 普通对象的问题

普通 object 类型会将嵌套数组扁平化存储,导致跨对象字段的交叉匹配:

// 写入的文档
{
  "user": [
    { "first": "张", "last": "三" },
    { "first": "李", "last": "四" }
  ]
}

// ElasticSearch 内部存储(扁平化)
{
  "user.first":  ["张", "李"],
  "user.last":   ["三", "四"]
}

// 查询 first=张 AND last=四 会错误地匹配到这条文档
// 因为 "张" 在 user.first 数组中,"四" 在 user.last 数组中

3.4.2 使用 nested 类型

使用 nested 类型可以保持对象内部的独立性:

PUT /users
{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested",
        "properties": {
          "first": { "type": "keyword" },
          "last": { "type": "keyword" }
        }
      }
    }
  }
}

nested 类型将每个嵌套对象作为独立文档存储(hidden document),查询时通过 join 关联。这意味着 100 个嵌套对象会产生 101 个 Lucene 文档。

// nested 查询
GET /users/_search
{
  "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "term": { "user.first": "张" } },
            { "term": { "user.last": "三" } }
          ]
        }
      }
    }
  }
}

nested 的代价:在嵌套层级深或数组长的场景下,查询性能会显著下降。如果只需要存储不需要按嵌套对象查询,用普通 object 即可;如果需要父子文档的复杂关联操作,考虑 join 类型或应用层关联。

3.4.3 join 类型(父子文档)

join 类型定义了文档间的父子关系,父文档和子文档存储在同一个索引中:

PUT /company
{
  "mappings": {
    "properties": {
      "relation": {
        "type": "join",
        "relations": {
          "department": "employee"
        }
      }
    }
  }
}

// 写入父文档
PUT /company/_doc/1
{
  "name": "技术部",
  "relation": "department"
}

// 写入子文档(指定父 ID)
PUT /company/_doc/2?routing=1
{
  "name": "张三",
  "relation": { "name": "employee", "parent": "1" }
}

// 查询某部门的所有员工
GET /company/_search
{
  "query": {
    "has_parent": {
      "parent_type": "department",
      "query": { "term": { "name": "技术部" } }
    }
  }
}

join 类型的限制:父子文档必须路由到同一个分片(通过 routing),每次查询需要 join 操作,性能较差。只有在真正需要父子关系时才使用,否则推荐使用嵌套文档或应用层关联。

3.5 索引别名与零停机重建

索引别名是指向一个或多个索引的软链接,是生产环境不可或缺的工具:

// 创建别名
POST /_aliases
{
  "actions": [
    { "add": { "index": "products_v1", "alias": "products" } }
  ]
}

// 通过别名读写(对应用透明)
POST /products/_doc    // 实际写入 products_v1
GET /products/_search  // 实际查询 products_v1

// 零停机重建索引
POST /_aliases
{
  "actions": [
    { "add": { "index": "products_v2", "alias": "products" } },
    { "remove": { "index": "products_v1", "alias": "products" } }
  ]
}

别名的高级用法

// 写入别名:只指向一个索引
POST /_aliases
{
  "actions": [
    { "add": { "index": "products_v2", "alias": "products_write" } }
  ]
}

// 查询别名:可以指向多个索引
POST /_aliases
{
  "actions": [
    { "add": { "indices": ["products_v1", "products_v2"], "alias": "products_read" } }
  ]
}

// 过滤别名:只暴露部分数据
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products",
        "alias": "products_onsale",
        "filter": { "term": { "status": "on_sale" } }
      }
    }
  ]
}

第四章 索引优化实战

从分片策略到写入吞吐,系统性地优化索引层性能。

4.1 分片策略设计

分片数量是索引创建后无法修改(只能 reindex)的关键决策。分片过多导致每个分片体积小、元数据开销大、查询扇出多;分片过少导致单分片体积大、查询慢、故障恢复慢。官方指导原则是每个主分片控制在 10-50 GB,文档数不超过 2 亿。

分片数量对查询性能的影响

分片大小        查询延迟(100万文档,match查询)
──────────────────────────────────────────────
  5 GB              8 ms     ← 过多分片,扇出开销大
 10 GB             12 ms
 20 GB             18 ms
 30 GB             25 ms     ← 最优区间
 50 GB             42 ms
 80 GB             85 ms
100 GB            130 ms
150 GB            280 ms     ← 过大,单分片查询慢

分片数量的计算公式:主分片数 = 预估总数据量 / 目标单分片大小。例如预计 300 GB 数据,目标单分片 30 GB,则主分片数 = 10。

数据规模 推荐主分片数 副本数 说明
< 10 GB 1 1 小数据无需分片,避免分片开销
10-100 GB 2-3 1 按 30-50 GB 单分片规划
100 GB-1 TB 5-20 1-2 考虑 ILM 滚动策略配合
> 1 TB ILM 管理 1-2 使用数据流 + 滚动索引

过度分片的代价:每个分片都是一个独立的 Lucene 索引,消耗堆内存(约 50 KB 元数据/分片)、文件句柄和 CPU 资源。一个集群有数万分片时,主节点的集群状态发布会成为瓶颈。规则:每 GB 堆内存不超过 20 个分片。

4.1.1 分片分配过滤

可以通过分片分配过滤将特定索引分配到特定节点,实现冷热分离:

// 节点配置(elasticsearch.yml)
// hot 节点:node.attr.data_type: hot
// warm 节点:node.attr.data_type: warm
// cold 节点:node.attr.data_type: cold

// 索引级分片分配过滤
PUT /logs-2026.07/_settings
{
  "index.routing.allocation.require.data_type": "hot"
}

// 通过 ILM 自动迁移
// hot 阶段 → warm 阶段时自动修改 allocation 规则

4.1.2 分片均衡

ElasticSearch 内置分片均衡器,基于两个阈值:

  • cluster.routing.allocation.balance.shard(默认 0.45):节点间分片数均衡
  • cluster.routing.allocation.balance.index(默认 0.55):同一索引的分片在节点间均衡

当节点 disk usage 超过 cluster.routing.allocation.disk.watermark.high(默认 90%)时,新分片不会分配到该节点;超过 flood_stage(默认 95%)时,索引被标记为只读。

// 调整磁盘水位线
PUT _cluster/settings
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.low": "85%",
    "cluster.routing.allocation.disk.watermark.high": "90%",
    "cluster.routing.allocation.disk.watermark.flood_stage": "95%"
  }
}

4.2 写入优化

4.2.1 批量写入与 refresh_interval

ElasticSearch 默认每秒 refresh 一次,将内存缓冲区的新文档生成新的 Segment 使其可搜索。每次 refresh 都会创建新 Segment,频繁 refresh 会导致 Segment 数量膨胀、合并压力大、写入吞吐下降。批量写入场景下应临时调大 refresh_interval 或关闭 refresh。

// 批量导入前:关闭 refresh
PUT /products/_settings
{
  "refresh_interval": "-1",
  "number_of_replicas": 0
}

// 执行批量导入... 使用 _bulk API,每批 5-15 MB

// 导入完成后:恢复设置
PUT /products/_settings
{
  "refresh_interval": "1s",
  "number_of_replicas": 1
}

// 手动触发 force merge,减少 Segment 数量
POST /products/_forcemerge?max_num_segments=1

批量导入时临时将副本数设为 0 可以避免写入时同步副本的开销,导入完成后再恢复。force merge 将多个小 Segment 合并为一个大 Segment,减少查询时需要扫描的 Segment 数量。

4.2.2 Bulk API 批量大小

Bulk 请求的理想大小在 5-15 MB 之间。过小导致网络和请求开销占比高,过大导致内存压力和超时。

# Python 示例:自适应批量大小
from elasticsearch import Elasticsearch, helpers

es = Elasticsearch(["http://localhost:9200"])

def generate_docs():
    for i in range(1000000):
        yield {
            "_index": "products",
            "_id": str(i),
            "_source": {
                "name": f"商品 {i}",
                "price": round(random.uniform(10, 999), 2),
                "category": random.choice(["数码", "图书", "家居"])
            }
        }

# chunk_size 控制每批文档数,max_chunk_bytes 控制每批字节上限
helpers.bulk(es, generate_docs(), chunk_size=1000, max_chunk_bytes=15*1024*1024)

Java 示例:使用 BulkProcessor

BulkProcessor bulkProcessor = BulkProcessor.builder(
        client,
        new BulkProcessor.Listener() {
            @Override
            public void beforeBulk(long executionId, BulkRequest request) {
                // 批量请求前的回调
            }

            @Override
            public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {
                // 批量请求成功后的回调
                if (response.hasFailures()) {
                    logger.warn("Bulk has failures: {}", response.buildFailureMessage());
                }
            }

            @Override
            public void afterBulk(long executionId, BulkRequest request, Throwable failure) {
                // 批量请求失败后的回调
                logger.error("Bulk failed", failure);
            }
        })
    .setBulkActions(1000)           // 每 1000 条触发一次
    .setBulkSize(new ByteSizeValue(15, ByteSizeUnit.MB))  // 每 15MB 触发一次
    .setFlushInterval(TimeValue.timeValueSeconds(5))       // 每 5 秒触发一次
    .setConcurrentRequests(4)       // 允许 4 个并发请求
    .setBackoffPolicy(BackoffPolicy.exponentialBackoff(
        TimeValue.timeValueMillis(100), 3))  // 指数退避重试
    .build();

// 使用
for (Product product : products) {
    bulkProcessor.add(new IndexRequest("products")
        .id(product.getId())
        .source(JSON.toJSONString(product), XContentType.JSON));
}

// 关闭时刷新剩余请求
bulkProcessor.awaitClose(10, TimeUnit.MINUTES);

4.2.3 写入一致性保障

参数 说明 推荐值
wait_for_active_shards 写入前等待多少活跃分片 1(默认,仅主分片即可)
index.translog.durability translog 持久化策略 request(每次请求 fsync,最安全)或 async(异步,更高吞吐)
index.translog.sync_interval async 模式下的同步间隔 5s(默认)
index.translog.flush_threshold_size translog 大小触发 flush 512mb(默认)
// 追求写入吞吐:async 模式
PUT /products/_settings
{
  "index.translog.durability": "async",
  "index.translog.sync_interval": "5s",
  "index.refresh_interval": "30s"
}

// 追求数据安全:request 模式(默认)
PUT /products/_settings
{
  "index.translog.durability": "request"
}

4.3 Segment 合并策略

Lucene 的 TieredMergePolicy 采用分层合并策略,将大小相近的 Segment 合并在一起。

参数 默认值 调优建议
index.merge.policy.segments_per_tier 10 SSD 上可降到 5-7,减少 Segment 数量但增加合并 I/O
index.merge.policy.max_merge_at_once 10 单次合并最大 Segment 数,I/O 充足可调大
index.merge.policy.max_merged_segment 5 GB 合并后最大 Segment 大小,大索引可调到 10-50 GB
index.merge.scheduler.max_thread_count max(1, min(4, cpu/2)) SSD 上可调大,HDD 建议 1

force merge 的正确时机:对于只读索引(如历史日志、归档数据),执行 _forcemerge?max_num_segments=1 将所有 Segment 合并为一个,可以显著减少查询时的 Segment 扫描数量和文件句柄占用。但运行中的索引不要 force merge 到 1 个 Segment,因为合并后无法再合并,新写入会重新产生小 Segment。

4.4 索引模板与数据流

对于时序数据(日志、指标),使用数据流(Data Stream)配合 ILM 策略是最佳实践。数据流自动管理后备索引的创建、滚动和删除。

4.4.1 完整的 ILM 策略

// 1. 创建 ILM 策略
PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_age": "1d",
            "max_primary_shard_size": "50gb"
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },
          "set_priority": { "priority": 50 },
          "allocate": {
            "include": { "data_type": "warm" }
          }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "set_priority": { "priority": 25 },
          "allocate": {
            "include": { "data_type": "cold" }
          }
        }
      },
      "frozen": {
        "min_age": "60d",
        "actions": {
          "searchable_snapshot": { "snapshot_repository": "my-repo" }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

这个 ILM 策略实现了完整的冷热分层:

  • 热阶段(0-7 天):在 SSD 上高频写入查询,按天或 50GB 滚动创建新索引
  • 暖阶段(7-30 天):缩减分片数、force merge 后迁移到暖节点
  • 冷阶段(30-60 天):迁移到冷节点,减少副本
  • 冻结阶段(60-90 天):转为可搜索快照,极大降低存储成本
  • 删除阶段(90 天后):自动删除

4.4.2 创建索引模板和数据流

// 2. 创建组件模板(settings)
PUT _component_template/logs-settings
{
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs-policy",
      "index.refresh_interval": "1s",
      "index.routing.allocation.include.data_type": "hot"
    }
  }
}

// 3. 创建组件模板(mappings)
PUT _component_template/logs-mappings
{
  "template": {
    "mappings": {
      "dynamic": "true",
      "dynamic_templates": [
        {
          "strings_as_keyword": {
            "match_mapping_type": "string",
            "mapping": { "type": "keyword", "ignore_above": 512 }
          }
        }
      ],
      "properties": {
        "@timestamp": { "type": "date" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" },
        "message": { "type": "text" }
      }
    }
  }
}

// 4. 创建索引模板
PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "data_stream": {},
  "composed_of": ["logs-settings", "logs-mappings"],
  "priority": 200
}

// 5. 写入数据自动创建数据流
POST logs-app/_doc
{ "@timestamp": "2026-07-04T10:00:00Z", "level": "info", "message": "服务启动" }

4.4.3 数据流管理命令

# 查看数据流
GET _data_stream/logs-app

# 手动滚动数据流
POST logs-app/_rollover

# 修改数据流的 ILM 策略
PUT _data_stream/logs-app
{
  "index_patterns": ["logs-app-*"],
  "data_stream": {},
  "ilm_policy": "new-logs-policy"
}

# 删除数据流(同时删除后备索引)
DELETE _data_stream/logs-app

4.5 reindex 重建索引

当需要修改映射(如改变字段类型)或合并索引时,使用 reindex:

// 1. 创建新索引
PUT /products_v2
{
  "mappings": {
    "properties": {
      "name": { "type": "text", "analyzer": "ik_max_word" },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "category": { "type": "keyword" }
    }
  }
}

// 2. reindex(支持管道转换)
POST /_reindex?wait_for_completion=false
{
  "source": { "index": "products" },
  "dest": { "index": "products_v2" },
  "script": {
    "source": """
      // 可以在 reindex 时做字段转换
      if (ctx._source.price != null) {
        ctx._source.price = ctx._source.price * 1.0;
      }
      // 删除不需要的字段
      ctx._source.remove('internal_id');
    """
  }
}

// 3. 切换别名
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products", "alias": "products" } },
    { "add": { "index": "products_v2", "alias": "products" } }
  ]
}

// 4. 删除旧索引
DELETE /products

reindex 性能优化

// 提高批量大小
POST /_reindex?slices=auto&refresh=false
{
  "source": { "index": "products", "size": 5000 },
  "dest": { "index": "products_v2", "op_type": "create" }
}

// 按时间范围分批 reindex(避免一次性处理过多)
POST /_reindex
{
  "source": {
    "index": "products",
    "query": {
      "range": { "create_time": { "gte": "2026-01-01", "lt": "2026-04-01" } }
    }
  },
  "dest": { "index": "products_v2" }
}

slices=auto 让 ElasticSearch 自动并行化 reindex,利用多核加速。op_type: create 确保不会覆盖已有文档。

第五章 查询 DSL 编写指南

从基础查询到复杂聚合,掌握查询 DSL 的完整体系与编写技巧。

5.1 查询上下文与过滤上下文

ElasticSearch 查询分为两种上下文,理解它们的区别是编写高效查询的前提。

维度 Query Context(查询上下文) Filter Context(过滤上下文)
相关性打分 计算 _score,结果按相关性排序 不计算 _score,只判断是否匹配
缓存 不缓存(得分依赖查询词) 结果被节点级缓存,相同条件命中快
适用场景 全文搜索、相关性排序 精确过滤、范围筛选、状态判断
典型查询 match、multi_match、bool/must term、terms、range、bool/filter
性能 较慢(需要计算 BM25 得分) 快(位图运算 + 缓存)

核心原则:不需要相关性得分的条件一律放在 filter 子句中。这是查询优化最重要的单一原则,可以同时获得"不打分"和"被缓存"两重加速。

// 好的做法:filter 中的条件稳定且可缓存
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "耳机" } }
      ],
      "filter": [
        { "term": { "category": "数码" } },
        { "range": { "price": { "gte": 100, "lte": 2000 } } }
      ]
    }
  }
}

// 避免的做法:将不需要打分的条件放在 must 中
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "耳机" } },
        { "term": { "category": "数码" } },      // 浪费:不需要打分
        { "range": { "price": { "gte": 100 } } }  // 浪费:不被缓存
      ]
    }
  }
}

5.2 全文搜索查询

5.2.1 match 查询

match 是最常用的全文搜索查询。对于中文,查询文本会经过与索引相同的分析器处理:

// 基本匹配:查询文本被分词后做 OR 匹配
GET /products/_search
{
  "query": {
    "match": { "name": "无线蓝牙耳机" }
  }
}

// 要求所有词都匹配(AND)
{
  "query": {
    "match": {
      "name": {
        "query": "无线 蓝牙 耳机",
        "operator": "and"
      }
    }
  }
}

// minimum_should_match 控制最少匹配词数
{
  "query": {
    "match": {
      "description": {
        "query": "高清 无线 蓝牙 降噪 耳机",
        "minimum_should_match": "75%"
      }
    }
  }
}

// 使用 fuzziness 支持拼写纠错
{
  "query": {
    "match": {
      "name": {
        "query": "蓝芽耳机",
        "fuzziness": "AUTO"
      }
    }
  }
}

match 查询的参数详解

参数 说明 示例值
query 查询文本 "无线 耳机"
operator 词元间的逻辑关系 "or"(默认)、"and"
minimum_should_match 最少匹配的词元数 "1""75%""2<75%"
analyzer 覆盖字段的分析器 "ik_smart"
fuzziness 模糊匹配程度 "AUTO""0""1""2"
prefix_length 模糊匹配时不变形的前缀长度 1
max_expansions 模糊匹配的最大扩展数 50
zero_terms_query 分析后无词元时的行为 "none"(默认)、"all"
cutoff_frequency 高频词处理阈值(7.x 后已弃用) -

5.2.2 match_phrase 短语查询

当需要词序精确匹配时使用 match_phrase

// "蓝牙耳机" 必须连续出现
{
  "query": {
    "match_phrase": { "name": "蓝牙耳机" }
  }
}

// 允许中间间隔 1 个词
{
  "query": {
    "match_phrase": {
      "name": { "query": "蓝牙 降噪", "slop": 1 }
    }
  }
}

slop 参数允许匹配的词元之间有间隔。slop=1 表示"蓝牙"和"降噪"之间可以有一个其他词。

5.2.3 multi_match 多字段查询

跨多个字段搜索时使用 multi_match,支持不同的打分策略:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "蓝牙耳机",
      "fields": ["name^3", "description^1"],
      "type": "best_fields"
    }
  }
}

name^3 表示 name 字段得分乘以 3 的权重提升。type 参数控制打分策略:

type 行为 适用场景
best_fields 取匹配度最高字段的得分 同一概念分散在不同字段,取最佳匹配
most_fields 累加所有匹配字段的得分 多字段匹配提升相关性
cross_fields 将多字段视为一个字段处理 人名等拆分存储的场景
phrase 在每个字段上做短语匹配,取最高分 需要词序精确的场景
phrase_prefix 短语前缀匹配 搜索补全场景
bool_prefix 匹配前缀,最后词做前缀匹配 搜索建议

5.2.4 布尔查询组合

bool 查询是 DSL 中最强大的组合工具:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "耳机" } }
      ],
      "should": [
        { "match": { "description": "降噪" } },
        { "term": { "tags": "蓝牙" } }
      ],
      "filter": [
        { "range": { "price": { "gte": 100, "lte": 2000 } } },
        { "terms": { "category": ["数码", "音频"] } }
      ],
      "must_not": [
        { "term": { "status": "discontinued" } }
      ],
      "minimum_should_match": 1
    }
  }
}
子句 作用 是否打分 是否影响匹配
must 必须匹配
filter 必须匹配 否(缓存)
should 至少匹配 minimum_should_match 个 有 must/filter 时不影响,否则必须匹配至少 1 个
must_not 必须不匹配

5.2.5 constant_score 查询

当你希望 filter 条件的结果有一个固定得分(而不是 0)时使用:

{
  "query": {
    "bool": {
      "should": [
        {
          "constant_score": {
            "filter": { "term": { "tags": "热销" } },
            "boost": 1.5
          }
        },
        {
          "match": { "name": "耳机" }
        }
      ]
    }
  }
}

5.3 精确值查询

5.3.1 term 与 terms 查询

// term:精确匹配单个值(不分词)
{ "query": { "term": { "category": "数码" } } }

// terms:匹配多个值(类似 SQL IN)
{ "query": { "terms": { "category": ["数码", "音频", "外设"] } } }

// terms lookup:从另一个文档的字段获取匹配值
{
  "query": {
    "terms": {
      "tags": {
        "index": "user-preferences",
        "id": "user_123",
        "path": "interested_tags"
      }
    }
  }
}

注意term 查询对 text 类型字段可能不匹配预期结果,因为 text 字段经过分词后存储的是词元。查询 text 字段时应该使用 match 查询,或者查询 .keyword 子字段。

5.3.2 range 范围查询

// 数值范围
{ "query": { "range": { "price": { "gte": 100, "lte": 2000 } } } }

// 日期范围(支持数学表达式)
{
  "query": {
    "range": {
      "create_time": {
        "gte": "now-7d/d",    // 7 天前的 0 点
        "lt": "now/d"          // 今天的 0 点
      }
    }
  }
}

// 日期数学表达式示例
"now"           // 当前时间
"now-1h"        // 1 小时前
"now/d"         // 今天的 0 点(舍入到天)
"now+1d/d"      // 明天的 0 点
"2026-07-04||/d"  // 2026-07-04 的 0 点

日期查询优化:对 date 字段做范围查询时,使用 now/1d 这样的舍入表达式可以利用查询缓存,而精确到秒的时间戳每次都不同,无法被缓存。

// 好:舍入到天,可缓存
{ "range": { "create_time": { "gte": "now/d", "lt": "now+1d/d" } } }

// 差:精确到毫秒,每次查询不同,不缓存
{ "range": { "create_time": { "gte": "2026-07-04T10:30:00.123Z" } } }

5.4 聚合查询

聚合是 ElasticSearch 作为分析引擎的核心能力。聚合分为三类:桶聚合(Bucket)、指标聚合(Metric)和管道聚合(Pipeline)。

5.4.1 桶聚合与指标聚合组合

GET /products/_search
{
  "size": 0,
  "query": {
    "range": { "create_time": { "gte": "2026-06-01" } }
  },
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 10,
        "order": { "avg_price": "desc" }
      },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } },
        "price_stats": { "stats": { "field": "price" } },
        "price_histogram": {
          "histogram": {
            "field": "price",
            "interval": 200
          }
        }
      }
    }
  }
}

这个查询按分类分桶,每个桶内计算平均价格、价格统计信息和价格直方图。size: 0 表示不返回文档,只返回聚合结果,减少网络传输。

5.4.2 常用指标聚合

聚合类型 说明 示例
avg 平均值 { "avg": { "field": "price" } }
sum 求和 { "sum": { "field": "price" } }
max / min 最大值/最小值 { "max": { "field": "price" } }
stats 统计信息(count, min, max, avg, sum) { "stats": { "field": "price" } }
cardinality 去重计数(HyperLogLog) { "cardinality": { "field": "user_id" } }
percentiles 百分位数 { "percentiles": { "field": "latency", "percents": [50, 95, 99] } }
percentile_ranks 值的百分位排名 { "percentile_ranks": { "field": "latency", "values": [100, 500] } }
top_hits 桶内 top N 文档 { "top_hits": { "size": 3, "sort": [{ "price": "desc" }] } }
value_count 值计数 { "value_count": { "field": "price" } }

5.4.3 日期直方图聚合

{
  "aggs": {
    "daily_sales": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "1d",
        "format": "yyyy-MM-dd",
        "time_zone": "+08:00",
        "min_doc_count": 0
      },
      "aggs": {
        "revenue": { "sum": { "field": "price" } }
      }
    }
  }
}
时间间隔参数 说明
calendar_interval: "1d" 按天(日历对齐)
calendar_interval: "1M" 按月(日历对齐,每月天数不同)
fixed_interval: "30d" 固定 30 天
fixed_interval: "1h" 固定 1 小时
fixed_interval: "30m" 固定 30 分钟

calendar_interval 按日历对齐(如按月时每月天数不同),fixed_interval 是固定的时间间隔。

5.4.4 管道聚合

管道聚合在其他聚合结果上做二次计算:

{
  "aggs": {
    "monthly": {
      "date_histogram": {
        "field": "create_time",
        "calendar_interval": "1M"
      },
      "aggs": {
        "monthly_revenue": { "sum": { "field": "price" } },
        "cumulative_revenue": {
          "cumulative_sum": { "buckets_path": "monthly_revenue" }
        },
        "mom_growth": {
          "derivative": { "buckets_path": "monthly_revenue" }
        }
      }
    }
  }
}

常用管道聚合:

  • cumulative_sum:累计求和
  • derivative:导数(环比变化)
  • moving_avg:移动平均
  • bucket_sort:桶排序
  • avg_bucket:桶平均值

5.4.5 高基数聚合的处理

高基数聚合的陷阱terms 聚合在高基数字段(如 user_id、IP 地址)上会生成大量桶,消耗大量内存并可能导致 OOM。解决方案:使用 composite 聚合分页获取所有桶,或使用 cardinality 聚合计算去重数(基于 HyperLogLog 算法,内存占用固定)。

// composite 聚合:分页获取高基数聚合结果
{
  "size": 0,
  "aggs": {
    "by_user": {
      "composite": {
        "size": 1000,
        "sources": [
          { "user": { "terms": { "field": "user_id" } } },
          { "date": { "date_histogram": { "field": "@timestamp", "calendar_interval": "1d" } } }
        ]
      },
      "aggs": {
        "total_spent": { "sum": { "field": "amount" } }
      }
    }
  }
}

// 响应中包含 after_key,用于获取下一页
// {
//   "aggregations": {
//     "by_user": {
//       "after_key": { "user": "user_999", "date": "2026-07-01" },
//       "buckets": [...]
//     }
//   }
// }

// 下一页查询
{
  "aggs": {
    "by_user": {
      "composite": {
        "size": 1000,
        "sources": [...],
        "after": { "user": "user_999", "date": "2026-07-01" }
      }
    }
  }
}

5.5 深度分页

ElasticSearch 的 from + size 分页在深度分页时性能急剧下降——协调节点需要从每个分片取 from + size 条文档,合并排序后丢弃前 from 条。例如 from=10000, size=10,3 个分片需要各取 10010 条,协调节点处理 30030 条再丢弃 10000 条。

分页方式 适用场景 限制
from + size 浅分页(< 10000) index.max_result_window 默认 10000
search_after 深度分页、实时翻页 需要排序字段唯一,不支持随机跳页
scroll 批量导出全量数据 非实时,占用资源,9.x 后推荐用 PIT + search_after
PIT + search_after 推荐的全量遍历方式 需要先创建 PIT 保持数据快照

5.5.1 PIT + search_after 深度分页

// 1. 创建 PIT
POST /products/_pit?keep_alive=5m
// 返回: { "id": "pit_id_string" }

// 2. 首次查询
GET /_search
{
  "size": 100,
  "pit": {
    "id": "上一步返回的 pit_id",
    "keep_alive": "5m"
  },
  "query": { "match_all": {} },
  "sort": [
    { "create_time": "asc" },
    { "_shard_doc": "asc" }
  ]
}

// 3. 后续查询:使用上一次结果的 sort 值
GET /_search
{
  "size": 100,
  "pit": {
    "id": "pit_id",
    "keep_alive": "5m"
  },
  "query": { "match_all": {} },
  "sort": [
    { "create_time": "asc" },
    { "_shard_doc": "asc" }
  ],
  "search_after": ["2026-07-01T00:00:00Z", 12345]
}

// 4. 用完删除 PIT
DELETE /_pit
{ "id": "pit_id" }

排序字段必须包含 _shard_doc(即 _doc)作为兜底排序,确保排序值全局唯一。PIT 提供了一致的数据快照,避免遍历过程中新写入文档导致重复或遗漏。

5.6 查询性能分析

使用 _search?profile=true 可以查看查询在每个分片上的执行细节,定位性能瓶颈:

GET /products/_search?profile=true
{
  "query": {
    "bool": {
      "must": [{ "match": { "name": "耳机" } }],
      "filter": [{ "range": { "price": { "lte": 500 } } }]
    }
  },
  "size": 5
}

profile 输出会显示每个 query 子句的 time_in_nanos(耗时)、breakdown(各阶段耗时明细)和 children(子查询耗时)。根据这些信息可以判断是 match 查询慢还是 filter 慢,是否命中了缓存。

profile 输出的关键字段

{
  "profile": {
    "shards": [
      {
        "id": "[index_name][0][node_id]",
        "searches": [
          {
            "query": {
              "type": "BooleanQuery",
              "description": "+name:耳机 #price:[-∞ TO 500]",
              "time_in_nanos": 145000,
              "breakdown": {
                "create_weight": 5000,      // 创建查询权重
                "build_scorer": 80000,       // 构建 Scorer
                "next_doc": 30000,           // 遍历文档
                "score": 15000,              // 计算得分
                "advance": 5000,             // 跳转文档
                "match_count": 0,
                "compute_max_score": 0,
                "shallow_advance": 10000
              },
              "children": [
                {
                  "type": "TermQuery",
                  "description": "name:耳机",
                  "time_in_nanos": 80000,
                  "breakdown": { ... }
                },
                {
                  "type": "PointRangeQuery",
                  "description": "price:[-∞ TO 500]",
                  "time_in_nanos": 40000,
                  "breakdown": { ... }
                }
              ]
            }
          }
        ]
      }
    ]
  }
}

分析方法

  1. time_in_nanos 总耗时,找出最慢的子查询
  2. breakdown 中哪个阶段耗时最多(build_scorer 高可能意味着扫描了大量文档)
  3. 对比 matchfilter 的耗时分布
  4. 检查是否使用了 rewrite(如 MultiTermQuery 被重写为 TermQuery 的合集)

5.7 suggester 搜索建议

ElasticSearch 提供三种建议器:term suggester(词项建议)、phrase suggester(短语建议)、completion suggester(自动补全)。

5.7.1 completion suggester(自动补全)

// 索引映射
PUT /products_suggest
{
  "mappings": {
    "properties": {
      "name": {
        "type": "completion",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "preserve_separators": true,
        "max_input_length": 50
      }
    }
  }
}

// 写入建议数据
POST /products_suggest/_doc
{
  "name": {
    "input": ["无线蓝牙耳机", "蓝牙耳机", "无线耳机"],
    "weight": 10
  }
}

// 查询建议
GET /products_suggest/_search
{
  "suggest": {
    "product-suggest": {
      "prefix": "蓝牙",
      "completion": {
        "field": "name",
        "size": 5,
        "skip_duplicates": true
      }
    }
  }
}

completion suggester 使用 FST 结构存储,性能极高,适合搜索框自动补全场景。

第六章 ES|QL 查询语言

ElasticSearch 9.x 的一等查询语言,用管道式语法简化复杂分析。

6.1 为什么需要 ES|QL

传统的 Query DSL 是 JSON 格式,编写和阅读复杂查询时需要嵌套多层结构。ES|QL(Elasticsearch Query Language)是 ElasticSearch 8.11 引入、在 9.4 中大幅增强的管道式查询语言,语法类似 SQL 但增加了管道操作符,更适合数据探索和转换。

ES|QL 的核心优势在于管道式处理:数据从 FROM 源流出,经过一系列 | 管道命令逐步变换,每一步的输出是下一步的输入。这种线性结构比嵌套 JSON 更直观,尤其适合多步骤的数据转换和分析。

ES|QL vs Query DSL 对比

维度 Query DSL (JSON) ES|QL
语法 嵌套 JSON 管道式(类似 SQL + Unix pipe)
可读性 复杂查询难读 线性流程,逐步转换
执行效率 通用引擎 专用计算引擎,某些场景更快
功能 全功能 持续增强中,9.4 已覆盖大部分场景
缓存 节点级缓存 独立的计算会话
适用 API 集成 数据探索、仪表盘、告警

6.2 基础语法

ES|QL 通过 POST /_query 端点执行,支持 text、json、csv 等多种输出格式:

// 通过 REST API 执行 ES|QL
POST /_query?format=txt
{
  "query": """
    FROM products
    | WHERE price > 100 AND price < 1000
    | KEEP name, price, category
    | SORT price DESC
    | LIMIT 20
  """
}

ES|QL 的关键字不区分大小写。常用管道命令:

命令 作用 示例
FROM 指定数据源(索引、数据流、视图) FROM products
WHERE 过滤行 WHERE price > 100
KEEP 保留指定列 KEEP name, price
DROP 移除指定列 DROP internal_id
RENAME 重命名列 RENAME price AS unit_price
EVAL 计算新列 EVAL discount = price * 0.8
STATS 聚合统计(类似 SQL GROUP BY) STATS avg = AVG(price) BY category
SORT 排序 SORT price DESC
LIMIT 限制行数 LIMIT 100
GROK 解析非结构化文本 GROK message "%{GREEDYDATA:msg}"
DISSECT 解析结构化文本 DISSECT message "%{service} %{level} %{}"
MV_EXPAND 展开多值字段 MV_EXPAND tags
ENRICH 关联查找 ENRICH policy_name ON field

6.3 全文搜索与匹配

ES|QL 支持全文搜索,使用 match 函数或 : 运算符:

// 全文匹配
FROM products
| WHERE name : "蓝牙耳机"
| KEEP name, price, category
| SORT price ASC
| LIMIT 10

// 使用 match 函数(支持更多参数)
FROM products
| WHERE match(name, "降噪 耳机")
| EVAL score = score(name)
| SORT score DESC
| LIMIT 10

// 多字段匹配
FROM products
| WHERE name : "耳机" OR description : "降噪"
| KEEP name, price
| LIMIT 20

6.4 聚合分析

ES|QL 的 STATS 命令是聚合分析的核心,支持按字段分组并计算指标:

// 基础聚合
FROM products
| STATS
    count = COUNT(*),
    avg_price = AVG(price),
    min_price = MIN(price),
    max_price = MAX(price),
    total_value = SUM(price)
  BY category
| SORT avg_price DESC

// 多层聚合
FROM products
| WHERE create_time >= "2026-06-01"
| EVAL month = DATE_FORMAT("yyyy-MM", create_time)
| STATS
    revenue = SUM(price),
    items = COUNT(*)
  BY month, category
| SORT month ASC, revenue DESC

// 使用 sparkline 生成迷你图表(9.4 新增)
FROM products
| STATS price_trend = sparkline(price) BY category
| KEEP category, price_trend

// 使用 LIMIT BY 限制每组行数(9.4 Tech Preview)
FROM products
| SORT price DESC
| LIMIT 3 BY category

ES|QL 聚合函数一览

函数 说明
COUNT(*) 行计数
COUNT(field) 非空值计数
SUM(field) 求和
AVG(field) 平均值
MIN(field) / MAX(field) 最小值/最大值
MEDIAN(field) 中位数
PERCENTILE(field, p) 百分位数
STD_DEV(field) 标准差
VAR_SAMP(field) 样本方差
cardinality(field) 去重计数
sparkline(field) 迷你直方图
COUNT_DISTINCT(field) 精确去重计数

6.5 ES|QL Views(9.4 新特性)

9.4 引入了 Views 功能——将一段 ES|QL 查询封装为虚拟索引,外部可以像查询物理索引一样引用它:

-- 创建视图:标准化原始日志
CREATE VIEW clean_logs AS
  FROM raw-logs-*
  | RENAME @timestamp AS ts, host.name AS host
  | EVAL level = UPPER(log.level)
  | WHERE level != "DEBUG"
  | KEEP ts, host, level, message;

-- 像查询索引一样查询视图
FROM clean_logs
| STATS count = COUNT(*) BY host
| SORT count DESC
| LIMIT 10

-- 视图可以与物理索引混合查询
FROM clean_logs, alerts-*
| WHERE level == "ERROR"
| KEEP ts, host, message, alert_type
| SORT ts DESC
| LIMIT 50

Views 的安全限制:当底层索引启用了文档级安全(DLS)或字段级安全(FLS)时,视图无法被查询。这个限制在 ES 安全层强制执行。在混合版本跨集群搜索(CCS)场景中,也需要特别注意视图解析的兼容性。

6.6 时间序列查询

9.4 为 ES|QL 增加了 TS 源命令和 METRICS_INFOTS_INFO 等时间序列内省命令:

// 查看数据流中的指标列表
TS k8s-metrics
| METRICS_INFO
| WHERE metric_type == "counter"
| SORT metric_name

// 查看每个时间序列的维度
TS k8s-metrics
| TS_INFO
| SORT metric_name, dimensions

// 计算速率(counter rate)
TS k8s-metrics
| STATS avg_rate = AVG(RATE(network.bytes, 5m)) BY TBUCKET(10m), host
| SORT host, TBUCKET(10m)

// 9.4 新增:窗口大小不必是 bucket 的整数倍
TS k8s-metrics
| STATS AVG(RATE(requests, 15m)) BY TBUCKET(10m), host

6.7 数据解析与转换

ES|QL 强大的地方在于数据解析能力,特别适合日志分析场景:

// GROK 解析非结构化日志
FROM nginx-logs
| GROK message "%{IP:client_ip} - %{DATA:user} \[%{TIMESTAMP_ISO8601:ts}\] \"%{WORD:method} %{URIPATH:path} %{DATA:protocol}\" %{NUMBER:status} %{NUMBER:bytes}"
| KEEP client_ip, method, path, status, bytes
| WHERE status >= 400
| SORT ts DESC
| LIMIT 20

// DISSECT 解析结构化日志(比 GROK 快)
FROM app-logs
| DISSECT message "%{service} [%{level}] %{timestamp} %{msg}"
| EVAL level = UPPER(level)
| WHERE level == "ERROR"

// JSON 提取
FROM events
| EVAL user_agent = JSON_EXTRACT(headers, "user-agent")
| EVAL ip = JSON_EXTRACT(request, "remote_addr")

// 9.4 新增函数
FROM web-logs
| USER_AGENT user_agent           // 解析 UA 为结构化字段
| REGISTERED_DOMAIN host          // 提取注册域名
| URI_PART url                    // 提取 URI 各部分

6.8 ES|QL 函数大全

6.8.1 字符串函数

函数 说明 示例
CONCAT(a, b) 拼接字符串 CONCAT(first, " ", last)
UPPER(s) / LOWER(s) 大小写转换 UPPER(level)
LENGTH(s) 字符串长度 LENGTH(message)
SUBSTRING(s, start, len) 截取子串 SUBSTRING(name, 1, 10)
TRIM(s) 去除首尾空白 TRIM(input)
REPLACE(s, pattern, repl) 正则替换 REPLACE(phone, "\\D", "")
SPLIT(s, sep) 分割为多值 SPLIT(tags, ",")
LEFT(s, n) / RIGHT(s, n) 左/右截取 LEFT(name, 3)

6.8.2 日期函数

函数 说明 示例
NOW() 当前时间 NOW()
DATE_FORMAT(fmt, date) 格式化日期 DATE_FORMAT("yyyy-MM", @timestamp)
DATE_TRUNC(interval, date) 时间截断 DATE_TRUNC(1 hour, @timestamp)
DATE_EXTRACT(field, date) 提取日期部分 DATE_EXTRACT("hour", @timestamp)
TO_DATE(s) 字符串转日期 TO_DATE("2026-07-04")

6.8.3 数学函数

函数 说明
ABS(x) 绝对值
CEIL(x) / FLOOR(x) 向上/向下取整
ROUND(x, n) 四舍五入
SQRT(x) 平方根
POW(x, y) 幂运算
LOG(x) / LN(x) 对数
GREATEST(a, b) / LEAST(a, b) 最大/最小值

6.8.4 多值函数

函数 说明
MV_EXPAND(field) 将多值字段展开为多行
MV_UNION(a, b) 合并多值(9.4 新增)
MV_INTERSECTS(a, b) 交集(9.4 新增)
MV_DIFFERENCE(a, b) 差集(9.4 新增)
MV_COUNT(field) 多值字段元素数
MV_MAX(field) / MV_MIN(field) 多值字段最大/最小值

第七章 向量搜索与语义检索

利用 kNN 搜索和 semantic_text 构建 AI 驱动的语义检索系统。

7.1 向量搜索基础

传统全文搜索基于关键词匹配,无法理解语义——搜索"手机"时找不到文档中只写了"智能手机"的结果。向量搜索通过将文本编码为高维向量,在向量空间中计算相似度,实现语义级别的匹配。

ElasticSearch 支持两种向量搜索方式:

  • dense_vector 字段类型:存储预计算的向量,适合需要精细控制的场景
  • semantic_text 字段类型:自动完成嵌入计算和向量存储,最简单的方式

9.4 中,semantic_text 默认使用 BFLOAT16 量化与 bbq_disk 存储,TEXT_EMBEDDINGRERANK 均已正式 GA。

向量搜索的写入与查询路径

写入路径:
  原始文档 → 嵌入模型(text-embedding) → 向量(dense_vector) → 量化(bbq_disk) → HNSW 索引

查询路径:
  查询文本 → 嵌入模型(相同模型) → 查询向量 → kNN 搜索(HNSW + 量化) → Top-K 结果

7.2 使用 semantic_text

semantic_text 是最简单的向量搜索方式——只需声明字段类型,ElasticSearch 自动调用配置好的推理端点完成嵌入计算:

// 1. 创建推理端点(使用内置模型或外部 API)
PUT _inference/text_embedding/my-embedding
{
  "service": "openai",
  "service_settings": {
    "model_id": "text-embedding-3-small",
    "api_key": "sk-..."
  }
}

// 2. 创建索引,使用 semantic_text 字段
PUT /knowledge-base
{
  "mappings": {
    "properties": {
      "content": {
        "type": "semantic_text",
        "inference_id": "my-embedding"
      },
      "title": { "type": "text" },
      "category": { "type": "keyword" }
    }
  }
}

// 3. 写入文档(自动生成向量)
POST /knowledge-base/_doc
{
  "title": "ElasticSearch 性能调优",
  "content": "通过调整分片数量和映射配置可以显著提升查询性能...",
  "category": "技术"
}

// 4. 语义搜索
GET /knowledge-base/_search
{
  "query": {
    "semantic": {
      "field": "content",
      "query": "如何优化搜索速度"
    }
  }
}

写入时自动调用嵌入模型生成向量并存储,查询时同样自动将查询文本编码为向量执行 kNN 搜索。整个过程对开发者透明,无需手动管理向量。

使用 Elastic 内置模型(ELSER)

// 部署 ELSER 模型
PUT _ml/trained_models/.elser_model_2
{
  "description": "ELSER v2 model"
}

// 启动模型部署
POST _ml/trained_models/.elser_model_2/deployment/_start?wait_for=starting

// 创建推理端点
PUT _inference/text_embedding/elser-endpoint
{
  "service": "elser",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 4
  }
}

// 使用 ELSER 的 semantic_text
PUT /knowledge-base-elser
{
  "mappings": {
    "properties": {
      "content": {
        "type": "semantic_text",
        "inference_id": "elser-endpoint"
      }
    }
  }
}

ELSER(Elastic Learned Sparse EncodeR)是 Elastic 自研的稀疏向量模型,不需要外部 API,在 ElasticSearch 集群内运行。

7.3 手动 dense_vector 与 kNN

需要更精细控制时,可以手动管理 dense_vector 字段:

PUT /articles-vector
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "embedding": {
        "type": "dense_vector",
        "dims": 1536,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "bbq_disk",
          "m": 16,
          "ef_construction": 100
        }
      }
    }
  }
}

// kNN 查询
GET /articles-vector/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [0.1, 0.2, /* 1536 维向量 */],
    "k": 10,
    "num_candidates": 100
  },
  "_source": ["title"]
}

HNSW 参数详解

参数 默认值 说明
type bbq_disk(9.4 默认) 量化类型:hnsw(无量化)、bbq_diskbbq_memory
m 16 HNSW 图中每个节点的最大连接数。越大精度越高但内存越多
ef_construction 100 构建时的探索因子。越大索引质量越高但构建越慢
confidence_interval - 量化误差的置信区间控制

k 是最终返回的近邻数量,num_candidates 是每个分片上探索的候选数量(影响精度与性能的权衡)。9.4 中 bbq_disk 成为默认的量化类型。

相似度度量

度量 说明 适用场景
cosine 余弦相似度 文本嵌入(最常用)
dot_product 点积 已归一化的向量
l2_norm / max_inner_product 欧氏距离/最大内积 特定模型要求

7.4 混合搜索

纯向量搜索可能遗漏精确关键词匹配的结果,纯全文搜索可能遗漏语义相关但用词不同的结果。混合搜索将两者结合:

GET /knowledge-base/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "semantic": {
            "field": "content",
            "query": "如何优化搜索速度"
          }
        },
        {
          "match": {
            "title": "性能 调优 优化"
          }
        }
      ]
    }
  },
  "size": 10
}

混合查询会在 should 子句中同时计算向量相似度得分和 BM25 文本得分,两者加权融合后排序。混合搜索相比纯 BM25 搜索,"找不到明显结果"的用户投诉可降低 70-85%,代价是每查询增加约 5-15ms 延迟。

使用 RRF(Reciprocal Rank Fusion)融合

GET /knowledge-base/_search
{
  "size": 10,
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "semantic": {
                "field": "content",
                "query": "如何优化搜索速度"
              }
            }
          }
        },
        {
          "standard": {
            "query": {
              "match": {
                "title": "性能 调优 优化"
              }
            }
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  }
}

RRF 是一种无需调参的排名融合方法,通过倒数排名加权合并多个查询结果,在混合搜索中表现稳定。

7.5 RAG 重排序

9.4 中 RERANK 命令正式 GA。在 kNN 初检后使用重排序模型对候选结果做二次精排:

// 先用 kNN 取 50 个候选,再用 rerank 精排取 top 10
GET /knowledge-base/_search
{
  "size": 10,
  "knn": {
    "field": "content.embedding",
    "query_vector_builder": {
      "text_embedding": {
        "model_id": "my-embedding",
        "model_text": "如何优化搜索速度"
      }
    },
    "k": 50,
    "num_candidates": 500
  },
  "rescore": {
    "window_size": 50,
    "query": {
      "rescore_query": {
        "text_expansion": {
          "content": {
            "model_id": ".elser_model_2",
            "model_text": "如何优化搜索速度"
          }
        }
      }
    }
  }
}

这种两阶段检索(粗排 + 精排)是 RAG 系统的标准模式:

  1. 粗排阶段:用低延迟的向量搜索获取候选集
  2. 精排阶段:用更精确的模型对候选集重新打分

在延迟和质量之间取得平衡。

7.6 向量搜索性能优化

7.6.1 量化选择

量化类型 精度 速度 存储 适用场景
hnsw(无量化) 最高 最大 对精度要求极高,小规模数据
bbq_disk(9.4 默认) 小 60-70% 通用场景,推荐
bbq_memory 最快 小 60-70% 内存充足,追求极致延迟
1-bit 量化 最快 最小 大规模粗排
// 配置量化位深度(9.4 新增)
{
  "properties": {
    "embedding": {
      "type": "dense_vector",
      "dims": 1536,
      "index_options": {
        "type": "bbq_disk",
        "config": {
          "bits": 4    // 可选 1, 2, 4, 7
        }
      }
    }
  }
}

7.6.2 过滤优化

kNN 查询支持 filter 参数,在向量搜索前先过滤候选集。9.4 的 DiskBBQ 算法在带限制性过滤的场景下性能提升 3 倍以上:

// 带过滤的 kNN 查询
{
  "knn": {
    "field": "embedding",
    "query_vector": [/* ... */],
    "k": 10,
    "num_candidates": 100,
    "filter": {
      "bool": {
        "filter": [
          { "term": { "category": "技术" } },
          { "range": { "publish_date": { "gte": "2026-01-01" } } }
        ]
      }
    }
  }
}

7.6.3 GPU 加速(9.4 GA)

9.4 中 GPU 向量索引正式 GA,通过集成 NVIDIA cuVS 实现:

# elasticsearch.yml 配置
node.attr.ml.gpu: true

GPU 加速效果:

  • 索引吞吐提升 12 倍
  • force merge 快 7 倍
  • 适合大规模向量索引场景(亿级以上文档)

第八章 性能调优深度实战

从 JVM 堆内存到查询缓存,系统性地调优集群性能。

8.1 JVM 堆内存调优

ElasticSearch 运行在 JVM 上,堆内存配置是最基础也最关键的调优项。核心原则:堆内存不超过物理内存的 50%,且不超过 31 GB(压缩指针的阈值)。

配置项 推荐值 说明
-Xms-Xmx 相同值,设为物理内存的 50% 避免堆动态扩缩导致的停顿
堆内存上限 ≤ 31 GB 超过 31 GB 无法使用压缩指针,内存效率下降
堆外内存 留给 Lucene 文件系统缓存 Lucene 依赖 OS 页缓存加速 Segment 读取
GC 算法 G1GC(JDK 21+ 默认) ElasticSearch 9.x 内置 JDK 21,G1GC 适合大堆
# jvm.options 配置
-Xms16g
-Xmx16g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=25

# 或通过环境变量
ES_JAVA_OPTS="-Xms16g -Xmx16g -XX:+UseG1GC"

堆内存不是越大越好:超过 31 GB 后,JVM 无法使用压缩指针(Compressed OOPs),每个对象引用从 4 字节变为 8 字节,实际有效内存反而下降。更重要的是,Lucene 依赖操作系统的文件系统缓存来加速 Segment 的读取——如果堆占满物理内存,Lucene 的随机读取将退化为磁盘 I/O,查询性能断崖式下降。留 50% 物理内存给 OS 缓存是铁律。

JVM 调优检查清单

  • [ ] -Xms-Xmx 设置为相同值
  • [ ] 堆内存不超过 31 GB
  • [ ] 堆内存不超过物理内存的 50%
  • [ ] 使用 G1GC(9.x 默认)
  • [ ] 监控 GC 频率和停顿时间
  • [ ] 确认使用压缩指针(日志中有 Compressed ordinary object pointers

8.2 查询性能调优

8.2.1 filter 缓存与查询结构

filter 上下文的查询结果会被自动缓存为位图(bitmap),相同条件再次查询时直接命中缓存,跳过整个倒排索引扫描。关键是要确保过滤条件可缓存且重复使用。

// 好的做法:filter 中的条件稳定且可缓存
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "耳机" } }
      ],
      "filter": [
        { "term": { "category": "数码" } },
        { "range": { "price": { "gte": 100, "lte": 2000 } } }
      ]
    }
  }
}

// 避免的做法:将不需要打分的条件放在 must 中
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "耳机" } },
        { "term": { "category": "数码" } },      // 浪费:不需要打分
        { "range": { "price": { "gte": 100 } } }  // 浪费:不被缓存
      ]
    }
  }
}

哪些查询会被缓存

  • termtermsrange 在 filter 上下文中
  • boolfiltermust_not 子句
  • 使用 now/1d 等舍入表达式的日期 range

哪些查询不会被缓存

  • matchmulti_match 等 query 上下文查询
  • 使用 now 精确到毫秒的日期 range
  • script 查询

8.2.2 避免脚本查询

script_score 和脚本聚合是性能杀手——每条匹配文档都需要执行脚本,无法利用索引结构。如果必须使用脚本,优先用 painless 并启用脚本缓存:

// 避免:script_score 全表扫描
{
  "query": {
    "script_score": {
      "query": { "match_all": {} },
      "script": { "source": "doc['popularity'].value * doc['recency'].value" }
    }
  }
}

// 替代方案:在写入时计算好排序字段
// 文档写入时增加 rank_score 字段
{ "name": "耳机", "rank_score": 8.5, "popularity": 100, "recency": 0.085 }

// 查询时直接用 function_score 或 sort
{
  "query": { "match": { "name": "耳机" } },
  "sort": [{ "rank_score": "desc" }]
}

必须使用脚本时的优化

// 使用 script 的正确姿势
{
  "query": {
    "bool": {
      "filter": [
        { "range": { "price": { "gte": 100 } } }  // 先用 filter 缩小范围
      ]
    }
  },
  "script_fields": {
    "custom_score": {
      "script": {
        "source": "doc['popularity'].value * params.weight",
        "params": { "weight": 1.5 }  // 使用 params 避免编译
      }
    }
  }
}

关键点:

  1. 先用 filter 缩小文档范围,再对结果执行脚本
  2. 使用 params 传递参数,脚本源码会被编译缓存
  3. 避免 doc['field'] 访问 text 字段(无法访问分词后的值)

8.2.3 日期查询优化

date 字段做范围查询时,使用 now/1d 这样的舍入表达式可以利用查询缓存:

// 好:舍入到天,可缓存
{ "range": { "create_time": { "gte": "now/d", "lt": "now+1d/d" } } }

// 差:精确到毫秒,每次查询不同,不缓存
{ "range": { "create_time": { "gte": "2026-07-04T10:30:00.123Z" } } }

8.2.4 索引预排序

如果查询总是按某个字段排序(如时间倒序),可以在索引设置中启用 index.sort,写入时文档按指定字段排序存储,查询时 Lucene 可以提前终止扫描:

PUT /logs-app
{
  "settings": {
    "index.sort.field": ["@timestamp"],
    "index.sort.order": ["desc"]
  }
}

查询 sort @timestamp desc, size 10 时,Lucene 按已排序顺序读取文档,收集到 10 条后立即终止,无需扫描全部匹配文档。对于时间序列数据,这可以带来数量级的性能提升。

索引预排序的性能收益

场景:10 亿条日志,查询最近 10 条
────────────────────────────────────
无预排序:     ~3000 ms(扫描所有匹配文档后排序取 top 10)
有预排序:       ~5 ms(按已排序顺序读取 10 条即终止)
提升:        600 倍

8.3 集群级调优

8.3.1 节点角色分离

生产集群建议将主节点、数据节点、协调节点分离部署:

# elasticsearch.yml 节点角色配置

# 专用主节点(3 个,保证选举多数)
node.roles: [master]

# 数据节点(根据数据温度细分)
node.roles: [data, data_content, data_hot, data_warm, data_cold]

# 协调节点(不存数据,只路由和合并)
node.roles: []

# ingest 节点(数据预处理)
node.roles: [ingest]

节点配置建议

节点角色 CPU 内存 磁盘 说明
master 4 核 8 GB 50 GB SSD 只需存集群状态,配置不需要太高
data_hot 16+ 核 64 GB 1-2 TB NVMe SSD 高 IOPS,写入和查询负载重
data_warm 8 核 32 GB 2-4 TB SSD 中等性能,查询为主
data_cold 4 核 16 GB 4-8 TB HDD 低成本存储,偶尔查询
coordinating 8+ 核 16-32 GB 100 GB 需要内存做结果合并和聚合
ingest 4-8 核 8-16 GB 100 GB 执行 pipeline 预处理

8.3.2 线程池配置

// 查看线程池状态
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected,completed

// 关键线程池
// search: 查询线程池,queue 满会拒绝查询请求
// write: 写入线程池,queue 满会拒绝写入请求
// merge: Segment 合并线程池

// 调整搜索线程池大小
PUT _cluster/settings
{
  "transient": {
    "thread_pool.search.queue_size": 2000,
    "thread_pool.search.size": 20
  }
}

rejected 指标:当线程池的 active 线程数达到上限且 queue 满时,新请求会被 rejected。持续增长 的 rejected 数表明集群负载过高,需要扩容或优化查询。

8.3.3 熔断器配置

ElasticSearch 有多层熔断器防止 OOM:

熔断器 默认阈值 说明
parent 95% 总堆使用率上限
fielddata 40% fielddata 缓存上限
request 60% 单请求内存上限
in_flight_requests 100% 进行中请求内存上限
script_compilation 75/5min 5 分钟内脚本编译数上限
// 调整熔断器
PUT _cluster/settings
{
  "transient": {
    "indices.breaker.total.limit": "70%",
    "indices.breaker.fielddata.limit": "40%",
    "indices.breaker.request.limit": "60%"
  }
}

8.4 监控与诊断

8.4.1 关键监控 API

API 用途 关键指标
_cluster/health 集群健康状态 status、number_of_nodes、unassigned_shards
_cat/indices?v 索引概览 docs.count、store.size、health
_cat/shards?v 分片分布 shard、prirep、state、node
_nodes/stats 节点统计 jvm.mem、thread_pool、indices.search
_cat/thread_pool?v 线程池状态 active、queue、rejected
_search/profile 查询性能分析 time_in_nanos、breakdown
_cat/allocation?v 磁盘使用 disk.used、disk.percent
_nodes/hot_threads 热点线程 CPU 占用高的线程

8.4.2 慢查询日志

// 开启慢查询日志
PUT /products/_settings
{
  "index.search.slowlog.threshold.query.warn": "2s",
  "index.search.slowlog.threshold.query.info": "500ms",
  "index.search.slowlog.threshold.fetch.warn": "1s",
  "index.search.slowlog.threshold.fetch.info": "200ms",
  "index.indexing.slowlog.threshold.index.warn": "1s",
  "index.indexing.slowlog.threshold.index.info": "200ms"
}

慢查询日志记录超过阈值的查询详情,是发现性能问题的第一手段。配合 _search/profile 可以精确定位到是哪个子查询慢、是否命中缓存、扫描了多少文档。

慢查询日志示例

[2026-07-04T10:30:15,123][WARN][index.search.slowlog.query] [node-1] [products][0] took[1.5s], took_millis[1500], total_hits[1000000], types[], stats[], search_type[QUERY_THEN_FETCH], total_shards[3], source[{"query":{"bool":{"must":[{"match":{"name":{"query":"耳机"}}}]}}}],

8.4.3 热点线程分析

当节点 CPU 使用率异常时,使用 hot_threads API 定位问题:

# 获取热点线程
GET /_nodes/hot_threads?threads=10&interval=500ms&snapshots=10

# 输出示例
# node-1
#   85.3% [cpu] com.elasticsearch.search.SearchService.executeQuery()
#   10.2% [cpu] org.apache.lucene.search.BooleanQuery$BooleanWeight.scorer()
#    4.5% [cpu] com.lucene.index.SegmentReader.document()

8.5 常见性能问题排查

8.5.1 查询慢的排查流程

查询慢
  │
  ├── 是否深度分页?
  │     ├── 是 → 改用 search_after / PIT
  │     └── 否 ↓
  │
  ├── filter 是否在 must 中?
  │     ├── 是 → 移到 filter 子句
  │     └── 否 ↓
  │
  ├── 是否有脚本查询?
  │     ├── 是 → 预计算字段替代脚本
  │     └── 否 ↓
  │
  ├── 聚合是否高基数?
  │     ├── 是 → 用 composite 或 cardinality
  │     └── 否 ↓
  │
  ├── Segment 数量是否过多?
  │     ├── 是 → force merge 只读索引
  │     └── 否 ↓
  │
  ├── 是否使用 profile 定位?
  │     └── 使用 _search?profile=true 找到最慢子查询
  │
  └── 检查集群负载
        ├── GC 停顿是否频繁 → 调整 JVM
        ├── 磁盘 I/O 是否饱和 → 增加 hot 节点
        └── 线程池 rejected 是否高 → 扩容或优化

8.5.2 写入慢的排查流程

写入慢
  │
  ├── refresh_interval 是否过短?
  │     └── 批量写入时设为 -1 或 30s
  │
  ├── Bulk 批次是否太小?
  │     └── 调整到 5-15 MB
  │
  ├── 副本同步是否成为瓶颈?
  │     └── 批量导入时临时设为 0
  │
  ├── Segment 合并是否落后?
  │     └── 检查 _cat/segments,调整合并策略
  │
  ├── translog 是否过大?
  │     └── 检查 index.translog.flush_threshold_size
  │
  └── 检查磁盘 I/O
        └── SSD vs HDD,IOPS 是否充足

8.5.3 常见问题速查表

症状 可能原因 解决方案
查询偶尔超时 GC 停顿 检查 JVM 堆使用率,调整 GC
聚合 OOM 高基数聚合 用 composite 分页或 cardinality
写入 rejected write 线程池满 扩容 data 节点或优化批量大小
集群状态 yellow 副本未分配 检查节点数量和磁盘水位
搜索结果不全 分片故障 检查 _cluster/health 和 _cat/shards
磁盘只读 超过 flood_stage 清理数据或增加磁盘,手动解除只读
CPU 飙高 合并或查询风暴 检查 hot_threads,限制合并线程
内存增长不释放 fielddata 缓存 限制 fielddata 断路器

第九章 生产级实战案例

将前面学到的知识综合应用到电商搜索和日志分析两个典型场景。

9.1 电商搜索系统

电商搜索是 ElasticSearch 最典型的应用场景,需要同时满足相关性、性能和业务规则的需求。

9.1.1 索引设计

PUT /ecommerce
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "refresh_interval": "1s",
    "analysis": {
      "analyzer": {
        "pinyin_analyzer": {
          "tokenizer": "pinyin_tokenizer"
        }
      },
      "tokenizer": {
        "pinyin_tokenizer": {
          "type": "pinyin",
          "keep_first_letter": true,
          "keep_full_pinyin": true,
          "lowercase": true
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "product_id": { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart",
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 },
          "pinyin": { "type": "text", "analyzer": "pinyin_analyzer" },
          "completion": { "type": "completion" }
        }
      },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "category": { "type": "keyword" },
      "brand": { "type": "keyword" },
      "tags": { "type": "keyword" },
      "sales_count": { "type": "integer" },
      "rating": { "type": "float" },
      "status": { "type": "keyword" },
      "create_time": { "type": "date" },
      "description": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

这个映射实现了多维度搜索能力:名称支持中文分词(IK)、拼音搜索和自动补全;价格为 scaled_float 避免精度问题;分类、品牌、标签为 keyword 支持精确过滤和聚合。

9.1.2 搜索查询实现

GET /ecommerce/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "蓝牙耳机",
            "fields": ["name^5", "name.pinyin^3", "description^1"],
            "type": "best_fields",
            "minimum_should_match": "70%"
          }
        }
      ],
      "should": [
        { "term": { "tags": "热销" } },
        { "range": { "rating": { "gte": 4.5 } } }
      ],
      "filter": [
        { "term": { "status": "on_sale" } },
        { "range": { "price": { "gte": 50, "lte": 3000 } } }
      ],
      "minimum_should_match": 0
    }
  },
  "functions": [
    {
      "filter": { "range": { "sales_count": { "gte": 1000 } } },
      "weight": 1.5
    }
  ],
  "sort": [
    { "_score": "desc" },
    { "sales_count": "desc" },
    { "create_time": "desc" }
  ],
  "aggs": {
    "categories": { "terms": { "field": "category", "size": 10 } },
    "brands": { "terms": { "field": "brand", "size": 10 } },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 100 },
          { "from": 100, "to": 500 },
          { "from": 500, "to": 1000 },
          { "from": 1000 }
        ]
      }
    }
  },
  "size": 20
}

这个查询同时实现了:

  • 多字段加权搜索(名称权重最高,拼音次之,描述最低)
  • 状态和价格过滤(走缓存)
  • 热销标签和评分加权提升
  • 销量和时间的多级排序
  • 分类/品牌/价格区间的聚合面(用于搜索结果页的筛选器)

9.1.3 搜索补全实现

// 搜索补全查询
GET /ecommerce/_search
{
  "suggest": {
    "product-suggest": {
      "prefix": "蓝",
      "completion": {
        "field": "name.completion",
        "size": 10,
        "skip_duplicates": true
      }
    }
  }
}

// 同时返回搜索结果和补全建议
GET /ecommerce/_search
{
  "query": { "match_all": {} },
  "suggest": {
    "product-suggest": {
      "prefix": "蓝牙",
      "completion": {
        "field": "name.completion",
        "size": 5
      }
    }
  },
  "size": 0
}

9.1.4 定时同步与更新

# Python 示例:从数据库同步商品到 ElasticSearch
import psycopg2
from elasticsearch import Elasticsearch, helpers

es = Elasticsearch(["http://localhost:9200"])
pg = psycopg2.connect("dbname=shop user=postgres")

def sync_products():
    cursor = pg.cursor()
    cursor.execute("""
        SELECT id, name, price, category, brand, tags, sales_count,
               rating, status, create_time, description
        FROM products
        WHERE update_time > NOW() - INTERVAL '1 hour'
    """)

    actions = []
    for row in cursor:
        action = {
            "_index": "ecommerce",
            "_id": row[0],
            "_source": {
                "product_id": row[0],
                "name": row[1],
                "price": row[2],
                "category": row[3],
                "brand": row[4],
                "tags": row[5],
                "sales_count": row[6],
                "rating": row[7],
                "status": row[8],
                "create_time": row[9],
                "description": row[10],
                "name_completion": {
                    "input": [row[1], row[1].split()],
                    "weight": row[6]  # 按销量设置权重
                }
            }
        }
        actions.append(action)

    helpers.bulk(es, actions, chunk_size=500)
    print(f"Synced {len(actions)} products")

9.2 日志分析平台

日志分析是 ElasticSearch 另一个核心场景。

9.2.1 索引模板设计

PUT _index_template/app-logs
{
  "index_patterns": ["app-logs-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs-policy",
      "index.refresh_interval": "1s",
      "index.mapping.total_fields.limit": 2000,
      "index.sort.field": ["@timestamp"],
      "index.sort.order": ["desc"]
    },
    "mappings": {
      "dynamic": "true",
      "dynamic_templates": [
        {
          "strings_as_keyword": {
            "match_mapping_type": "string",
            "mapping": { "type": "keyword", "ignore_above": 512 }
          }
        }
      ],
      "properties": {
        "@timestamp": { "type": "date" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" },
        "host": { "type": "keyword" },
        "message": { "type": "text", "analyzer": "standard" },
        "trace_id": { "type": "keyword" },
        "duration_ms": { "type": "integer" }
      }
    }
  }
}

9.2.2 使用 ES|QL 分析日志

// 按服务统计错误率和 P99 延迟
FROM app-logs
| WHERE @timestamp >= NOW() - 1 hours
| STATS
    total = COUNT(*),
    errors = COUNT(WHERE level == "ERROR"),
    error_rate = errors * 100.0 / total,
    p99_latency = PERCENTILE(duration_ms, 99),
    avg_latency = AVG(duration_ms)
  BY service
| SORT error_rate DESC

// 查找慢请求的调用链
FROM app-logs
| WHERE duration_ms > 1000 AND level != "DEBUG"
| KEEP @timestamp, service, trace_id, message, duration_ms
| SORT duration_ms DESC
| LIMIT 50

// 错误趋势分析
FROM app-logs
| WHERE level == "ERROR" AND @timestamp >= NOW() - 24 hours
| EVAL hour = DATE_TRUNC(1 hour, @timestamp)
| STATS error_count = COUNT(*) BY hour, service
| SORT hour ASC, service

// Top 10 错误消息
FROM app-logs
| WHERE level == "ERROR"
| STATS count = COUNT(*) BY message
| SORT count DESC
| LIMIT 10

9.2.3 告警查询

// 使用 Kibana Alerting 配置告警条件
GET /app-logs/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "range": { "@timestamp": { "gte": "now-5m/m" } } },
        { "terms": { "level": ["ERROR", "FATAL"] } }
      ]
    }
  },
  "aggs": {
    "by_service": {
      "terms": { "field": "service", "size": 20 },
      "aggs": {
        "error_count": { "value_count": { "field": "@timestamp" } },
        "sample_messages": {
          "top_hits": {
            "size": 3,
            "_source": ["message", "trace_id"],
            "sort": [{ "@timestamp": "desc" }]
          }
        }
      }
    }
  }
}

这个告警查询在最近 5 分钟内按服务分组统计错误数,并附带 3 条最新错误消息的样本。配合 Kibana Alerting 可以在错误数超过阈值时触发通知。

9.2.4 使用 Ingest Pipeline 预处理日志

// 创建 Ingest Pipeline
PUT _ingest/pipeline/app-logs-pipeline
{
  "description": "应用日志预处理",
  "processors": [
    {
      "grok": {
        "field": "message",
        "patterns": [
          "%{TIMESTAMP_ISO8601:log_time} \\[%{LOGLEVEL:level}\\] \\[%{DATA:service}\\] \\[%{DATA:trace_id}\\] %{GREEDYDATA:msg}"
        ]
      }
    },
    {
      "date": {
        "field": "log_time",
        "formats": ["ISO8601"],
        "target_field": "@timestamp"
      }
    },
    {
      "convert": {
        "field": "duration_ms",
        "type": "integer",
        "ignore_missing": true
      }
    },
    {
      "script": {
        "source": """
          if (ctx.duration_ms != null && ctx.duration_ms > 2000) {
            ctx.slow_request = true;
          }
        """
      }
    },
    {
      "remove": {
        "field": "log_time",
        "ignore_missing": true
      }
    }
  ],
  "on_failure": [
    {
      "set": {
        "field": "error.message",
        "value": "Pipeline failed: {{ _ingest.on_failure_message }}"
      }
    }
  ]
}

// 在索引设置中指定默认 pipeline
PUT _component_template/app-logs-pipeline
{
  "template": {
    "settings": {
      "index.default_pipeline": "app-logs-pipeline"
    }
  }
}

9.3 多集群与跨集群搜索

对于大规模部署,可以使用跨集群搜索(CCS)在多个集群上执行查询:

// 配置远程集群
PUT _cluster/settings
{
  "persistent": {
    "cluster": {
      "remote": {
        "cluster_east": {
          "seeds": ["east-node1:9300", "east-node2:9300"]
        },
        "cluster_west": {
          "seeds": ["west-node1:9300", "west-node2:9300"]
        }
      }
    }
  }
}

// 跨集群搜索
GET /cluster_east:logs-*,cluster_west:logs-*/_search
{
  "query": {
    "bool": {
      "filter": [
        { "range": { "@timestamp": { "gte": "now-1h" } } },
        { "term": { "level": "ERROR" } }
      ]
    }
  },
  "size": 50
}

// 跨集群 ES|QL 查询
POST /_query
{
  "query": """
    FROM cluster_east:app-logs, cluster_west:app-logs
    | WHERE level == "ERROR" AND @timestamp >= NOW() - 1 hours
    | STATS error_count = COUNT(*) BY service
    | SORT error_count DESC
  """
}

9.4 安全配置

生产环境的安全配置清单:

# elasticsearch.yml
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.http.ssl.enabled: true

# 生成证书
# elasticsearch-certutil ca
# elasticsearch-certutil cert --ca elastic-stack-ca.p12

# 创建用户和角色
// 创建自定义角色
POST /_security/role/logs_reader
{
  "indices": [
    {
      "names": ["app-logs-*"],
      "privileges": ["read", "view_index_metadata"]
    }
  ]
}

// 创建用户
POST /_security/user/log_viewer
{
  "password": "secure_password",
  "roles": ["logs_reader"],
  "full_name": "Log Viewer"
}

// 创建 API Key(推荐用于服务间认证)
POST /_security/api_key
{
  "name": "log-collector",
  "role_descriptors": {
    "log_writer": {
      "indices": [
        {
          "names": ["app-logs-*"],
          "privileges": ["write", "create_doc", "create_index"]
        }
      ]
    }
  }
}

9.5 学习路径总结

入门                进阶                高级                优化                专家                生产
 │                  │                  │                  │                  │                  │
 ├─ 概念+CRUD       ├─ 映射+DSL        ├─ 聚合+分页       ├─ 索引+查询调优   ├─ ES|QL+向量搜索  ├─ 集群运维+监控
 ├─ 倒排索引        ├─ 分析器+分词     ├─ 管道聚合        ├─ JVM 调优        ├─ 混合搜索        ├─ 安全配置
 ├─ 分片+副本       ├─ 动态模板        ├─ PIT+search_after├─ Segment 合并    ├─ RAG 重排序      ├─ 跨集群搜索
 └─ REST API        └─ nested/join     └─ composite 聚合  └─ 索引预排序      └─ GPU 加速        └─ ILM 冷热分层

本教程覆盖了从入门到生产的完整路径。掌握索引映射设计、查询 DSL 编写和性能调优是三个最核心的能力——映射决定了数据的存储效率和功能边界,DSL 决定了查询的表达能力和性能表现,调优则决定了系统能否在生产负载下稳定运行。ES|QL 和向量搜索代表了 ElasticSearch 的未来方向,值得持续投入学习。

参考资料

  1. Elasticsearch 9.4 Release Notes - https://www.elastic.co/docs/release-notes/elasticsearch
  2. Elasticsearch 9.4 What’s New - https://versionlog.com/elasticsearch/9.4/
  3. Elastic Documentation - Tune for search speed - https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/search-speed
  4. Elastic Documentation - Tune for disk usage - https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/disk-usage
  5. Elastic Documentation - ES|QL reference - https://www.elastic.co/docs/reference/query-languages/esql
  6. Elastic Documentation - Dense vector field type - https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector
  7. Elastic Search Labs - Elasticsearch sizing best practices - https://www.elastic.co/search-labs/kr/blog/elasticsearch-node-shard-size-best-practices