Hadoop学习教程,从入门到精通,Elasticsearch 完整知识点详解(16)

Elasticsearch 完整知识点详解


一、什么是 Elasticsearch

1.1 概念

Elasticsearch 是一个基于 Apache Lucene 构建的开源、分布式、RESTful 风格的全文搜索和分析引擎

1.2 核心特点

特点 说明
分布式 数据自动分布到多个节点
RESTful API 基于 HTTP + JSON 进行交互
近实时(NRT) 文档写入后约 1 秒即可被搜索到
高可用 通过副本机制实现故障转移
多类型搜索 全文搜索、结构化搜索、分析聚合

1.3 典型应用场景

复制代码
- 电商商品搜索(京东、淘宝)
- 日志分析(ELK:Elasticsearch + Logstash + Kibana)
- 站内搜索
- 数据可视化分析

二、基本概念

2.1 索引(Index)、类型(Type)和文档(Document)

2.1.1 与关系型数据库的类比
复制代码
关系型数据库          Elasticsearch
─────────────────────────────────
Database(数据库)    Index(索引)
Table(表)          Type(类型)[7.x版本后已废弃]
Row(行)            Document(文档)
Column(列)         Field(字段)
Schema(表结构)     Mapping(映射)
SQL                  DSL(Domain Specific Language)
2.1.2 索引(Index)

索引是具有相似结构的文档的集合,类似于数据库中的一张表。

json 复制代码
// 创建一个名为 "employee" 的索引
// PUT 请求:http://localhost:9200/employee
PUT /employee
{
  "settings": {
    "number_of_shards": 3,      // 设置主分片数量为3
    "number_of_replicas": 1      // 设置每个主分片的副本数为1
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text"            // name字段为文本类型,支持全文搜索
      },
      "age": {
        "type": "integer"         // age字段为整数类型
      },
      "email": {
        "type": "keyword"         // email字段为关键字类型,精确匹配
      }
    }
  }
}
2.1.3 类型(Type)
  • 在 Elasticsearch 6.x 中,一个索引可以有多个类型
  • 在 Elasticsearch 7.x 中,Type 已被废弃 ,默认为 _doc
  • 在 Elasticsearch 8.x 中,Type 完全移除
json 复制代码
// 6.x 版本:创建带类型的映射
// PUT /employee/_mapping/fulltime
PUT /employee/_mapping
{
  "properties": {
    "name": { "type": "text" },
    "age": { "type": "integer" },
    "hire_date": { "type": "date" }    // 日期类型
  }
}
2.1.4 文档(Document)

文档是索引中一条可以被索引的基本信息单元,以 JSON 格式表示。

json 复制代码
// 往 employee 索引中添加一个文档
// POST http://localhost:9200/employee/_doc/1
POST /employee/_doc/1
{
  "name": "张三",                    // 员工姓名
  "age": 28,                          // 员工年龄
  "email": "zhangsan@example.com",   // 员工邮箱
  "department": "技术部",             // 所属部门
  "salary": 15000.00,                // 薪资
  "hire_date": "2023-01-15"          // 入职日期
}

2.2 分片(Shards)和副本(Replicas)

2.2.1 分片(Shards)
复制代码
为什么要分片?
─────────────────────────────────────────────
1. 单个索引数据量过大,超出单个节点的存储容量
2. 搜索请求负载过高,单个节点处理能力不足
─────────────────────────────────────────────
分片类型:
  - 主分片(Primary Shard):每个文档存储在一个主分片中
  - 副本分片(Replica Shard):主分片的复制,用于容灾和提升读性能
json 复制代码
// 创建索引时指定分片和副本
PUT /my_index
{
  "settings": {
    "number_of_shards": 5,       // 主分片数量:5个(创建后不可修改)
    "number_of_replicas": 2      // 副本数量:2个(运行时可修改)
  }
}

// 运行时修改副本数量(主分片数量创建后不能修改)
PUT /my_index/_settings
{
  "number_of_replicas": 3        // 将副本数从2修改为3
}
2.2.2 分片的工作原理图解
复制代码
                    ┌─── Node 1 ───┐
                    │  Shard P0     │  P0 = 主分片0
                    │  Shard R1     │  R1 = 副本分片1
                    └───────────────┘
                    
  Index (5个主分片, 1个副本)
  
                    ┌─── Node 2 ───┐
                    │  Shard P1     │  P1 = 主分片1
                    │  Shard R2     │  R2 = 副本分片2
                    └───────────────┘
                    
                    ┌─── Node 3 ───┐
                    │  Shard P2     │  P2 = 主分片2
                    │  Shard R0     │  R0 = 副本分片0
                    └───────────────┘
                    
                    ┌─── Node 4 ───┐
                    │  Shard P3     │
                    │  Shard R4     │
                    └───────────────┘
                    
                    ┌─── Node 5 ───┐
                    │  Shard P4     │
                    │  Shard R3     │
                    └───────────────┘

2.3 路由(Routing)

2.3.1 路由原理
复制代码
文档写入时,Elasticsearch 根据以下公式决定文档存储在哪个分片:
─────────────────────────────────────────────
shard = hash(routing) % number_of_primary_shards
─────────────────────────────────────────────
默认 routing = 文档的 _id
2.3.2 自定义路由
json 复制代码
// 使用自定义路由将文档路由到特定分片
// 让同一部门的员工文档路由到同一个分片,提高查询效率
POST /employee/_doc?routing=技术部
{
  "name": "李四",
  "age": 32,
  "department": "技术部",
  "salary": 18000.00
}

// 使用自定义路由进行查询(必须指定相同的routing)
GET /employee/_search?routing=技术部
{
  "query": {
    "match": {
      "name": "李四"
    }
  }
}

三、集群架构

3.1 节点类型

复制代码
Elasticsearch 集群中的节点角色:
─────────────────────────────────────────────────────
角色                   说明
─────────────────────────────────────────────────────
Master Node           负责集群管理(创建/删除索引、分配分片等)
Data Node             负责数据存储和搜索操作
Coordinating Node     负责接收客户端请求并转发到合适节点
Ingest Node           负责数据预处理(管道处理)
Machine Learning Node 负责机器学习任务
─────────────────────────────────────────────────────

3.2 集群架构图

复制代码
                        ┌─────────────┐
        请求 ────────> │ Coordinating │
                        │    Node      │
                        └──────┬──────┘
                               │
                  ┌────────────┼────────────┐
                  ▼            ▼            ▼
           ┌──────────┐ ┌──────────┐ ┌──────────┐
           │Master     │ │Data      │ │Data      │
           │Node       │ │Node 1    │ │Node 2    │
           │(可兼Data) │ │          │ │          │
           └──────────┘ └──────────┘ └──────────┘

四、集群环境搭建

4.1 单节点安装

bash 复制代码
# ============================================================
# 步骤1:下载 Elasticsearch
# ============================================================
# 从官网下载 Elasticsearch 7.17.x 版本
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.9-linux-x86_64.tar.gz

# ============================================================
# 步骤2:解压安装包
# ============================================================
tar -zxvf elasticsearch-7.17.9-linux-x86_64.tar.gz
# 解压后得到 elasticsearch-7.17.9 目录

# ============================================================
# 步骤3:创建 elasticsearch 用户
# ============================================================
# ES不允许以 root 用户启动,必须创建专用用户
useradd elasticsearch           # 创建 elasticsearch 用户
passwd elasticsearch            # 设置密码
chown -R elasticsearch:elasticsearch /opt/elasticsearch-7.17.9  # 授权

# ============================================================
# 步骤4:修改配置文件
# ============================================================
vim /opt/elasticsearch-7.17.9/config/elasticsearch.yml
yaml 复制代码
# ======================== elasticsearch.yml ========================
# 集群名称(同一集群中的所有节点必须相同)
cluster.name: my-application

# 节点名称(集群中每个节点必须唯一)
node.name: node-1

# 数据存储路径
path.data: /opt/elasticsearch-7.17.9/data

# 日志存储路径
path.logs: /opt/elasticsearch-7.17.9/logs

# 绑定的IP地址(0.0.0.0 表示允许所有IP访问)
network.host: 0.0.0.0

# HTTP端口
http.port: 9200

# 集群内部通信端口
transport.port: 9300

# 集群初始主节点列表(单节点模式)
cluster.initial_master_nodes: ["node-1"]
bash 复制代码
# ============================================================
# 步骤5:修改 JVM 参数
# ============================================================
vim /opt/elasticsearch-7.17.9/config/jvm.options

# 设置 JVM 堆内存(建议设置为物理内存的一半,最大不超过32GB)
-Xms512m      # 初始堆内存大小
-Xmx512m      # 最大堆内存大小

# ============================================================
# 步骤6:系统参数调整
# ============================================================
# 修改系统文件描述符限制
vim /etc/security/limits.conf
# 在文件末尾添加:
elasticsearch soft nofile 65536     # 软限制:最大打开文件数
elasticsearch hard nofile 65536     # 硬限制:最大打开文件数
elasticsearch soft nproc 4096       # 软限制:最大进程数
elasticsearch hard nproc 4096       # 硬限制:最大进程数

# 修改虚拟内存限制
vim /etc/sysctl.conf
# 在文件末尾添加:
vm.max_map_count=262144             # 最大内存映射区域数

# 使配置生效
sysctl -p

# ============================================================
# 步骤7:启动 Elasticsearch
# ============================================================
# 切换到 elasticsearch 用户
su elasticsearch

# 后台启动
/opt/elasticsearch-7.17.9/bin/elasticsearch -d

