Elasticsearch 常用操作语法
Elasticsearch 常用操作语法速查手册
一、Elasticsearch 与 MySQL 概念对比
理解 Elasticsearch 和 MySQL 之间的对应关系,是快速上手 ES 的关键。
1.1 核心概念对照表
| MySQL 概念 | Elasticsearch 概念 | 说明 |
|---|---|---|
| Database(数据库) | Index(索引) | 一个索引类似于一个数据库,存放一类相关的数据 |
| Table(表) | Type(类型,ES7+ 已废弃) | ES7 之后一个索引只有一个默认类型 _doc,可以理解为"表"的概念被弱化了 |
| Row(行) | Document(文档) | ES 中的一条数据就是一篇文档,以 JSON 格式存储 |
| Column(列) | Field(字段) | 文档中的每个字段对应 MySQL 表中的一列 |
| Schema(表结构) | Mapping(映射) | Mapping 定义了索引中每个字段的数据类型和分析方式 |
| SQL | DSL (Domain Specific Language) | ES 使用 JSON 格式的 DSL 进行查询 |
| SELECT | GET / _search | 查询操作 |
| INSERT | PUT / POST | 新增操作 |
| UPDATE | POST / _update | 更新操作 |
| DELETE | DELETE | 删除操作 |
1.2 本质区别速览
| 对比维度 | MySQL | Elasticsearch |
|---|---|---|
| 数据模型 | 关系型,表与表之间通过外键关联 | 文档型,数据以 JSON 文档形式存储 |
| 事务支持 | 完整的 ACID 事务支持 | 不支持事务(单文档级别的原子操作除外) |
| 查询语言 | SQL | DSL(基于 JSON 的查询语言) |
| 擅长场景 | 复杂的多表关联查询、事务性操作 | 全文搜索、日志分析、近实时检索 |
| 索引方式 | B+ Tree 索引 | 倒排索引(Inverted Index) |
| 扩展方式 | 通常纵向扩展(加硬件) | 天然支持横向扩展(加节点) |
| 数据实时性 | 实时 | 近实时(默认 1 秒刷新间隔) |
1.3 查询语句对照示例
以一个"用户表"为例,对比 MySQL 和 ES 的写法:
场景:查询 age 大于 25 且 name 包含"张"的用户
MySQL 写法:
SELECT * FROM users WHERE age > 25 AND name LIKE '%张%';
Elasticsearch 写法:
GET /users/_search
{
"query": {
"bool": {
"must": [
{
"range": {
"age": {
"gt": 25
}
}
},
{
"match": {
"name": "张"
}
}
]
}
}
}
1.4 增删改查对照
新增数据
MySQL:
INSERT INTO users (name, age, email) VALUES ('张三', 28, 'zhangsan@example.com');
Elasticsearch:
POST /users/_doc
{
"name": "张三",
"age": 28,
"email": "zhangsan@example.com"
}
查询单条数据
MySQL:
SELECT * FROM users WHERE id = 1;
Elasticsearch:
GET /users/_doc/1
更新数据
MySQL:
UPDATE users SET age = 30 WHERE id = 1;
Elasticsearch:
POST /users/_update/1
{
"doc": {
"age": 30
}
}
删除数据
MySQL:
DELETE FROM users WHERE id = 1;
Elasticsearch:
DELETE /users/_doc/1
1.5 聚合查询对照
场景:按 age 分组,统计每组人数
MySQL:
SELECT age, COUNT(*) AS count FROM users GROUP BY age;
Elasticsearch:
GET /users/_search
{
"size": 0,
"aggs": {
"age_group": {
"terms": {
"field": "age",
"size": 10
}
}
}
}
二、Elasticsearch 常用操作语法详解
2.1 索引管理
2.1.1 创建索引
创建一个名为 products 的索引,设置 3 个分片和 1 个副本:
PUT /products
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "float"
},
"category": {
"type": "keyword"
},
"description": {
"type": "text"
},
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
说明:
number_of_shards:主分片数量,创建后不可修改number_of_replicas:副本数量,可以随时调整text类型:会被分词,适合全文搜索keyword类型:不分词,适合精确匹配、聚合、排序
2.1.2 查看索引信息
GET /products
2.1.3 查看索引的 mapping 结构
GET /products/_mapping
2.1.4 修改索引设置
只能修改部分设置(比如副本数),分片数不可修改:
PUT /products/_settings
{
"number_of_replicas": 2
}
2.1.5 删除索引
DELETE /products
2.1.6 判断索引是否存在
HEAD /products
返回 200 表示存在,404 表示不存在。
2.1.7 打开/关闭索引
关闭索引后无法读写,但可以恢复:
POST /products/_close
POST /products/_open
2.1.8 重建索引(Reindex)
当 mapping 不满足需求时,需要创建新索引并迁移数据:
POST /_reindex
{
"source": {
"index": "products"
},
"dest": {
"index": "products_v2"
}
}
2.2 文档操作(CRUD)
2.2.1 新增文档
指定 ID 新增:
PUT /products/_doc/1
{
"name": "iPhone 15",
"price": 7999.00,
"category": "手机",
"description": "苹果最新款智能手机,配备A17芯片",
"created_at": "2024-01-15 10:30:00"
}
自动生成 ID 新增:
POST /products/_doc
{
"name": "华为 Mate 60",
"price": 6999.00,
"category": "手机",
"description": "华为旗舰手机,支持卫星通信",
"created_at": "2024-02-20 14:00:00"
}
2.2.2 批量新增
使用 _bulk API 一次性添加多条数据:
POST /_bulk
{"index": {"_index": "products", "_id": "3"}}
{"name": "MacBook Pro", "price": 14999.00, "category": "电脑", "description": "苹果笔记本电脑,搭载M3芯片"}
{"index": {"_index": "products", "_id": "4"}}
{"name": "联想 ThinkPad", "price": 8999.00, "category": "电脑", "description": "经典商务笔记本电脑"}
{"index": {"_index": "products", "_id": "5"}}
{"name": "AirPods Pro", "price": 1899.00, "category": "耳机", "description": "苹果无线降噪耳机"}
2.2.3 查询单个文档
GET /products/_doc/1
返回结果示例:
{
"_index": "products",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"name": "iPhone 15",
"price": 7999.00,
"category": "手机",
"description": "苹果最新款智能手机",
"created_at": "2024-01-15 10:30:00"
}
}
2.2.4 查询部分字段(类似 SELECT name, price)
使用 _source 过滤返回的字段:
GET /products/_doc/1
{
"_source": ["name", "price"]
}
2.2.5 更新文档
全量替换: 使用 PUT 方法,需要传入完整的文档数据:
PUT /products/_doc/1
{
"name": "iPhone 15 Pro",
"price": 8999.00,
"category": "手机",
"description": "苹果专业版智能手机",
"created_at": "2024-01-15 10:30:00"
}
局部更新: 只修改需要变更的字段:
POST /products/_update/1
{
"doc": {
"price": 7499.00
}
}
2.2.6 条件更新(脚本更新)
使用 painless 脚本进行运算:
POST /products/_update/1
{
"script": {
"source": "ctx._source.price -= params.discount",
"params": {
"discount": 500
}
}
}
2.2.7 删除文档
DELETE /products/_doc/1
2.2.8 判断文档是否存在
HEAD /products/_doc/1
返回 200 表示存在,404 表示不存在。
2.3 查询操作
2.3.1 查询所有文档(match_all)
相当于 MySQL 的 SELECT * FROM products:
GET /products/_search
{
"query": {
"match_all": {}
}
}
2.3.2 全文搜索(match)
搜索 description 中包含"苹果"的文档:
GET /products/_search
{
"query": {
"match": {
"description": "苹果手机"
}
}
}
match 查询会先对搜索词进行分词,再匹配。"苹果手机"会被分成"苹果"和"手机"两个词,任一命中即可。
2.3.3 短语搜索(match_phrase)
要求搜索词作为一个整体出现,顺序也要一致:
GET /products/_search
{
"query": {
"match_phrase": {
"description": "苹果手机"
}
}
}
只有 description 中连续出现"苹果手机"这个短语的文档才会被匹配。
2.3.4 精确搜索(term)
用于 keyword、数值、日期等不分词的字段:
GET /products/_search
{
"query": {
"term": {
"category": "手机"
}
}
}
注意: term 查询不会对搜索词进行分词,所以 text 类型的字段使用 term 搜索可能搜不到结果。如果要在 text 字段上用 term,需要搜索分词后的词项。
2.3.5 范围查询(range)
查询价格在 5000 到 10000 之间的商品:
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 5000,
"lte": 10000
}
}
}
}
范围操作符说明:
gt:大于(greater than)gte:大于等于(greater than or equal)lt:小于(less than)lte:小于等于(less than or equal)
2.3.6 多字段查询(multi_match)
在多个字段中搜索同一个关键词:
GET /products/_search
{
"query": {
"multi_match": {
"query": "苹果",
"fields": ["name", "description"]
}
}
}
相当于 MySQL 的 WHERE name LIKE '%苹果%' OR description LIKE '%苹果%'。
2.3.7 布尔组合查询(bool)
bool 查询是 ES 中最灵活的查询方式,可以组合多个查询条件。
子句类型:
| 子句 | 说明 | 等价于 MySQL |
|---|---|---|
must |
必须满足,影响评分 | AND |
should |
满足一个或多个,影响评分 | OR |
must_not |
必须不满足,不影响评分 | NOT / AND NOT |
filter |
必须满足,不影响评分,有缓存 | AND(性能更好) |
示例: 查询价格大于 5000 且类别为"手机"的商品,但排除价格低于 6000 的:
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"category": "手机"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 5000
}
}
}
],
"must_not": [
{
"range": {
"price": {
"lt": 6000
}
}
}
]
}
}
}
建议: 不需要评分的精确过滤条件放在 filter 中,性能更好,因为 filter 的结果可以被缓存。
2.3.8 分页查询(from + size)
GET /products/_search
{
"from": 0,
"size": 10,
"query": {
"match_all": {}
}
}
from:从第几条开始(偏移量),相当于 MySQL 的OFFSETsize:返回多少条,相当于 MySQL 的LIMIT
注意: from + size 不能超过 10000(默认限制),深度分页建议使用 search_after。
2.3.9 排序(sort)
按价格从高到低排序:
GET /products/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
按多个字段排序:
GET /products/_search
{
"query": {
"match_all": {}
},
"sort": [
{"price": {"order": "desc"}},
{"created_at": {"order": "asc"}}
]
}
2.3.10 高亮显示(highlight)
搜索并高亮显示匹配的关键词:
GET /products/_search
{
"query": {
"match": {
"description": "苹果"
}
},
"highlight": {
"fields": {
"description": {}
},
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
返回结果中匹配的词会被 <em> 标签包裹。
2.3.11 返回指定字段(_source 过滤)
只返回 name 和 price 字段:
GET /products/_search
{
"_source": ["name", "price"],
"query": {
"match_all": {}
}
}
不返回任何原文,只要搜索结果的 id 和评分:
GET /products/_search
{
"_source": false,
"query": {
"match_all": {}
}
}
2.3.12 前缀查询(prefix)
搜索 name 以"华为"开头的文档:
GET /products/_search
{
"query": {
"prefix": {
"name": "华为"
}
}
}
2.3.13 通配符查询(wildcard)
* 匹配任意字符,? 匹配单个字符:
GET /products/_search
{
"query": {
"wildcard": {
"name": "*Pro*"
}
}
}
注意: 通配符查询性能较差,不建议在大数据量场景使用。
2.3.14 正则查询(regexp)
GET /products/_search
{
"query": {
"regexp": {
"name": "iP(hone|ad).*"
}
}
}
2.3.15 模糊查询(fuzzy)
用于搜索时的拼写纠错,允许一定的编辑距离:
GET /products/_search
{
"query": {
"fuzzy": {
"name": {
"value": "iPhon",
"fuzziness": 2
}
}
}
}
fuzziness 表示允许的最大编辑距离(插入、删除、替换字符的次数)。
2.3.16 exists 查询
查找某个字段存在的文档(类似 MySQL 的 IS NOT NULL):
GET /products/_search
{
"query": {
"exists": {
"field": "description"
}
}
}
2.4 聚合分析
聚合(Aggregation)是 ES 的一大特色功能,类似于 MySQL 的 GROUP BY 和聚合函数。
2.4.1 指标聚合
统计总数量:
GET /products/_search
{
"size": 0,
"aggs": {
"total_count": {
"value_count": {
"field": "price"
}
}
}
}
计算平均值:
GET /products/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
计算最大值和最小值:
GET /products/_search
{
"size": 0,
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
}
}
}
求和:
GET /products/_search
{
"size": 0,
"aggs": {
"total_price": {
"sum": {
"field": "price"
}
}
}
}
一次性获取多个统计指标(stats):
GET /products/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": {
"field": "price"
}
}
}
}
返回结果包含 count、min、max、avg、sum 五个指标。
2.4.2 分桶聚合(terms)
按类别分组,统计每个类别的商品数量(类似 GROUP BY):
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10
}
}
}
}
2.4.3 范围分桶(range)
按价格区间分组:
GET /products/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{"to": 2000},
{"from": 2000, "to": 5000},
{"from": 5000, "to": 10000},
{"from": 10000}
]
}
}
}
}
2.4.4 嵌套聚合
按类别分组后,再统计每个类别的平均价格:
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
2.4.5 过滤后聚合
先过滤出"手机"类商品,再计算平均价格:
GET /products/_search
{
"size": 0,
"query": {
"term": {
"category": "手机"
}
},
"aggs": {
"avg_phone_price": {
"avg": {
"field": "price"
}
}
}
}
2.5 批量操作
2.5.1 批量读取(_mget)
一次请求获取多个文档:
GET /_mget
{
"docs": [
{"_index": "products", "_id": "1"},
{"_index": "products", "_id": "2"},
{"_index": "products", "_id": "3"}
]
}
也可以在同一个索引中简化写法:
GET /products/_mget
{
"ids": ["1", "2", "3"]
}
2.5.2 批量操作(_bulk)
支持在一个请求中执行多种操作(增、删、改):
POST /_bulk
{"index": {"_index": "products", "_id": "6"}}
{"name": "小米14", "price": 3999.00, "category": "手机", "description": "小米旗舰手机"}
{"update": {"_index": "products", "_id": "2"}}
{"doc": {"price": 6499.00}}
{"delete": {"_index": "products", "_id": "5"}}
2.6 Mapping 映射管理
2.6.1 查看 mapping
GET /products/_mapping
2.6.2 新增字段
向已有索引添加新字段(不能修改已有字段的类型):
PUT /products/_mapping
{
"properties": {
"brand": {
"type": "keyword"
},
"stock": {
"type": "integer"
}
}
}
2.6.3 常见字段类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| text | 全文检索,会被分词 | 文章标题、商品描述 |
| keyword | 精确匹配,不分词 | 状态码、标签、枚举值 |
| integer / long | 整数 | 数量、年龄 |
| float / double | 浮点数 | 价格、评分 |
| boolean | 布尔值 | 是否有效、是否删除 |
| date | 日期 | 创建时间、更新时间 |
| object | 对象(嵌套 JSON) | 地址信息等 |
| nested | 嵌套数组对象 | 评论列表等需要独立查询的数组 |
| ip | IP 地址 | 用户 IP |
| geo_point | 地理坐标 | 经纬度 |
2.7 查看集群和节点信息
2.7.1 查看集群健康状态
GET /_cluster/health
返回 green(健康)、yellow(副本不足)、red(主分片异常)。
2.7.2 查看节点信息
GET /_cat/nodes?v
2.7.3 查看所有索引
GET /_cat/indices?v
2.7.4 查看分片信息
GET /_cat/shards?v
2.7.5 查看磁盘使用情况
GET /_cat/allocation?v
2.8 别名管理(Alias)
别名类似于给索引取一个"外号",可以零停机切换索引。
2.8.1 创建别名
POST /_aliases
{
"actions": [
{
"add": {
"index": "products_v2",
"alias": "products"
}
}
]
}
2.8.2 切换别名(原子操作)
将别名从旧索引指向新索引:
POST /_aliases
{
"actions": [
{
"remove": {
"index": "products_v1",
"alias": "products"
}
},
{
"add": {
"index": "products_v2",
"alias": "products"
}
}
]
}
2.8.3 带过滤条件的别名
只索引某个类别的数据:
POST /_aliases
{
"actions": [
{
"add": {
"index": "products",
"alias": "phone_products",
"filter": {
"term": {
"category": "手机"
}
}
}
}
]
}
2.9 索引模板
当有大量格式相同的索引时(如按日期创建的日志索引),可以使用模板自动应用配置。
2.9.1 创建索引模板
以 logstash 日志索引为例:
PUT /_index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"timestamp": {
"type": "date"
},
"level": {
"type": "keyword"
},
"message": {
"type": "text"
},
"service": {
"type": "keyword"
}
}
}
}
}
此后任何以 logs- 开头的索引都会自动应用这个模板。
2.9.2 查看索引模板
GET /_index_template/logs_template
2.9.3 删除索引模板
DELETE /_index_template/logs_template
2.10 滚动查询(深分页方案)
当需要遍历大量数据时,使用 search_after 避免深度分页的性能问题。
GET /products/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"price": "desc"},
{"_id": "asc"}
]
}
取最后一条的 sort 值作为下一次查询的起点:
GET /products/_search
{
"size": 5,
"query": {
"match_all": {}
},
"search_after": [8999.0, "2"],
"sort": [
{"price": "desc"},
{"_id": "asc"}
]
}
Scroll API(全量导出场景)
GET /products/_search?scroll=1m
{
"size": 100,
"query": {
"match_all": {}
}
}
后续翻页:
POST /_search/scroll
{
"scroll": "1m",
"scroll_id": "上一次返回的 scroll_id"
}
用完后清除 scroll 上下文:
DELETE /_search/scroll
{
"scroll_id": "scroll_id值"
}
三、ES 与 MySQL 语法速查对照表
| 操作 | MySQL | Elasticsearch |
|---|---|---|
| 查询全部 | SELECT * FROM table |
GET /index/_search {"query":{"match_all":{}}} |
| 等值查询 | WHERE field = 'value' |
{"term":{"field":"value"}} |
| 模糊查询 | WHERE field LIKE '%value%' |
{"match":{"field":"value"}} |
| 范围查询 | WHERE field BETWEEN a AND b |
{"range":{"field":{"gte":a,"lte":b}}} |
| 多条件 AND | WHERE a=1 AND b=2 |
{"bool":{"must":[{"term":{"a":1}},{"term":{"b":2}}]}} |
| 多条件 OR | WHERE a=1 OR b=2 |
{"bool":{"should":[{"term":{"a":1}},{"term":{"b":2}}]}} |
| 非条件 | WHERE NOT a=1 |
{"bool":{"must_not":[{"term":{"a":1}}]}} |
| IS NULL | WHERE field IS NULL |
{"bool":{"must_not":[{"exists":{"field":"field"}}]}} |
| IS NOT NULL | WHERE field IS NOT NULL |
{"exists":{"field":"field"}} |
| 分页 | LIMIT 10 OFFSET 20 |
"from":20,"size":10 |
| 排序 | ORDER BY field DESC |
"sort":[{"field":{"order":"desc"}}] |
| 聚合统计 | COUNT / AVG / SUM / MAX / MIN |
对应 value_count / avg / sum / max / min |
| 分组统计 | GROUP BY field |
{"terms":{"field":"field"}} |
| 取部分字段 | SELECT name, price |
"_source":["name","price"] |
| 去重 | SELECT DISTINCT field |
使用 terms 聚合 |
| 更新 | UPDATE SET field=value |
POST /_update/id {"doc":{"field":"value"}} |
| 删除 | DELETE FROM table WHERE id=1 |
DELETE /index/_doc/1 |
四、实际业务场景示例
4.1 电商商品搜索
搜索商品名称或描述中包含"蓝牙耳机",价格在 100-500 之间,按销量降序排列,返回前 20 条:
GET /products/_search
{
"from": 0,
"size": 20,
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "蓝牙耳机",
"fields": ["name^3", "description"]
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 100,
"lte": 500
}
}
}
]
}
},
"sort": [
{"sales": {"order": "desc"}}
],
"highlight": {
"fields": {
"name": {},
"description": {}
}
}
}
说明:name^3 表示 name 字段的权重是 description 的 3 倍,搜索结果中 name 匹配的评分更高。
4.2 日志分析
查询最近 1 小时内 error 级别的日志,按服务名分组统计数量:
GET /logs-*/_search
{
"size": 0,
"query": {
"bool": {
"must": [
{
"term": {
"level": "error"
}
},
{
"range": {
"timestamp": {
"gte": "now-1h"
}
}
}
]
}
},
"aggs": {
"by_service": {
"terms": {
"field": "service",
"size": 20
}
}
}
}
4.3 用户画像查询
查询年龄在 25-35 之间,兴趣标签包含"摄影"或"旅行"的用户:
GET /users/_search
{
"query": {
"bool": {
"filter": [
{
"range": {
"age": {
"gte": 25,
"lte": 35
}
}
}
],
"should": [
{
"term": {
"tags": "摄影"
}
},
{
"term": {
"tags": "旅行"
}
}
],
"minimum_should_match": 1
}
}
}
minimum_should_match: 1 表示 should 子句中至少满足一个条件。
五、常见注意事项
-
text 和 keyword 的区别:text 类型会被分词,适合全文搜索;keyword 类型不分词,适合精确匹配和聚合。同一个字段可以同时设置两种类型(multi-fields)。
-
term 和 match 的区别:term 不分词直接匹配,match 会先对搜索词分词再匹配。text 字段应该用 match,keyword 字段应该用 term。
-
filter 和 must 的区别:filter 的结果会被缓存且不参与评分,性能更好。不需要评分的条件应尽量放在 filter 中。
-
深分页问题:from + size 方式在深度分页时性能急剧下降,超过 10000 条建议使用 search_after 或 scroll API。
-
mapping 不可修改:已有的字段类型创建后不能直接修改,需要通过 reindex 重建索引来变更。
-
near real-time(近实时):文档写入后默认 1 秒才能被搜索到(由 refresh_interval 控制),如果需要写入后立即可查,可以在写入请求中加
?refresh=true(会影响性能)。 -
字段类型选择:不需要搜索的字段设置
"index": false,不需要返回的字段设置"enabled": false,可以节省存储空间和提升性能。
六、在 Java 中使用 Elasticsearch
本章介绍如何在 Java 项目中操作 Elasticsearch。主要使用官方推荐的 Elasticsearch Java API Client(ES 7.15+ 推荐,ES 8.x 默认客户端)。
6.1 客户端选择
| 客户端 | 状态 | 说明 |
|---|---|---|
| Elasticsearch Java API Client | 推荐 | ES 官方最新客户端,功能完整,类型安全 |
| RestHighLevel Client (HLRC) | 已废弃 | ES 7.x 时代的主力客户端,ES 8.x 已标记为废弃 |
| Spring Data Elasticsearch | 可选 | Spring 生态封装,适合 Spring Boot 项目 |
| Jest | 不推荐 | 第三方客户端,已停止维护 |
6.2 Maven 依赖配置
使用 Elasticsearch Java API Client 需要添加以下依赖:
<dependencies>
<!-- Elasticsearch Java API Client -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.11.0</version>
</dependency>
<!-- JSON 处理(Jackson) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Jakarta JSON Processing(必需) -->
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.1.2</version>
</dependency>
</dependencies>
6.3 创建客户端连接
6.3.1 基础连接(无认证)
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
public class EsClient {
public static ElasticsearchClient createClient() {
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)
).build();
RestClientTransport transport = new RestClientTransport(
restClient, new JacksonJsonpTransport()
);
return new ElasticsearchClient(transport);
}
}
6.3.2 带认证的连接
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
public class EsClient {
public static ElasticsearchClient createClientWithAuth() {
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials("elastic", "your_password"));
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200, "http")
).setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
).build();
RestClientTransport transport = new RestClientTransport(
restClient, new JacksonJsonpTransport()
);
return new ElasticsearchClient(transport);
}
}
6.3.3 连接多个节点(集群)
RestClient restClient = RestClient.builder(
new HttpHost("node1", 9200),
new HttpHost("node2", 9200),
new HttpHost("node3", 9200)
).build();
6.4 定义实体类
创建一个与索引文档对应的 Java 实体类:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Product {
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("price")
private Double price;
@JsonProperty("category")
private String category;
@JsonProperty("description")
private String description;
@JsonProperty("tags")
private List<String> tags;
public Product() {}
public Product(String name, Double price, String category, String description) {
this.name = name;
this.price = price;
this.category = category;
this.description = description;
}
// 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 Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
}
6.5 索引管理操作
6.5.1 创建索引
public void createIndex(ElasticsearchClient client) throws IOException {
CreateIndexResponse response = client.indices().create(c -> c
.index("products")
.settings(s -> s
.numberOfShards("3")
.numberOfReplicas("1")
)
.mappings(m -> m
.properties("name", p -> p
.text(t -> t.analyzer("ik_max_word"))
)
.properties("price", p -> p
.float_(f -> f)
)
.properties("category", p -> p
.keyword(k -> k)
)
.properties("description", p -> p
.text(t -> t)
)
)
);
System.out.println("索引创建结果: " + response.acknowledged());
}
6.5.2 判断索引是否存在
public boolean indexExists(ElasticsearchClient client, String indexName) throws IOException {
return client.indices().exists(e -> e.index(indexName)).value();
}
6.5.3 删除索引
public void deleteIndex(ElasticsearchClient client, String indexName) throws IOException {
client.indices().delete(d -> d.index(indexName));
System.out.println("索引 " + indexName + " 已删除");
}
6.6 文档 CRUD 操作
6.6.1 新增文档(指定 ID)
public void indexDocument(ElasticsearchClient client) throws IOException {
Product product = new Product("iPhone 15", 7999.00, "手机", "苹果最新款智能手机");
IndexResponse response = client.index(i -> i
.index("products")
.id("1")
.document(product)
);
System.out.println("文档ID: " + response.id());
System.out.println("结果: " + response.result());
}
6.6.2 新增文档(自动生成 ID)
public void indexDocumentAutoId(ElasticsearchClient client) throws IOException {
Product product = new Product("华为 Mate 60", 6999.00, "手机", "华为旗舰手机");
IndexResponse response = client.index(i -> i
.index("products")
.document(product)
);
System.out.println("自动生成的ID: " + response.id());
}
6.6.3 根据 ID 查询文档
public Product getDocument(ElasticsearchClient client, String id) throws IOException {
GetResponse<Product> response = client.get(g -> g
.index("products")
.id(id),
Product.class
);
if (response.found()) {
Product product = response.source();
System.out.println("找到文档: " + product.getName());
return product;
} else {
System.out.println("文档不存在");
return null;
}
}
6.6.4 局部更新文档
public void updateDocument(ElasticsearchClient client, String id) throws IOException {
UpdateResponse<Product> response = client.update(u -> u
.index("products")
.id(id)
.action(a -> a
.doc(Map.of("price", 6999.00))
),
Product.class
);
System.out.println("更新结果: " + response.result());
}
6.6.5 删除文档
public void deleteDocument(ElasticsearchClient client, String id) throws IOException {
DeleteResponse response = client.delete(d -> d
.index("products")
.id(id)
);
System.out.println("删除结果: " + response.result());
}
6.6.6 判断文档是否存在
public boolean documentExists(ElasticsearchClient client, String id) throws IOException {
return client.exists(e -> e
.index("products")
.id(id)
).value();
}
6.7 查询操作
6.7.1 查询所有文档
public List<Product> matchAll(ElasticsearchClient client) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.matchAll(m -> m)
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.2 全文搜索(match 查询)
public List<Product> searchByKeyword(ElasticsearchClient client, String keyword) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.match(m -> m
.field("description")
.query(keyword)
)
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.3 精确查询(term 查询)
public List<Product> searchByCategory(ElasticsearchClient client, String category) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.term(t -> t
.field("category")
.value(category)
)
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.4 范围查询
public List<Product> searchByPriceRange(ElasticsearchClient client,
Double minPrice, Double maxPrice) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.range(r -> r
.field("price")
.gte(JsonData.of(minPrice))
.lte(JsonData.of(maxPrice))
)
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.5 布尔组合查询(bool 查询)
public List<Product> boolQuery(ElasticsearchClient client, String keyword,
Double minPrice, String category) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m
.multiMatch(mm -> mm
.fields("name", "description")
.query(keyword)
)
)
.filter(f -> f
.range(r -> r
.field("price")
.gte(JsonData.of(minPrice))
)
)
.filter(f -> f
.term(t -> t
.field("category")
.value(category)
)
)
)
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.6 分页查询
public List<Product> searchWithPagination(ElasticsearchClient client,
int from, int size) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.matchAll(m -> m)
)
.from(from)
.size(size)
.sort(so -> so
.field(f -> f.field("price").order(SortOrder.Desc))
),
Product.class
);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
}
6.7.7 高亮搜索
public List<Map<String, Object>> searchWithHighlight(ElasticsearchClient client,
String keyword) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.match(m -> m
.field("description")
.query(keyword)
)
)
.highlight(h -> h
.fields("description", hf -> hf
.preTags("<em>")
.postTags("</em>")
)
),
Product.class
);
List<Map<String, Object>> results = new ArrayList<>();
for (Hit<Product> hit : response.hits().hits()) {
Map<String, Object> result = new HashMap<>();
result.put("product", hit.source());
result.put("highlights", hit.highlight());
results.add(result);
}
return results;
}
6.8 聚合操作
6.8.1 分组统计
public Map<String, Long> aggregateByCategory(ElasticsearchClient client) throws IOException {
SearchResponse<Void> response = client.search(s -> s
.index("products")
.size(0)
.aggregations("by_category", a -> a
.terms(t -> t
.field("category")
.size(10)
)
),
Void.class
);
Aggregate aggregate = response.aggregations().get("by_category");
List<StringTermsBucket> buckets = aggregate.sters().buckets().array();
Map<String, Long> result = new LinkedHashMap<>();
for (StringTermsBucket bucket : buckets) {
result.put(bucket.key(), bucket.docCount());
}
return result;
}
6.8.2 计算平均值
public Double averagePrice(ElasticsearchClient client) throws IOException {
SearchResponse<Void> response = client.search(s -> s
.index("products")
.size(0)
.aggregations("avg_price", a -> a
.avg(avg -> avg
.field("price")
)
),
Void.class
);
return response.aggregations().get("avg_price").avg().value();
}
6.8.3 多指标聚合
public Map<String, Object> priceStats(ElasticsearchClient client) throws IOException {
SearchResponse<Void> response = client.search(s -> s
.index("products")
.size(0)
.aggregations("price_stats", a -> a
.stats(st -> st
.field("price")
)
),
Void.class
);
StatsAggregate stats = response.aggregations().get("price_stats").stats();
Map<String, Object> result = new LinkedHashMap<>();
result.put("count", stats.count());
result.put("min", stats.min());
result.put("max", stats.max());
result.put("avg", stats.avg());
result.put("sum", stats.sum());
return result;
}
6.9 批量操作
public void bulkIndex(ElasticsearchClient client) throws IOException {
List<Product> products = Arrays.asList(
new Product("MacBook Pro", 14999.00, "电脑", "苹果笔记本电脑"),
new Product("ThinkPad", 8999.00, "电脑", "联想商务笔记本"),
new Product("AirPods Pro", 1899.00, "耳机", "苹果无线降噪耳机")
);
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
for (int i = 0; i < products.size(); i++) {
bulkBuilder.operations(op -> op
.index(idx -> idx
.index("products")
.id(String.valueOf(i + 10))
.document(products.get(i))
)
);
}
BulkResponse response = bulkBuilder.build();
if (response.errors()) {
for (BulkResponseItem item : response.items()) {
if (item.error() != null) {
System.out.println("错误: " + item.error().reason());
}
}
} else {
System.out.println("批量操作成功,共 " + products.size() + " 条");
}
}
6.10 使用 Spring Data Elasticsearch
如果你使用 Spring Boot,可以使用 Spring Data Elasticsearch 来简化操作。
6.10.1 Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
6.10.2 application.yml 配置
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: your_password
6.10.3 定义实体
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Text)
private String description;
// getter 和 setter
}
6.10.4 定义 Repository
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
List<Product> findByCategory(String category);
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
List<Product> findByNameContaining(String keyword);
}
6.10.5 使用 Repository
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product save(Product product) {
return productRepository.save(product);
}
public Optional<Product> findById(String id) {
return productRepository.findById(id);
}
public List<Product> findByCategory(String category) {
return productRepository.findByCategory(category);
}
public List<Product> searchByName(String keyword) {
return productRepository.findByNameContaining(keyword);
}
public void deleteById(String id) {
productRepository.deleteById(id);
}
}
6.11 完整示例代码
以下是一个包含所有基本操作的完整示例类:
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.json.jackson.JacksonJsonpTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
public class ElasticsearchDemo {
private final ElasticsearchClient client;
public ElasticsearchDemo() {
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)
).build();
RestClientTransport transport = new RestClientTransport(
restClient, new JacksonJsonpTransport()
);
this.client = new ElasticsearchClient(transport);
}
public void createIndex() throws IOException {
CreateIndexResponse response = client.indices().create(c -> c
.index("products")
.settings(s -> s
.numberOfShards("1")
.numberOfReplicas("1")
)
.mappings(m -> m
.properties("name", p -> p.text(t -> t.analyzer("ik_max_word")))
.properties("price", p -> p.float_(f -> f))
.properties("category", p -> p.keyword(k -> k))
.properties("description", p -> p.text(t -> t))
)
);
System.out.println("创建索引: " + response.acknowledged());
}
public void addDocument(String id, Product product) throws IOException {
IndexResponse response = client.index(i -> i
.index("products")
.id(id)
.document(product)
);
System.out.println("文档 " + id + " 添加成功");
}
public Product getDocument(String id) throws IOException {
GetResponse<Product> response = client.get(g -> g
.index("products").id(id), Product.class
);
return response.found() ? response.source() : null;
}
public void updateDocument(String id, Map<String, Object> fields) throws IOException {
client.update(u -> u
.index("products")
.id(id)
.action(a -> a.doc(fields)),
Product.class
);
System.out.println("文档 " + id + " 更新成功");
}
public void deleteDocument(String id) throws IOException {
client.delete(d -> d.index("products").id(id));
System.out.println("文档 " + id + " 已删除");
}
public List<Product> search(String keyword) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.multiMatch(m -> m
.fields("name^2", "description")
.query(keyword)
)
),
Product.class
);
return response.hits().hits().stream()
.map(Hit::source)
.collect(Collectors.toList());
}
public List<Product> searchWithFilter(String keyword, Double minPrice,
String category) throws IOException {
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q
.bool(b -> b
.must(m -> m.multiMatch(mm -> mm
.fields("name^2", "description")
.query(keyword)
))
.filter(f -> f.range(r -> r
.field("price")
.gte(JsonData.of(minPrice))
))
.filter(f -> f.term(t -> t
.field("category")
.value(category)
))
)
),
Product.class
);
return response.hits().hits().stream()
.map(Hit::source)
.collect(Collectors.toList());
}
public void close() throws IOException {
client._transport().close();
}
public static void main(String[] args) throws IOException {
ElasticsearchDemo demo = new ElasticsearchDemo();
demo.createIndex();
demo.addDocument("1", new Product("iPhone 15", 7999.00, "手机", "苹果手机"));
demo.addDocument("2", new Product("华为 Mate 60", 6999.00, "手机", "华为手机"));
demo.addDocument("3", new Product("MacBook Pro", 14999.00, "电脑", "苹果电脑"));
Product product = demo.getDocument("1");
System.out.println("查询到: " + product.getName());
List<Product> results = demo.search("苹果");
System.out.println("搜索结果: " + results.size() + " 条");
demo.updateDocument("1", Map.of("price", 7499.00));
demo.deleteDocument("3");
demo.close();
}
}
附录:常见问题解答
Q1: 应该使用哪个 Java 客户端?
推荐使用 Elasticsearch Java API Client,它是 ES 官方最新维护的客户端,类型安全且功能完整。如果是新项目,不要使用已废弃的 RestHighLevel Client。
Q2: Spring Boot 项目推荐哪种方式?
简单查询使用 Spring Data Elasticsearch 的 Repository 接口即可,复杂查询建议注入 ElasticsearchClient 原生客户端。
Q3: 如何处理日期字段?
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
@JsonProperty("created_at")
private LocalDateTime createdAt;
Q4: 如何实现深分页?
使用 search_after 方式,避免 from + size 超过 10000 的限制:
SearchResponse<Product> response = client.search(s -> s
.index("products")
.query(q -> q.matchAll(m -> m))
.size(10)
.sort(so -> so.field(f -> f.field("price").order(SortOrder.Desc)))
.sort(so -> so.field(f -> f.field("_id").order(SortOrder.Asc)))
.searchAfter(lastSortValues),
Product.class
);
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)