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 插件:集群状态可视化 │
└──────────────┴─────────────────────────────────────────────────┘