# ============================================================
# 步骤8:验证是否启动成功
# ============================================================
curl http://localhost:9200
# 正常返回:
# {
#   "name" : "node-1",
#   "cluster_name" : "my-application",
#   "cluster_uuid" : "...",
#   "version" : {
#     "number" : "7.17.9",
#     ...
#   },
#   "tagline" : "You Know, for Search"
# }

4.2 集群多节点搭建(3节点)

yaml 复制代码
# ======================== node-1 的 elasticsearch.yml ========================
cluster.name: my-application          # 集群名称(三个节点一致)
node.name: node-1                     # 节点1名称
node.master: true                     # 该节点可以作为主节点
node.data: true                       # 该节点存储数据
path.data: /opt/es/data
path.logs: /opt/es/logs
network.host: 192.168.1.101           # 绑定本机IP
http.port: 9200
transport.port: 9300
discovery.seed_hosts:                 # 集群发现:列出所有节点的IP
  - 192.168.1.101
  - 192.168.1.102
  - 192.168.1.103
cluster.initial_master_nodes:         # 初始主节点候选列表
  - node-1
  - node-2
  - node-3
yaml 复制代码
# ======================== node-2 的 elasticsearch.yml ========================
cluster.name: my-application          # 集群名称(与node-1一致)
node.name: node-2                     # 节点2名称(必须唯一)
node.master: true
node.data: true
path.data: /opt/es/data
path.logs: /opt/es/logs
network.host: 192.168.1.102           # 绑定node-2的IP
http.port: 9200
transport.port: 9300
discovery.seed_hosts:
  - 192.168.1.101
  - 192.168.1.102
  - 192.168.1.103
cluster.initial_master_nodes:
  - node-1
  - node-2
  - node-3
yaml 复制代码
# ======================== node-3 的 elasticsearch.yml ========================
cluster.name: my-application
node.name: node-3
node.master: true
node.data: true
path.data: /opt/es/data
path.logs: /opt/es/logs
network.host: 192.168.1.103
http.port: 9200
transport.port: 9300
discovery.seed_hosts:
  - 192.168.1.101
  - 192.168.1.102
  - 192.168.1.103
cluster.initial_master_nodes:
  - node-1
  - node-2
  - node-3

五、安装 Kibana

bash 复制代码
# ============================================================
# 步骤1:下载 Kibana
# ============================================================
# Kibana 版本必须与 Elasticsearch 版本一致
wget https://artifacts.elastic.co/downloads/kibana/kibana-7.17.9-linux-x86_64.tar.gz

# ============================================================
# 步骤2:解压
# ============================================================
tar -zxvf kibana-7.17.9-linux-x86_64.tar.gz

# ============================================================
# 步骤3:修改配置文件
# ============================================================
vim /opt/kibana-7.17.9-linux-x86_64/config/kibana.yml
yaml 复制代码
# ======================== kibana.yml ========================
# Kibana 服务端口
server.port: 5601

# Kibana 绑定的主机地址
server.host: "0.0.0.0"

# Kibana 显示名称
server.name: "my-kibana"

# Elasticsearch 集群地址(连接到 ES)
elasticsearch.hosts: ["http://192.168.1.101:9200"]

# 设置中文界面
i18n.locale: "zh-CN"
bash 复制代码
# ============================================================
# 步骤4:启动 Kibana
# ============================================================
# Kibana 也不能以 root 用户启动,需切换到普通用户或使用 --allow-root
/opt/kibana-7.17.9-linux-x86_64/bin/kibana --allow-root

# ============================================================
# 步骤5:访问 Kibana
# ============================================================
# 浏览器访问:http://192.168.1.101:5601
# 点击左侧菜单 "Dev Tools" 进入控制台

六、REST API

6.1 集群状态 API

json 复制代码
// ============================================================
// 1. 查看集群健康状态
// GET /_cluster/health
// 返回集群的颜色状态:green(正常)、yellow(副本不足)、red(主分片缺失)
// ============================================================
GET /_cluster/health

// 返回结果示例:
// {
//   "cluster_name": "my-application",
//   "status": "green",               // 集群状态:green/yellow/red
//   "timed_out": false,
//   "number_of_nodes": 3,            // 节点总数
//   "number_of_data_nodes": 3,       // 数据节点数
//   "active_primary_shards": 10,     // 活动主分片数
//   "active_shards": 20,             // 活动分片总数(主+副本)
//   "relocating_shards": 0,          // 正在迁移的分片数
//   "initializing_shards": 0,        // 正在初始化的分片数
//   "unassigned_shards": 0           // 未分配的分片数
// }


// ============================================================
// 2. 查看集群状态详情
// GET /_cluster/state
// 返回集群的完整状态信息
// ============================================================
GET /_cluster/state


// ============================================================
// 3. 查看集群节点信息
// GET /_cat/nodes?v
// v 参数表示显示表头(verbose)
// ============================================================
GET /_cat/nodes?v

// 返回结果示例:
// ip           heap.percent ram.percent cpu load_1m node.role master name
// 192.168.1.101          35          92   5    0.15 dilmrt    *      node-1
// 192.168.1.102          42          90   3    0.10 dilmrt    -      node-2
// 192.168.1.103          28          88   4    0.12 dilmrt    -      node-3


// ============================================================
// 4. 查看分片信息
// GET /_cat/shards?v
// ============================================================
GET /_cat/shards?v

// 返回结果示例:
// index     shard prirep state   docs  store ip            node
// employee  0     p      STARTED  2   5.2kb 192.168.1.101 node-1
// employee  0     r      STARTED  2   5.2kb 192.168.1.102 node-2
// employee  1     p      STARTED  1   3.1kb 192.168.1.102 node-2
// employee  1     r      STARTED  1   3.1kb 192.168.1.103 node-3


// ============================================================
// 5. 查看索引列表
// GET /_cat/indices?v
// ============================================================
GET /_cat/indices?v

// ============================================================
// 6. 查看主节点信息
// GET /_cat/master?v
// ============================================================
GET /_cat/master?v

6.2 索引 API

json 复制代码
// ============================================================
// 1. 创建索引
// PUT /索引名
// ============================================================
PUT /my_store
{
  "settings": {
    "number_of_shards": 3,           // 主分片数:3
    "number_of_replicas": 1           // 每个主分片的副本数:1
  },
  "mappings": {
    "properties": {
      "product_name": {
        "type": "text",               // 文本类型,支持分词和全文搜索
        "analyzer": "ik_max_word"     // 使用IK分词器进行最大分词
      },
      "category": {
        "type": "keyword"             // 关键字类型,不分词,精确匹配
      },
      "price": {
        "type": "double"              // 双精度浮点类型
      },
      "stock": {
        "type": "integer"             // 整数类型
      },
      "create_time": {
        "type": "date",               // 日期类型
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      },
      "is_on_sale": {
        "type": "boolean"             // 布尔类型
      },
      "tags": {
        "type": "keyword"             // 数组类型,数组中每个元素为keyword
      }
    }
  }
}


// ============================================================
// 2. 查看索引信息
// GET /索引名
// ============================================================
GET /my_store


// ============================================================
// 3. 查看索引的 Mapping 映射
// GET /索引名/_mapping
// ============================================================
GET /my_store/_mapping


// ============================================================
// 4. 查看索引的 Settings 设置
// GET /索引名/_settings
// ============================================================
GET /my_store/_settings


// ============================================================
// 5. 打开/关闭索引
// 索引关闭后不能进行读写操作,可用于节省资源
// ============================================================
POST /my_store/_close          // 关闭索引
POST /my_store/_open           // 打开索引


// ============================================================
// 6. 删除索引
// DELETE /索引名(操作不可逆,慎用!)
// ============================================================
DELETE /my_store


// ============================================================
// 7. 判断索引是否存在
// HEAD /索引名
// 返回 200 表示存在,404 表示不存在
// ============================================================
HEAD /my_store


// ============================================================
// 8. 修改索引设置(如副本数)
// ============================================================
PUT /my_store/_settings
{
  "number_of_replicas": 2     // 动态修改副本数(主分片数不可修改)
}


// ============================================================
// 9. 索引别名操作
// 别名可以理解为索引的"快捷方式"或"软链接"
// ============================================================

// 创建别名
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "my_store",       // 目标索引
        "alias": "store_alias"     // 别名名称
      }
    }
  ]
}

// 通过别名查询(等同于查询 my_store)
GET /store_alias/_search
{
  "query": {
    "match_all": {}                // 查询所有文档
  }
}

// 删除别名
POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "my_store",
        "alias": "store_alias"
      }
    }
  ]
}

6.3 文档 API

json 复制代码
// ============================================================
// 1. 新建文档(指定ID)
// PUT /索引名/_doc/文档ID
// 如果ID已存在则覆盖(全量替换)
// ============================================================
PUT /my_store/_doc/1
{
  "product_name": "iPhone 15 Pro",
  "category": "手机",
  "price": 8999.00,
  "stock": 500,
  "create_time": "2024-01-15 10:30:00",
  "is_on_sale": true,
  "tags": ["苹果", "5G", "旗舰"]
}


// ============================================================
// 2. 新建文档(自动生成ID)
// POST /索引名/_doc
// ============================================================
POST /my_store/_doc
{
  "product_name": "华为 Mate 60",
  "category": "手机",
  "price": 6999.00,
  "stock": 800,
  "create_time": "2024-02-20 14:00:00",
  "is_on_sale": true,
  "tags": ["华为", "5G", "鸿蒙"]
}


// ============================================================
// 3. 批量创建文档
// POST /_bulk
// 每两行为一组:第一行为操作元数据,第二行为文档数据
// ============================================================
POST /_bulk
{"index":{"_index":"my_store","_id":"3"}}                          // 操作:索引文档3
{"product_name":"小米14","category":"手机","price":3999.00,"stock":1000,"is_on_sale":true,"tags":["小米","5G","旗舰"]}
{"index":{"_index":"my_store","_id":"4"}}                          // 操作:索引文档4
{"product_name":"MacBook Pro 2024","category":"笔记本","price":14999.00,"stock":200,"is_on_sale":true,"tags":["苹果","M3","专业"]}
{"index":{"_index":"my_store","_id":"5"}}                          // 操作:索引文档5
{"product_name":"ThinkPad X1 Carbon","category":"笔记本","price":9999.00,"stock":300,"is_on_sale":true,"tags":["联想","商务","轻薄"]}


// ============================================================
// 4. 查看单个文档
// GET /索引名/_doc/文档ID
// ============================================================
GET /my_store/_doc/1

// 返回结果:
// {
//   "_index": "my_store",
//   "_type": "_doc",
//   "_id": "1",
//   "_version": 1,
//   "found": true,
//   "_source": {
//     "product_name": "iPhone 15 Pro",
//     "category": "手机",
//     ...
//   }
// }


// ============================================================
// 5. 判断文档是否存在
// HEAD /索引名/_doc/文档ID
// 返回 200 存在,404 不存在
// ============================================================
HEAD /my_store/_doc/1


// ============================================================
// 6. 更新文档(局部更新)
// POST /索引名/_update/文档ID
// 只更新指定字段,不会覆盖整个文档
// ============================================================
POST /my_store/_update/1
{
  "doc": {
    "price": 8499.00,            // 更新价格
    "stock": 480                  // 更新库存
  }
}


// ============================================================
// 7. 脚本更新(使用Painless脚本语言)
// 将文档1的库存减少100
// ============================================================
POST /my_store/_update/1
{
  "script": {
    "source": "ctx._source.stock -= params.count",   // 脚本内容:库存减少指定数量
    "params": {
      "count": 100                                     // 减少的数量
    }
  }
}


// ============================================================
// 8. 删除文档
// DELETE /索引名/_doc/文档ID
// ============================================================
DELETE /my_store/_doc/1


// ============================================================
// 9. 批量操作(混合操作:添加、更新、删除)
// ============================================================
POST /_bulk
{"index":{"_index":"my_store","_id":"6"}}                           // 添加文档6
{"product_name":"iPad Air","category":"平板","price":4799.00,"stock":600,"is_on_sale":true}
{"update":{"_index":"my_store","_id":"3"}}                          // 更新文档3
{"doc":{"price":3799.00}}                                           // 更新价格为3799
{"delete":{"_index":"my_store","_id":"5"}}                          // 删除文档5

6.4 搜索 API

json 复制代码
// ============================================================
// 1. 查询所有文档
// GET /索引名/_search
// ============================================================
GET /my_store/_search

// 带分页的查询
GET /my_store/_search
{
  "from": 0,                        // 起始位置(从0开始)
  "size": 10                        // 每页返回的文档数量
}

// 只返回指定字段
GET /my_store/_search
{
  "_source": ["product_name", "price"],   // 只返回商品名和价格
  "from": 0,
  "size": 10
}


// ============================================================
// 2. match 查询(全文搜索,会对查询词进行分词)
// ============================================================
GET /my_store/_search
{
  "query": {
    "match": {
      "product_name": "iPhone Pro"   // 会被分词为 "iPhone" 和 "Pro"
    }
  }
}


// ============================================================
// 3. match_all 查询(查询所有文档)
// ============================================================
GET /my_store/_search
{
  "query": {
    "match_all": {}                  // 匹配所有文档
  }
}


// ============================================================
// 4. term 查询(精确匹配,不对查询词分词)
// 适用于 keyword、integer、boolean 等类型
// ============================================================
GET /my_store/_search
{
  "query": {
    "term": {
      "category": "手机"              // 精确匹配 category 为 "手机"
    }
  }
}


// ============================================================
// 5. range 查询(范围查询)
// ============================================================
GET /my_store/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 5000,                // 大于等于 5000
        "lte": 10000                // 小于等于 10000
      }
    }
  }
}

// 日期范围查询
GET /my_store/_search
{
  "query": {
    "range": {
      "create_time": {
        "gte": "2024-01-01",        // 大于等于 2024-01-01
        "lte": "2024-12-31"         // 小于等于 2024-12-31
      }
    }
  }
}


// ============================================================
// 6. bool 组合查询(布尔查询)
// must:必须匹配(相当于 AND)
// should:至少匹配一个(相当于 OR)
// must_not:必须不匹配(相当于 NOT)
// filter:过滤(不计算相关性分数,可缓存)
// ============================================================
GET /my_store/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "category": "手机"       // 必须匹配:类别为手机
          }
        }
      ],
      "should": [
        {
          "term": {
            "tags": "5G"            // 应该匹配:标签包含5G
          }
        },
        {
          "term": {
            "tags": "旗舰"          // 应该匹配:标签包含旗舰
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "lt": 3000            // 不匹配:价格低于3000
            }
          }
        }
      ],
      "filter": [
        {
          "range": {
            "stock": {
              "gt": 0               // 过滤:库存大于0
            }
          }
        }
      ]
    }
  }
}


// ============================================================
// 7. fuzzy 模糊查询(容错查询)
// ============================================================
GET /my_store/_search
{
  "query": {
    "fuzzy": {
      "product_name": {
        "value": "iPhon",           // 查询词(可能有拼写错误)
        "fuzziness": 2              // 允许的最大编辑距离
      }
    }
  }
}


// ============================================================
// 8. wildcard 通配符查询
// * 匹配任意字符(包括空)
// ? 匹配单个字符
// ============================================================
GET /my_store/_search
{
  "query": {
    "wildcard": {
      "product_name": {
        "value": "*Pro*"            // 包含 "Pro" 的所有商品
      }
    }
  }
}


// ============================================================
// 9. prefix 前缀查询
// ============================================================
GET /my_store/_search
{
  "query": {
    "prefix": {
      "product_name": {
        "value": "Mac"              // 以 "Mac" 开头的商品
      }
    }
  }
}


// ============================================================
// 10. 排序
// ============================================================
GET /my_store/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "asc"              // 按价格升序排列
      }
    },
    {
      "stock": {
        "order": "desc"             // 按库存降序排列
      }
    }
  ]
}


// ============================================================
// 11. 高亮显示
// 搜索结果中匹配的关键词会被 <em> 标签包裹
// ============================================================
GET /my_store/_search
{
  "query": {
    "match": {
      "product_name": "iPhone"
    }
  },
  "highlight": {
    "pre_tags": ["<em class='highlight'>"],     // 高亮前缀标签
    "post_tags": ["</em>"],                      // 高亮后缀标签
    "fields": {
      "product_name": {}                         // 对 product_name 字段进行高亮
    }
  }
}

6.5 Query DSL

json 复制代码
// ============================================================
// Query DSL 是 Elasticsearch 最强大的查询语言
// ============================================================

// ============================================================
// 1. multi_match 多字段匹配查询
// 在多个字段中搜索同一个关键词
// ============================================================
GET /my_store/_search
{
  "query": {
    "multi_match": {
      "query": "苹果",                          // 搜索关键词
      "fields": ["product_name", "tags"]        // 在商品名和标签中搜索
    }
  }
}


// ============================================================
// 2. exists 查询(判断字段是否存在)
// ============================================================
GET /my_store/_search
{
  "query": {
    "exists": {
      "field": "tags"                           // 查询包含 tags 字段的文档
    }
  }
}


// ============================================================
// 3. ids 查询(根据文档ID查询)
// ============================================================
GET /my_store/_search
{
  "query": {
    "ids": {
      "values": ["1", "2", "3"]                 // 查询ID为1、2、3的文档
    }
  }
}


// ============================================================
// 4. 聚合查询(Aggregation)
// 类似于 SQL 的 GROUP BY + 聚合函数
// ============================================================

// 统计每个类别的商品数量
GET /my_store/_search
{
  "size": 0,                                     // 不返回文档,只返回聚合结果
  "aggs": {
    "category_count": {                          // 聚合名称(自定义)
      "terms": {
        "field": "category",                     // 按 category 字段分组
        "size": 10                               // 返回前10个分组
      }
    }
  }
}

// 计算价格的统计信息(最大值、最小值、平均值、总和、数量)
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "price_stats": {
      "stats": {
        "field": "price"                         // 对 price 字段进行统计
      }
    }
  }
}

// 单独计算平均值
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"                         // 计算平均价格
      }
    }
  }
}

// 计算总和
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "total_stock": {
      "sum": {
        "field": "stock"                         // 计算总库存
      }
    }
  }
}

// 计算最大值
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "max_price": {
      "max": {
        "field": "price"                         // 计算最高价格
      }
    }
  }
}

// 嵌套聚合:按类别分组,然后计算每个类别的平均价格
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category"                      // 先按类别分组
      },
      "aggs": {
        "avg_price_by_category": {
          "avg": {
            "field": "price"                     // 再计算每个类别的平均价格
          }
        },
        "max_price_by_category": {
          "max": {
            "field": "price"                     // 再计算每个类别的最高价格
          }
        }
      }
    }
  }
}


// ============================================================
// 5. 范围聚合(Range Aggregation)
// 按价格区间分组统计
// ============================================================
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 5000, "key": "低价" },          // 价格 < 5000
          { "from": 5000, "to": 10000, "key": "中价" },  // 5000 <= 价格 < 10000
          { "from": 10000, "key": "高价" }        // 价格 >= 10000
        ]
      }
    }
  }
}


// ============================================================
// 6. 直方图聚合(Histogram Aggregation)
// 按固定间隔统计
// ============================================================
GET /my_store/_search
{
  "size": 0,
  "aggs": {
    "price_histogram": {
      "histogram": {
        "field": "price",
        "interval": 3000                          // 每3000为一个区间
      }
    }
  }
}


// ============================================================
// 7. 复合查询示例:带条件的聚合
// 先过滤出手机类商品,再统计价格
// ============================================================
GET /my_store/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": {
        "term": {
          "category": "手机"                      // 只统计手机类
        }
      }
    }
  },
  "aggs": {
    "phone_price_stats": {
      "stats": {
        "field": "price"
      }
    }
  }
}


// ============================================================
// 8. suggester 查询建议(自动补全/纠错)
// ============================================================
GET /my_store/_search
{
  "suggest": {
    "product_suggest": {
      "text": "iPhonn",                           // 用户输入的(可能有拼写错误的)文本
      "term": {
        "field": "product_name",                  // 基于哪个字段给出建议
        "suggest_mode": "always"                  // 建议模式:always/missing/popular
      }
    }
  }
}

七、Head 插件安装

bash 复制代码
# ============================================================
# 方式一:使用 Chrome 浏览器插件(推荐,最简单)
# ============================================================
# 在 Chrome 应用商店搜索 "Elasticsearch Head" 安装即可


# ============================================================
# 方式二:使用 Node.js 运行 head 插件
# ============================================================

# 步骤1:安装 Node.js 和 npm
yum install -y nodejs npm

# 步骤2:下载 head 插件
git clone https://github.com/mobz/elasticsearch-head.git

# 步骤3:进入目录,安装依赖
cd elasticsearch-head
npm install

# 步骤4:启动 head 插件
npm run start
# 默认访问地址:http://localhost:9100

# 步骤5:在 Elasticsearch 配置中添加跨域支持
# 编辑 elasticsearch.yml,添加以下配置:
vim /opt/elasticsearch-7.17.9/config/elasticsearch.yml
yaml 复制代码
# ======================== 添加跨域配置 ========================
http.cors.enabled: true            # 启用跨域支持
http.cors.allow-origin: "*"        # 允许所有来源访问
http.cors.allow-headers: Authorization,X-Requested-With,Content-Type,Content-Length  # 允许的请求头
bash 复制代码
# 步骤6:重启 Elasticsearch
# 在 head 页面中输入 Elasticsearch 地址 http://192.168.1.101:9200 连接

八、Java API 操作:员工信息

8.1 Maven 依赖

xml 复制代码
<!-- pom.xml -->
<dependencies>
    <!-- Elasticsearch 高级客户端(7.17.x版本) -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.17.9</version>
    </dependency>

    <!-- Elasticsearch 核心包 -->
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.17.9</version>
    </dependency>

    <!-- JSON 处理:Jackson -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
    </dependency>

    <!-- 日志:slf4j + logback -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.4.5</version>
    </dependency>
</dependencies>

8.2 员工实体类

java 复制代码
package com.es.entity;

import java.io.Serializable;
import java.util.List;

/**
 * 员工实体类
 * 对应 Elasticsearch 中的文档结构
 */
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 员工ID */
    private String id;

    /** 员工姓名 */
    private String name;

    /** 员工年龄 */
    private Integer age;

    /** 员工性别 */
    private String gender;

    /** 所属部门 */
    private String department;

    /** 职位 */
    private String position;

    /** 薪资 */
    private Double salary;

    /** 入职日期 */
    private String hireDate;

    /** 技能列表 */
    private List<String> skills;

    /** 邮箱 */
    private String email;

    /** 手机号 */
    private String phone;

    // ==================== 构造方法 ====================

    /**
     * 无参构造方法(Jackson反序列化需要)
     */
    public Employee() {
    }

    /**
     * 全参构造方法
     */
    public Employee(String id, String name, Integer age, String gender,
                    String department, String position, Double salary,
                    String hireDate, List<String> skills, String email, String phone) {
        this.id = id;                // 设置员工ID
        this.name = name;            // 设置员工姓名
        this.age = age;              // 设置员工年龄
        this.gender = gender;        // 设置员工性别
        this.department = department;// 设置所属部门
        this.position = position;    // 设置职位
        this.salary = salary;        // 设置薪资
        this.hireDate = hireDate;    // 设置入职日期
        this.skills = skills;        // 设置技能列表
        this.email = email;          // 设置邮箱
        this.phone = phone;          // 设置手机号
    }

    // ==================== Getter 和 Setter 方法 ====================

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public String getHireDate() {
        return hireDate;
    }

    public void setHireDate(String hireDate) {
        this.hireDate = hireDate;
    }

    public List<String> getSkills() {
        return skills;
    }

    public void setSkills(List<String> skills) {
        this.skills = skills;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                ", department='" + department + '\'' +
                ", position='" + position + '\'' +
                ", salary=" + salary +
                ", hireDate='" + hireDate + '\'' +
                ", skills=" + skills +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

8.3 Elasticsearch 客户端工具类

java 复制代码
package com.es.util;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;

/**
 * Elasticsearch 客户端工具类
 * 使用单例模式管理 RestHighLevelClient 连接
 */
public class ESClientUtil {

    /** Elasticsearch 服务器地址 */
    private static final String HOST = "192.168.1.101";

    /** Elasticsearch HTTP 端口 */
    private static final int PORT = 9200;

    /** HTTP 协议 */
    private static final String SCHEME = "http";

    /** RestHighLevelClient 实例(volatile 保证多线程可见性) */
    private static volatile RestHighLevelClient client;

    /**
     * 私有构造方法,防止外部实例化
     */
    private ESClientUtil() {
    }

    /**
     * 获取 RestHighLevelClient 单例实例(双重检查锁定模式)
     *
     * @return RestHighLevelClient 实例
     */
    public static RestHighLevelClient getClient() {
        if (client == null) {                             // 第一次检查(避免不必要的同步)
            synchronized (ESClientUtil.class) {           // 同步锁
                if (client == null) {                     // 第二次检查(防止重复创建)
                    // 创建 HTTP 主机配置
                    HttpHost httpHost = new HttpHost(HOST, PORT, SCHEME);
                    // 创建 RestClient 构建器
                    RestClientBuilder builder = RestClient.builder(httpHost);
                    // 构建高级客户端实例
                    client = new RestHighLevelClient(builder);
                }
            }
        }
        return client;                                   // 返回客户端实例
    }

    /**
     * 关闭客户端连接
     * 在应用程序关闭时调用,释放资源
     */
    public static void closeClient() {
        if (client != null) {                            // 检查客户端是否为null
            try {
                client.close();                          // 关闭客户端连接
                client = null;                           // 将引用置为null,帮助GC回收
            } catch (Exception e) {
                e.printStackTrace();                     // 打印异常堆栈信息
            }
        }
    }
}

8.4 员工索引管理类

java 复制代码
package com.es.service;

import com.es.util.ESClientUtil;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;

import java.io.IOException;

/**
 * 员工索引管理类
 * 负责创建、查看、删除员工索引及映射
 */
public class EmployeeIndexService {

    /** 员工索引名称常量 */
    private static final String INDEX_NAME = "employee_index";

    /** RestHighLevelClient 客户端 */
    private final RestHighLevelClient client = ESClientUtil.getClient();

    /**
     * 创建员工索引(包含Mapping映射定义)
     * 使用 XContentBuilder 构建JSON格式的映射结构
     */
    public void createIndex() throws IOException {
        // 第一步:创建索引请求对象
        CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);

        // 第二步:设置索引的 Settings 配置
        request.settings(Settings.builder()
                .put("number_of_shards", 3)          // 设置主分片数量为3
                .put("number_of_replicas", 1)         // 设置副本数量为1
        );

        // 第三步:使用 XContentBuilder 构建映射(Mapping)
        XContentBuilder mapping = XContentFactory.jsonBuilder()
                .startObject()                         // 开始根对象 {
                    .startObject("properties")         // 开始 properties 对象

                        // ---- name 字段:员工姓名 ----
                        .startObject("name")
                            .field("type", "text")             // 文本类型,支持全文搜索
                            .field("analyzer", "ik_max_word")  // 使用IK分词器(最大切分)
                            .field("search_analyzer", "ik_smart") // 搜索时使用IK智能分词
                        .endObject()

                        // ---- age 字段:员工年龄 ----
                        .startObject("age")
                            .field("type", "integer")          // 整数类型
                        .endObject()

                        // ---- gender 字段:员工性别 ----
                        .startObject("gender")
                            .field("type", "keyword")          // 关键字类型,不分词
                        .endObject()

                        // ---- department 字段:所属部门 ----
                        .startObject("department")
                            .field("type", "keyword")          // 关键字类型
                        .endObject()

                        // ---- position 字段:职位 ----
                        .startObject("position")
                            .field("type", "text")             // 文本类型
                            .field("analyzer", "ik_max_word")  // IK分词器
                        .endObject()

                        // ---- salary 字段:薪资 ----
                        .startObject("salary")
                            .field("type", "double")           // 双精度浮点类型
                        .endObject()

                        // ---- hireDate 字段:入职日期 ----
                        .startObject("hireDate")
                            .field("type", "date")             // 日期类型
                            .field("format", "yyyy-MM-dd")     // 日期格式
                        .endObject()

                        // ---- skills 字段:技能列表 ----
                        .startObject("skills")
                            .field("type", "keyword")          // 数组类型,每个元素为keyword
                        .endObject()

                        // ---- email 字段:邮箱 ----
                        .startObject("email")
                            .field("type", "keyword")          // 关键字类型(精确匹配)
                        .endObject()

                        // ---- phone 字段:手机号 ----
                        .startObject("phone")
                            .field("type", "keyword")          // 关键字类型
                        .endObject()

                    .endObject()                       // 结束 properties 对象
                .endObject();                          // 结束根对象 }

        // 将映射设置到请求中
        request.mapping(mapping);

        // 第四步:执行创建索引请求
        CreateIndexResponse response = client.indices()
                .create(request, RequestOptions.DEFAULT); // 使用默认请求选项

        // 第五步:判断索引是否创建成功
        boolean acknowledged = response.isAcknowledged(); // 获取创建结果
        if (acknowledged) {
            System.out.println("索引 [" + INDEX_NAME + "] 创建成功!");
        } else {
            System.out.println("索引 [" + INDEX_NAME + "] 创建失败!");
        }
    }

    /**
     * 判断索引是否存在
     *
     * @return true-存在, false-不存在
     */
    public boolean indexExists() throws IOException {
        // 创建"索引是否存在"的检查请求
        GetIndexRequest request = new GetIndexRequest(INDEX_NAME);
        // 执行检查并返回结果
        return client.indices().exists(request, RequestOptions.DEFAULT);
    }

    /**
     * 获取索引信息
     *
     * @return GetIndexResponse 索引信息响应对象
     */
    public GetIndexResponse getIndex() throws IOException {
        // 创建获取索引信息的请求
        GetIndexRequest request = new GetIndexRequest(INDEX_NAME);
        // 执行请求,获取索引信息
        return client.indices().get(request, RequestOptions.DEFAULT);
    }

    /**
     * 修改索引的 Mapping 映射
     * 注意:已有的字段映射不能修改,只能新增字段
     */
    public void updateMapping() throws IOException {
        // 创建修改映射的请求
        PutMappingRequest request = new PutMappingRequest(INDEX_NAME);

        // 使用 XContentBuilder 构建新的映射定义
        XContentBuilder mapping = XContentFactory.jsonBuilder()
                .startObject()
                    .startObject("properties")

                        // 新增 address 字段(地址)
                        .startObject("address")
                            .field("type", "text")             // 文本类型
                            .field("analyzer", "ik_max_word")  // IK分词器
                        .endObject()

                    .endObject()
                .endObject();

        // 设置映射到请求中
        request.source(mapping);

        // 执行修改映射请求
        AcknowledgedResponse response = client.indices()
                .putMapping(request, RequestOptions.DEFAULT);

        // 判断是否修改成功
        System.out.println("映射更新是否成功:" + response.isAcknowledged());
    }

    /**
     * 删除索引
     * 谨慎操作!删除后数据不可恢复!
     */
    public void deleteIndex() throws IOException {
        // 创建删除索引请求
        DeleteIndexRequest request = new DeleteIndexRequest(INDEX_NAME);
        // 执行删除操作
        AcknowledgedResponse response = client.indices()
                .delete(request, RequestOptions.DEFAULT);
        // 判断是否删除成功
        System.out.println("索引删除是否成功:" + response.isAcknowledged());
    }
}

8.5 员工文档操作类

java 复制代码
package com.es.service;

import com.es.entity.Employee;
import com.es.util.ESClientUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Max;
import org.elasticsearch.search.aggregations.metrics.MaxAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;

import java.io.IOException;
import java.util.*;

/**
 * 员工文档操作类
 * 负责员工信息的增删改查操作
 */
public class EmployeeDocService {

    /** 员工索引名称 */
    private static final String INDEX_NAME = "employee_index";

    /** RestHighLevelClient 客户端 */
    private final RestHighLevelClient client = ESClientUtil.getClient();

    /** Jackson 的 ObjectMapper 用于 JSON 序列化和反序列化 */
    private final ObjectMapper objectMapper = new ObjectMapper();

    // ================================================================
    //                        增 (Create)
    // ================================================================

    /**
     * 添加单个员工文档(使用 XContentBuilder)
     *
     * @param employee 员工对象
     * @return 文档ID
     */
    public String addEmployee(Employee employee) throws IOException {
        // 创建索引请求,指定索引名称和文档ID
        IndexRequest request = new IndexRequest(INDEX_NAME);

        // 如果员工有ID,使用指定ID;否则自动生成
        if (employee.getId() != null) {
            request.id(employee.getId());            // 设置文档ID
        }

        // 使用 XContentBuilder 构建文档内容(JSON格式)
        XContentBuilder builder = XContentFactory.jsonBuilder()
                .startObject()                                       // 开始JSON对象
                    .field("name", employee.getName())               // 写入姓名
                    .field("age", employee.getAge())                 // 写入年龄
                    .field("gender", employee.getGender())           // 写入性别
                    .field("department", employee.getDepartment())   // 写入部门
                    .field("position", employee.getPosition())       // 写入职位
                    .field("salary", employee.getSalary())           // 写入薪资
                    .field("hireDate", employee.getHireDate())       // 写入入职日期
                    .field("skills", employee.getSkills())           // 写入技能列表(数组)
                    .field("email", employee.getEmail())             // 写入邮箱
                    .field("phone", employee.getPhone())             // 写入手机号
                .endObject();                                        // 结束JSON对象

        // 将文档内容设置到请求中
        request.source(builder);

        // 设置超时时间(1分钟)
        request.timeout(TimeValue.timeValueMinutes(1));

        // 执行索引操作
        IndexResponse response = client.index(request, RequestOptions.DEFAULT);

        // 输出创建结果
        System.out.println("添加员工文档成功,文档ID:" + response.getId());
        System.out.println("结果状态:" + response.status().name());

        return response.getId();                                   // 返回文档ID
    }

    /**
     * 添加单个员工文档(使用 JSON 字符串方式)
     *
     * @param employee 员工对象
     * @return 文档ID
     */
    public String addEmployeeByJson(Employee employee) throws IOException {
        // 创建索引请求
        IndexRequest request = new IndexRequest(INDEX_NAME);

        // 设置文档ID
        if (employee.getId() != null) {
            request.id(employee.getId());
        }

        // 使用 ObjectMapper 将员工对象转换为 JSON 字符串
        String jsonStr = objectMapper.writeValueAsString(employee);

        // 使用 JSON 字符串作为文档源
        request.source(jsonStr, org.elasticsearch.common.xcontent.XContentType.JSON);

        // 执行索引操作
        IndexResponse response = client.index(request, RequestOptions.DEFAULT);

        System.out.println("添加员工文档成功(JSON方式),文档ID:" + response.getId());
        return response.getId();
    }

    /**
     * 批量添加员工
     * 使用 BulkRequest 一次性添加多个文档,提高效率
     *
     * @param employees 员工列表
     */
    public void batchAddEmployees(List<Employee> employees) throws IOException {
        // 创建批量请求对象
        BulkRequest bulkRequest = new BulkRequest();

        // 遍历员工列表,将每个员工添加到批量请求中
        for (Employee emp : employees) {
            // 构建每个文档的索引请求
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME);

            // 设置文档ID
            if (emp.getId() != null) {
                indexRequest.id(emp.getId());
            }

            // 设置文档内容(使用 Map 转 JSON)
            Map<String, Object> sourceMap = new HashMap<>();
            sourceMap.put("name", emp.getName());            // 姓名
            sourceMap.put("age", emp.getAge());              // 年龄
            sourceMap.put("gender", emp.getGender());        // 性别
            sourceMap.put("department", emp.getDepartment());// 部门
            sourceMap.put("position", emp.getPosition());    // 职位
            sourceMap.put("salary", emp.getSalary());        // 薪资
            sourceMap.put("hireDate", emp.getHireDate());    // 入职日期
            sourceMap.put("skills", emp.getSkills());        // 技能列表
            sourceMap.put("email", emp.getEmail());          // 邮箱
            sourceMap.put("phone", emp.getPhone());          // 手机号

            // 使用 Map 作为文档源
            indexRequest.source(sourceMap);

            // 将索引请求添加到批量请求中
            bulkRequest.add(indexRequest);
        }

        // 执行批量操作
        BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);

        // 检查是否有失败的操作
        if (bulkResponse.hasFailures()) {
            System.out.println("批量添加存在失败项:" + bulkResponse.buildFailureMessage());
        } else {
            System.out.println("批量添加成功,共添加 " + employees.size() + " 条员工记录");
        }
    }

    // ================================================================
    //                        查 (Read)
    // ================================================================

    /**
     * 根据文档ID查询单个员工
     *
     * @param id 文档ID
     * @return 员工对象,不存在返回 null
     */
    public Employee getEmployeeById(String id) throws IOException {
        // 创建获取文档的请求
        GetRequest request = new GetRequest(INDEX_NAME, id);

        // 执行获取操作
        GetResponse response = client.get(request, RequestOptions.DEFAULT);

        // 判断文档是否存在
        if (response.isExists()) {
            // 获取文档的源数据(Map格式)
            Map<String, Object> sourceMap = response.getSourceAsMap();

            // 将 Map 转换为 Employee 对象
            Employee employee = new Employee();
            employee.setId(response.getId());                                    // 设置ID
            employee.setName((String) sourceMap.get("name"));                    // 设置姓名
            employee.setAge((Integer) sourceMap.get("age"));                     // 设置年龄
            employee.setGender((String) sourceMap.get("gender"));                // 设置性别
            employee.setDepartment((String) sourceMap.get("department"));        // 设置部门
            employee.setPosition((String) sourceMap.get("position"));            // 设置职位
            employee.setSalary((Double) sourceMap.get("salary"));                // 设置薪资
            employee.setHireDate((String) sourceMap.get("hireDate"));            // 设置入职日期
            employee.setSkills((List<String>) sourceMap.get("skills"));          // 设置技能列表
            employee.setEmail((String) sourceMap.get("email"));                  // 设置邮箱
            employee.setPhone((String) sourceMap.get("phone"));                  // 设置手机号

            System.out.println("查询成功,员工信息:" + employee);
            return employee;                                                     // 返回员工对象
        } else {
            System.out.println("文档ID为 " + id + " 的员工不存在");
            return null;                                                         // 文档不存在返回null
        }
    }

    /**
     * 判断文档是否存在
     *
     * @param id 文档ID
     * @return true-存在, false-不存在
     */
    public boolean employeeExists(String id) throws IOException {
        // 创建检查文档是否存在的请求
        GetRequest request = new GetRequest(INDEX_NAME, id);
        // 执行检查(使用 exists 方法比 get 更高效,只检查不返回文档内容)
        return client.exists(request, RequestOptions.DEFAULT);
    }

    // ================================================================
    //                        改 (Update)
    // ================================================================

    /**
     * 局部更新员工信息
     * 只更新传入的字段,未传入的字段保持不变
     *
     * @param id       文档ID
     * @param fieldMap 需要更新的字段和值
     */
    public void updateEmployee(String id, Map<String, Object> fieldMap) throws IOException {
        // 创建更新请求
        UpdateRequest request = new UpdateRequest(INDEX_NAME, id);

        // 使用 doc 方式进行局部更新
        request.doc(fieldMap);

        // 设置重试策略:如果文档正在被更新,最多重试3次
        request.retryOnConflict(3);

        // 执行更新操作
        UpdateResponse response = client.update(request, RequestOptions.DEFAULT);

        System.out.println("更新员工文档成功,文档ID:" + response.getId());
        System.out.println("更新后的版本号:" + response.getVersion());
    }

    /**
     * 使用脚本更新员工薪资
     * 例如:给员工涨薪
     *
     * @param id         文档ID
     * @param salaryDiff 薪资变化量(正数为涨薪,负数为降薪)
     */
    public void updateSalaryByScript(String id, double salaryDiff) throws IOException {
        // 创建更新请求
        UpdateRequest request = new UpdateRequest(INDEX_NAME, id);

        // 使用 Painless 脚本进行更新
        // ctx._source.salary 表示当前文档的 salary 字段
        Map<String, Object> params = new HashMap<>();
        params.put("diff", salaryDiff);              // 脚本参数:薪资变化量

        // 构建脚本:将 salary 字段的值加上 diff 参数
        org.elasticsearch.script.Script script = new org.elasticsearch.script.Script(
                org.elasticsearch.script.ScriptType.INLINE,          // 内联脚本类型
                "painless",                                           // 脚本语言:Painless
                "ctx._source.salary += params.diff",                  // 脚本内容
                params                                                // 脚本参数
        );
        request.script(script);

        // 执行更新
        UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
        System.out.println("脚本更新薪资成功,文档ID:" + response.getId());
    }

    // ================================================================
    //                        删 (Delete)
    // ================================================================

    /**
     * 根据文档ID删除员工
     *
     * @param id 文档ID
     */
    public void deleteEmployee(String id) throws IOException {
        // 创建删除请求
        DeleteRequest request = new DeleteRequest(INDEX_NAME, id);

        // 执行删除操作
        DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);

        System.out.println("删除员工文档成功,文档ID:" + response.getId());
        System.out.println("删除结果:" + response.status().name());
    }

    /**
     * 批量删除员工
     *
     * @param ids 需要删除的文档ID列表
     */
    public void batchDeleteEmployees(List<String> ids) throws IOException {
        // 创建批量请求对象
        BulkRequest bulkRequest = new BulkRequest();

        // 遍历ID列表,将每个删除请求添加到批量请求中
        for (String id : ids) {
            // 创建单个删除请求
            DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, id);
            // 添加到批量请求
            bulkRequest.add(deleteRequest);
        }

        // 执行批量删除
        BulkResponse response = client.bulk(bulkRequest, RequestOptions.DEFAULT);

        // 检查是否有失败项
        if (response.hasFailures()) {
            System.out.println("批量删除存在失败项:" + response.buildFailureMessage());
        } else {
            System.out.println("批量删除成功,共删除 " + ids.size() + " 条记录");
        }
    }

    // ================================================================
    //                      搜索 (Search)
    // ================================================================

    /**
     * 查询所有员工
     *
     * @return 员工列表
     */
    public List<Employee> searchAll() throws IOException {
        // 创建搜索请求,指定索引名称
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        // 创建搜索源构建器(构建查询条件)
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchAllQuery()); // 查询所有文档
        sourceBuilder.from(0);                               // 从第0条开始
        sourceBuilder.size(100);                             // 返回最多100条
        sourceBuilder.timeout(TimeValue.timeValueSeconds(10)); // 设置超时时间10秒

        // 将搜索源设置到搜索请求中
        searchRequest.source(sourceBuilder);

        // 执行搜索
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        // 解析搜索结果
        return parseSearchResponse(response);
    }

    /**
     * 根据姓名搜索员工(全文搜索)
     * 会对查询词进行分词匹配
     *
     * @param name 搜索关键词
     * @return 匹配的员工列表
     */
    public List<Employee> searchByName(String name) throws IOException {
        // 创建搜索请求
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        // 构建查询条件:match 查询(全文搜索)
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery("name", name)); // 在 name 字段中搜索
        sourceBuilder.from(0);
        sourceBuilder.size(10);

        searchRequest.source(sourceBuilder);

        // 执行搜索并返回结果
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        return parseSearchResponse(response);
    }

    /**
     * 精确查询:根据部门查找员工
     * term 查询不会对查询词进行分词,适用于 keyword 类型字段
     *
     * @param department 部门名称
     * @return 匹配的员工列表
     */
    public List<Employee> searchByDepartment(String department) throws IOException {
        // 创建搜索请求
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        // 构建查询条件:term 查询(精确匹配)
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.termQuery("department", department));

        searchRequest.source(sourceBuilder);

        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        return parseSearchResponse(response);
    }

    /**
     * 范围查询:根据薪资范围搜索员工
     *
     * @param minSalary 最低薪资
     * @param maxSalary 最高薪资
     * @return 匹配的员工列表
     */
    public List<Employee> searchBySalaryRange(double minSalary, double maxSalary) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        // 构建查询条件:range 查询(范围查询)
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(
                QueryBuilders.rangeQuery("salary")   // 在 salary 字段上进行范围查询
                        .gte(minSalary)              // 大于等于 minSalary
                        .lte(maxSalary)              // 小于等于 maxSalary
        );

        // 按薪资降序排列
        sourceBuilder.sort("salary", SortOrder.DESC);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        return parseSearchResponse(response);
    }

    /**
     * 组合查询(Bool Query)
     * 多条件组合搜索
     *
     * @param department 部门(可为null,null表示不限)
     * @param minAge     最小年龄(可为null)
     * @param maxAge     最大年龄(可为null)
     * @param keyword    搜索关键词(可为null,在姓名和职位中搜索)
     * @return 匹配的员工列表
     */
    public List<Employee> complexSearch(String department, Integer minAge,
                                         Integer maxAge, String keyword) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 创建布尔查询构建器
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 条件1:部门过滤(精确匹配)
        if (department != null && !department.isEmpty()) {
            boolQuery.must(QueryBuilders.termQuery("department", department));
        }

        // 条件2:年龄范围过滤
        if (minAge != null || maxAge != null) {
            RangeQueryBuilder ageRange = QueryBuilders.rangeQuery("age");
            if (minAge != null) {
                ageRange.gte(minAge);                // 大于等于最小年龄
            }
            if (maxAge != null) {
                ageRange.lte(maxAge);                // 小于等于最大年龄
            }
            boolQuery.filter(ageRange);              // 使用 filter(不计算评分,可缓存)
        }

        // 条件3:关键词搜索(在姓名和职位中搜索)
        if (keyword != null && !keyword.isEmpty()) {
            // 多字段匹配查询
            boolQuery.should(QueryBuilders.matchQuery("name", keyword));     // 在姓名中搜索
            boolQuery.should(QueryBuilders.matchQuery("position", keyword)); // 在职位中搜索
            // should 表示"或"关系,至少匹配一个
        }

        // 设置查询条件
        sourceBuilder.query(boolQuery);
        sourceBuilder.from(0);
        sourceBuilder.size(20);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        return parseSearchResponse(response);
    }

    /**
     * 带高亮的搜索
     * 搜索结果中匹配的关键词会添加高亮标签
     *
     * @param field 搜索字段
     * @param keyword 搜索关键词
     * @return 匹配的员工列表(name和position字段已高亮处理)
     */
    public List<Employee> searchWithHighlight(String field, String keyword) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 设置查询条件
        sourceBuilder.query(QueryBuilders.matchQuery(field, keyword));

        // 设置高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.preTags("<strong class='highlight'>");   // 高亮前缀标签
        highlightBuilder.postTags("</strong>");                   // 高亮后缀标签
        highlightBuilder.field("name");                           // 对 name 字段高亮
        highlightBuilder.field("position");                       // 对 position 字段高亮
        highlightBuilder.requireFieldMatch(false);                // 不要求字段匹配才高亮

        sourceBuilder.highlighter(highlightBuilder);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        // 解析带高亮的结果
        return parseSearchResponseWithHighlight(response);
    }

    /**
     * 分页查询员工
     *
     * @param pageNum  页码(从1开始)
     * @param pageSize 每页大小
     * @return 当前页的员工列表
     */
    public List<Employee> searchWithPagination(int pageNum, int pageSize) throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 查询所有文档
        sourceBuilder.query(QueryBuilders.matchAllQuery());

        // 计算分页起始位置:(页码 - 1) * 每页大小
        int from = (pageNum - 1) * pageSize;
        sourceBuilder.from(from);           // 设置起始位置
        sourceBuilder.size(pageSize);       // 设置每页大小

        // 按入职日期降序排列
        sourceBuilder.sort("hireDate", SortOrder.DESC);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        return parseSearchResponse(response);
    }

    // ================================================================
    //                     聚合查询 (Aggregation)
    // ================================================================

    /**
     * 统计每个部门的员工人数
     *
     * @return Map:部门名称 -> 人数
     */
    public Map<String, Long> countByDepartment() throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 设置 size 为 0:不返回文档,只返回聚合结果
        sourceBuilder.size(0);

        // 创建 Terms 聚合:按 department 字段分组
        TermsAggregationBuilder aggregation = AggregationBuilders
                .terms("department_count")           // 聚合名称(自定义命名)
                .field("department")                 // 聚合字段
                .size(50);                           // 返回前50个分组

        sourceBuilder.aggregation(aggregation);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        // 解析聚合结果
        Map<String, Long> result = new LinkedHashMap<>();
        // 获取名为 "department_count" 的 Terms 聚合结果
        Terms terms = response.getAggregations().get("department_count");
        // 遍历每个桶(bucket)
        for (Terms.Bucket bucket : terms.getBuckets()) {
            String deptName = bucket.getKeyAsString();   // 桶的key(部门名称)
            long count = bucket.getDocCount();           // 桶中的文档数量
            result.put(deptName, count);
            System.out.println("部门:" + deptName + ",人数:" + count);
        }

        return result;
    }

    /**
     * 计算各部门的平均薪资
     *
     * @return Map:部门名称 -> 平均薪资
     */
    public Map<String, Double> avgSalaryByDepartment() throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);

        // 创建嵌套聚合:
        // 第一层:按 department 分组
        // 第二层:计算每组的平均薪资
        TermsAggregationBuilder termsAgg = AggregationBuilders
                .terms("by_department")              // 第一层聚合名称
                .field("department")                  // 分组字段
                .size(50);

        // 嵌套聚合:计算平均薪资
        AvgAggregationBuilder avgAgg = AggregationBuilders
                .avg("avg_salary")                    // 第二层聚合名称
                .field("salary");                     // 聚合字段

        // 将平均薪资聚合嵌套到部门分组聚合中
        termsAgg.subAggregation(avgAgg);

        sourceBuilder.aggregation(termsAgg);

        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        // 解析嵌套聚合结果
        Map<String, Double> result = new LinkedHashMap<>();
        Terms terms = response.getAggregations().get("by_department");
        for (Terms.Bucket bucket : terms.getBuckets()) {
            String deptName = bucket.getKeyAsString();              // 部门名称
            Avg avg = bucket.getAggregations().get("avg_salary");   // 获取平均薪资聚合
            double avgSalary = avg.getValue();                      // 平均薪资值
            result.put(deptName, avgSalary);
            System.out.println("部门:" + deptName + ",平均薪资:" + String.format("%.2f", avgSalary));
        }

        return result;
    }

    /**
     * 查找各部门的最高薪资
     *
     * @return Map:部门名称 -> 最高薪资
     */
    public Map<String, Double> maxSalaryByDepartment() throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.size(0);

        // 第一层聚合:按部门分组
        TermsAggregationBuilder termsAgg = AggregationBuilders
                .terms("by_department")
                .field("department")
                .size(50);

        // 第二层聚合:计算最高薪资
        MaxAggregationBuilder maxAgg = AggregationBuilders
                .max("max_salary")
                .field("salary");

        // 嵌套聚合
        termsAgg.subAggregation(maxAgg);

        sourceBuilder.aggregation(termsAgg);
        searchRequest.source(sourceBuilder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

        // 解析结果
        Map<String, Double> result = new LinkedHashMap<>();
        Terms terms = response.getAggregations().get("by_department");
        for (Terms.Bucket bucket : terms.getBuckets()) {
            String deptName = bucket.getKeyAsString();
            Max max = bucket.getAggregations().get("max_salary");
            double maxSalary = max.getValue();
            result.put(deptName, maxSalary);
            System.out.println("部门:" + deptName + ",最高薪资:" + String.format("%.2f", maxSalary));
        }

        return result;
    }

    // ================================================================
    //                     结果解析工具方法
    // ================================================================

    /**
     * 解析搜索响应,将结果转换为 Employee 列表
     *
     * @param response 搜索响应对象
     * @return 员工列表
     */
    private List<Employee> parseSearchResponse(SearchResponse response) {
        List<Employee> employees = new ArrayList<>();

        // 获取搜索命中的所有文档
        SearchHits hits = response.getHits();
        // 输出总命中数
        System.out.println("搜索命中总数:" + hits.getTotalHits().value);

        // 遍历每个命中的文档
        for (SearchHit hit : hits) {
            // 获取文档的源数据(Map格式)
            Map<String, Object> sourceMap = hit.getSourceAsMap();

            // 将 Map 转换为 Employee 对象
            Employee employee = mapToEmployee(sourceMap);
            employee.setId(hit.getId());                     // 设置文档ID

            employees.add(employee);                         // 添加到列表中
        }

        return employees;
    }

    /**
     * 解析带高亮的搜索响应
     *
     * @param response 搜索响应对象
     * @return 员工列表(name和position字段已替换为高亮文本)
     */
    private List<Employee> parseSearchResponseWithHighlight(SearchResponse response) {
        List<Employee> employees = new ArrayList<>();
        SearchHits hits = response.getHits();

        for (SearchHit hit : hits) {
            Map<String, Object> sourceMap = hit.getSourceAsMap();

            // 获取高亮字段
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();

            // 如果 name 字段有高亮,使用高亮文本替换原文本
            HighlightField nameHighlight = highlightFields.get("name");
            if (nameHighlight != null) {
                String highlightedName = nameHighlight.getFragments()[0].string();
                sourceMap.put("name", highlightedName);      // 用高亮文本替换
            }

            // 如果 position 字段有高亮,使用高亮文本替换原文本
            HighlightField positionHighlight = highlightFields.get("position");
            if (positionHighlight != null) {
                String highlightedPosition = positionHighlight.getFragments()[0].string();
                sourceMap.put("position", highlightedPosition); // 用高亮文本替换
            }

            Employee employee = mapToEmployee(sourceMap);
            employee.setId(hit.getId());
            employees.add(employee);
        }

        return employees;
    }

    /**
     * 将 Map 转换为 Employee 对象
     *
     * @param sourceMap 文档源数据Map
     * @return Employee 对象
     */
    private Employee mapToEmployee(Map<String, Object> sourceMap) {
        Employee employee = new Employee();
        employee.setName((String) sourceMap.get("name"));
        employee.setAge((Integer) sourceMap.get("age"));
        employee.setGender((String) sourceMap.get("gender"));
        employee.setDepartment((String) sourceMap.get("department"));
        employee.setPosition((String) sourceMap.get("position"));
        employee.setSalary((Double) sourceMap.get("salary"));
        employee.setHireDate((String) sourceMap.get("hireDate"));
        employee.setSkills((List<String>) sourceMap.get("skills"));
        employee.setEmail((String) sourceMap.get("email"));
        employee.setPhone((String) sourceMap.get("phone"));
        return employee;
    }
}

8.6 主测试类

java 复制代码
package com.es;

import com.es.entity.Employee;
import com.es.service.EmployeeDocService;
import com.es.service.EmployeeIndexService;
import com.es.util.ESClientUtil;

import java.io.IOException;
import java.util.*;

/**
 * Elasticsearch 员工信息管理 主测试类
 * 演示完整的索引创建、文档增删改查、搜索和聚合操作
 */
public class EmployeeMainTest {

    public static void main(String[] args) {
        // 创建服务实例
        EmployeeIndexService indexService = new EmployeeIndexService();
        EmployeeDocService docService = new EmployeeDocService();

        try {
            // ========================================
            // 步骤1:创建员工索引
            // ========================================
            System.out.println("========== 步骤1:创建索引 ==========");
            // 判断索引是否已存在
            if (!indexService.indexExists()) {
                indexService.createIndex();              // 不存在则创建
            } else {
                System.out.println("索引已存在,跳过创建");
            }

            // ========================================
            // 步骤2:准备测试数据
            // ========================================
            System.out.println("\n========== 步骤2:批量添加员工 ==========");

            // 创建员工列表
            List<Employee> employees = new ArrayList<>();

            // 员工1
            employees.add(new Employee(
                    "1001",                          // ID
                    "张三",                           // 姓名
                    28,                               // 年龄
                    "男",                             // 性别
                    "技术部",                          // 部门
                    "Java高级开发工程师",               // 职位
                    25000.00,                         // 薪资
                    "2022-03-15",                     // 入职日期
                    Arrays.asList("Java", "Spring", "MySQL", "Redis"), // 技能列表
                    "zhangsan@company.com",           // 邮箱
                    "13800138001"                     // 手机号
            ));

            // 员工2
            employees.add(new Employee(
                    "1002",
                    "李四",
                    32,
                    "男",
                    "技术部",
                    "Python数据工程师",
                    28000.00,
                    "2021-06-20",
                    Arrays.asList("Python", "Spark", "Hadoop", "Flink"),
                    "lisi@company.com",
                    "13800138002"
            ));

            // 员工3
            employees.add(new Employee(
                    "1003",
                    "王五",
                    26,
                    "女",
                    "产品部",
                    "产品经理",
                    20000.00,
                    "2023-01-10",
                    Arrays.asList("Axure", "Sketch", "用户研究"),
                    "wangwu@company.com",
                    "13800138003"
            ));

            // 员工4
            employees.add(new Employee(
                    "1004",
                    "赵六",
                    35,
                    "男",
                    "技术部",
                    "前端开发工程师",
                    22000.00,
                    "2020-09-01",
                    Arrays.asList("JavaScript", "Vue", "React", "TypeScript"),
                    "zhaoliu@company.com",
                    "13800138004"
            ));

            // 员工5
            employees.add(new Employee(
                    "1005",
                    "孙七",
                    30,
                    "女",
                    "人力资源部",
                    "HR经理",
                    18000.00,
                    "2021-11-05",
                    Arrays.asList("招聘", "培训", "绩效管理"),
                    "sunqi@company.com",
                    "13800138005"
            ));

            // 员工6
            employees.add(new Employee(
                    "1006",
                    "周八",
                    29,
                    "男",
                    "产品部",
                    "UI设计师",
                    19000.00,
                    "2022-05-18",
                    Arrays.asList("Photoshop", "Figma", "Sketch", "Illustrator"),
                    "zhouba@company.com",
                    "13800138006"
            ));

            // 批量添加所有员工
            docService.batchAddEmployees(employees);

            // 等待 ES 刷新(近实时搜索特性,写入后约1秒可搜索)
            Thread.sleep(2000);

            // ========================================
            // 步骤3:根据ID查询单个员工
            // ========================================
            System.out.println("\n========== 步骤3:根据ID查询员工 ==========");
            Employee emp = docService.getEmployeeById("1001");
            if (emp != null) {
                System.out.println("查询到的员工:" + emp);
            }

            // ========================================
            // 步骤4:全文搜索(根据姓名)
            // ========================================
            System.out.println("\n========== 步骤4:全文搜索员工 ==========");
            List<Employee> searchResult = docService.searchByName("张三");
            System.out.println("搜索到 " + searchResult.size() + " 个结果:");
            for (Employee e : searchResult) {
                System.out.println("  - " + e.getName() + " | " + e.getPosition());
            }

            // ========================================
            // 步骤5:精确查询(根据部门)
            // ========================================
            System.out.println("\n========== 步骤5:精确查询(技术部) ==========");
            List<Employee> techEmployees = docService.searchByDepartment("技术部");
            System.out.println("技术部员工共 " + techEmployees.size() + " 人:");
            for (Employee e : techEmployees) {
                System.out.println("  - " + e.getName() + " | " + e.getPosition()
                        + " | 薪资:" + e.getSalary());
            }

            // ========================================
            // 步骤6:范围查询(薪资范围)
            // ========================================
            System.out.println("\n========== 步骤6:范围查询(薪资20000-30000) ==========");
            List<Employee> rangeResult = docService.searchBySalaryRange(20000, 30000);
            System.out.println("薪资在20000-30000的员工:");
            for (Employee e : rangeResult) {
                System.out.println("  - " + e.getName() + " | 薪资:" + e.getSalary());
            }

            // ========================================
            // 步骤7:复杂组合查询
            // ========================================
            System.out.println("\n========== 步骤7:复杂组合查询 ==========");
            System.out.println("条件:技术部 + 年龄25-35 + 关键词'开发'");
            List<Employee> complexResult = docService.complexSearch(
                    "技术部",    // 部门:技术部
                    25,          // 最小年龄:25
                    35,          // 最大年龄:35
                    "开发"       // 关键词:包含"开发"
            );
            System.out.println("组合查询结果:" + complexResult.size() + " 条");
            for (Employee e : complexResult) {
                System.out.println("  - " + e.getName() + " | " + e.getPosition());
            }

            // ========================================
            // 步骤8:带高亮的搜索
            // ========================================
            System.out.println("\n========== 步骤8:带高亮的搜索 ==========");
            List<Employee> highlightResult = docService.searchWithHighlight("position", "工程师");
            for (Employee e : highlightResult) {
                System.out.println("  高亮结果:" + e.getName() + " | " + e.getPosition());
            }

            // ========================================
            // 步骤9:分页查询
            // ========================================
            System.out.println("\n========== 步骤9:分页查询(第1页,每页3条) ==========");
            List<Employee> page1 = docService.searchWithPagination(1, 3);
            System.out.println("第1页结果:");
            for (Employee e : page1) {
                System.out.println("  - " + e.getName() + " | " + e.getHireDate());
            }

            // ========================================
            // 步骤10:更新员工信息
            // ========================================
            System.out.println("\n========== 步骤10:更新员工信息 ==========");

            // 10.1 局部更新:修改张三的薪资和职位
            Map<String, Object> updateFields = new HashMap<>();
            updateFields.put("salary", 30000.00);           // 更新薪资为30000
            updateFields.put("position", "技术主管");        // 更新职位为技术主管
            docService.updateEmployee("1001", updateFields);

            // 10.2 脚本更新:给李四涨薪 3000
            docService.updateSalaryByScript("1002", 3000.00);

            Thread.sleep(1000); // 等待更新生效

            // 验证更新结果
            System.out.println("更新后的张三:");
            docService.getEmployeeById("1001");
            System.out.println("涨薪后的李四:");
            docService.getEmployeeById("1002");

            // ========================================
            // 步骤11:聚合查询
            // ========================================
            System.out.println("\n========== 步骤11:聚合查询 ==========");

            // 11.1 统计每个部门的人数
            System.out.println("--- 各部门人数统计 ---");
            Map<String, Long> deptCount = docService.countByDepartment();

            // 11.2 各部门平均薪资
            System.out.println("\n--- 各部门平均薪资 ---");
            Map<String, Double> avgSalary = docService.avgSalaryByDepartment();

            // 11.3 各部门最高薪资
            System.out.println("\n--- 各部门最高薪资 ---");
            Map<String, Double> maxSalary = docService.maxSalaryByDepartment();

            // ========================================
            // 步骤12:删除操作
            // ========================================
            System.out.println("\n========== 步骤12:删除操作 ==========");

            // 删除单个员工
            docService.deleteEmployee("1006");

            // 批量删除
            docService.batchDeleteEmployees(Arrays.asList("1005", "1004"));

            Thread.sleep(1000);

            // 验证删除后的数据
            System.out.println("删除后的所有员工:");
            List<Employee> remaining = docService.searchAll();
            for (Employee e : remaining) {
                System.out.println("  - " + e.getId() + " | " + e.getName() + " | " + e.getDepartment());
            }

        } catch (IOException e) {
            System.err.println("Elasticsearch 操作异常:" + e.getMessage());
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.err.println("线程休眠中断:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // ========================================
            // 最后:关闭客户端连接,释放资源
            // ========================================
            System.out.println("\n========== 关闭客户端连接 ==========");
            ESClientUtil.closeClient();
            System.out.println("客户端连接已关闭,程序结束");
        }
    }
}

九、知识点总结

复制代码
┌────────────────────────────────────────────────────────────────┐
│                    Elasticsearch 知识体系总结                     │
├──────────────┬─────────────────────────────────────────────────┤
│ 核心概念      │ Index(索引)、Document(文档)、Field(字段)      │
│              │ Shard(分片)、Replica(副本)、Routing(路由)     │
├──────────────┼─────────────────────────────────────────────────┤
│ 集群架构      │ Master Node(主节点)、Data Node(数据节点)       │
│              │ Coordinating Node(协调节点)                     │
├──────────────┼─────────────────────────────────────────────────┤
│ REST API     │ 集群API:/_cluster/health, /_cat/nodes           │
│              │ 索引API:PUT /index, DELETE /index                │
│              │ 文档API:POST/GET/PUT/DELETE /index/_doc/id      │
│              │ 搜索API:GET /index/_search                      │
├──────────────┼─────────────────────────────────────────────────┤
│ Query DSL    │ match:全文搜索(分词)                            │
│              │ term:精确匹配(不分词)                            │
│              │ range:范围查询                                   │
│              │ bool:组合查询(must/should/must_not/filter)     │
│              │ fuzzy:模糊查询                                   │
│              │ wildcard:通配符查询                               │
│              │ aggregation:聚合查询                             │
├──────────────┼─────────────────────────────────────────────────┤
│ Java API     │ RestHighLevelClient:高级REST客户端               │
│              │ IndexRequest:文档创建                            │
│              │ GetRequest:文档查询                              │
│              │ UpdateRequest:文档更新                           │
│              │ DeleteRequest:文档删除                           │
│              │ SearchRequest:搜索查询                           │
│              │ BulkRequest:批量操作                             │
├──────────────┼─────────────────────────────────────────────────┤
│ 工具         │ Kibana:可视化管理界面                             │
│              │ Head 插件:集群状态可视化                          │
└──────────────┴─────────────────────────────────────────────────┘