SpringBoot集成Elasticsearch | Elasticsearch 8.x专属Java Client
- 前言
-
- [1. 版本说明与Maven依赖](#1. 版本说明与Maven依赖)
- [2. 配置文件(application.yml)](#2. 配置文件(application.yml))
- [3. 核心代码实现](#3. 核心代码实现)
-
- [3.1 配置类(构建Java Client)](#3.1 配置类(构建Java Client))
- [3.2 工具类(封装8.x Java Client操作)](#3.2 工具类(封装8.x Java Client操作))
- [3.3 实体类(Employee8xJavaClient)](#3.3 实体类(Employee8xJavaClient))
- [3.4 Controller层(测试接口)](#3.4 Controller层(测试接口))
- [4. 测试步骤](#4. 测试步骤)
SpringBoot集成Elasticsearch的三种核心方式,
Spring官方场景启动器、
Elasticsearch 7.x专属HLRC(High Level Rest Client)
Elasticsearch 8.x专属Java Client。
前言
Elasticsearch 8.x专属Java Client
Elasticsearch 8.x官方推出全新的Elasticsearch Java Client ,替代了HLRC(HLRC在8.x中已弃用)。
它基于异步非阻塞IO,支持所有8.x新特性,但仅兼容8.x ES服务器,与7.x完全不兼容。
1. 版本说明与Maven依赖
ES 8.x Java Client需与ES服务器版本完全一致(如ES 8.14.0 → Java Client 8.14.0),且依赖结构与HLRC不同。
xml
<dependencies>
<!-- Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- 1. ES 8.x Java Client核心依赖(版本与ES服务器一致) -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.14.0</version>
</dependency>
<!-- 2. JSON处理器(Java Client依赖Jackson,需指定版本) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- 3. 日志依赖(避免SLF4J绑定错误) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
2. 配置文件(application.yml)
ES 8.x默认开启安全认证 和SSL加密,需配置账号密码、SSL信任策略(开发环境可关闭SSL验证,生产环境需配置证书)。
yaml
server:
port: 8083 # 与前两种方式区分端口
# ES 8.x Java Client配置
elasticsearch:
java-client:
cluster-name: es-cluster-8x # 集群名称(非必需)
hosts: 127.0.0.1:9200 # 服务器地址(集群用逗号分隔)
scheme: https # 8.x默认https(7.x默认http)
# 安全认证(8.x默认开启,账号默认elastic,密码在ES首次启动时生成)
username: elastic
password: 你的ES 8.x密码
# 超时配置(毫秒)
connect-timeout: 5000
socket-timeout: 30000
# SSL配置(开发环境关闭证书验证,生产环境需配置cert-path)
ssl:
disable-verification: true # 关闭SSL证书验证(开发用,生产禁用)
# cert-path: classpath:es-cert.pem # 生产环境:SSL证书路径
3. 核心代码实现
3.1 配置类(构建Java Client)
ES 8.x Java Client提供ElasticsearchClient(同步)和ElasticsearchAsyncClient(异步),此处以同步客户端为例。
java
package com.es.demo.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* ES 8.x Java Client配置类
*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch.java-client")
public class Es8xJavaClientConfig {
// 配置参数(与application.yml对应)
private String clusterName;
private String hosts;
private String scheme;
private String username;
private String password;
private int connectTimeout;
private int socketTimeout;
private SslConfig ssl;
// 内部类:SSL配置
@Data
public static class SslConfig {
private boolean disableVerification;
private String certPath;
}
/**
* 构建ElasticsearchClient(同步客户端,Spring单例注入)
*/
@Bean(name = "es8xElasticsearchClient")
public ElasticsearchClient elasticsearchClient() throws Exception {
// 1. 解析ES服务器地址
HttpHost[] httpHosts = parseHttpHosts();
// 2. 配置安全认证(8.x默认开启)
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(username, password)
);
// 3. 配置SSL(开发环境关闭证书验证)
RestClientBuilder builder = RestClient.builder(httpHosts)
.setHttpClientConfigCallback(httpClientBuilder -> {
// 设置认证
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
// 配置SSL
if (ssl.isDisableVerification()) {
try {
// 创建信任所有证书的SSL上下文(开发用,生产禁用)
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}}, new java.security.SecureRandom());
httpClientBuilder.setSSLContext(sslContext);
// 禁用主机名验证
httpClientBuilder.setSSLHostnameVerifier((hostname, session) -> true);
} catch (Exception e) {
throw new RuntimeException("配置SSL失败", e);
}
}
return httpClientBuilder;
})
// 配置超时时间
.setRequestConfigCallback(requestConfigBuilder -> {
requestConfigBuilder.setConnectTimeout(connectTimeout);
requestConfigBuilder.setSocketTimeout(socketTimeout);
return requestConfigBuilder;
});
// 4. 构建Transport(Java Client依赖RestClientTransport)
ElasticsearchTransport transport = new RestClientTransport(
builder.build(),
new JacksonJsonpMapper() // JSON处理器(Jackson)
);
// 5. 构建并返回同步客户端
ElasticsearchClient client = new ElasticsearchClient(transport);
log.info("ES 8.x Java Client初始化完成,集群名称:{},服务器地址:{}", clusterName, hosts);
return client;
}
/**
* 解析ES服务器地址为HttpHost数组
*/
private HttpHost[] parseHttpHosts() {
String[] hostArray = hosts.split(",");
HttpHost[] httpHosts = new HttpHost[hostArray.length];
for (int i = 0; i < hostArray.length; i++) {
String host = hostArray[i];
String[] hostPort = host.split(":");
if (hostPort.length != 2) {
throw new RuntimeException("ES地址格式错误:" + host + "(正确格式:host:port)");
}
httpHosts[i] = new HttpHost(hostPort[0], Integer.parseInt(hostPort[1]), scheme);
}
return httpHosts;
}
}
3.2 工具类(封装8.x Java Client操作)
8.x Java Client API与HLRC差异较大,需使用官方新API(如CreateIndexRequest、SearchRequest的构建方式)。
java
package com.es.demo.util;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Highlight;
import co.elastic.clients.elasticsearch.core.search.HighlightField;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.json.JsonData;
import co.elastic.clients.util.ObjectBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
/**
* ES 8.x Java Client工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class Es8xJavaClientUtil {
// 注入8.x Java Client(与配置类中Bean名称一致)
private final ElasticsearchClient es8xElasticsearchClient;
// 常量定义
public static final String KEYWORD_SUFFIX = ".keyword";
public static final Time DEFAULT_TIMEOUT = Time.of(t -> t.time("3s"));
/**
* 1. 创建索引(带自定义mapping)
*/
public boolean createIndex(String indexName, String mapping) throws IOException {
if (isIndexExist(indexName)) {
log.warn("索引[{}]已存在,无需重复创建", indexName);
return false;
}
// 构建创建索引请求
CreateIndexRequest.Builder requestBuilder = CreateIndexRequest.of(b -> b.index(indexName));
// 设置mapping(若传入)
if (StringUtils.isNotBlank(mapping)) {
requestBuilder.mappings(m -> m.withJson(mapping));
}
CreateIndexResponse response = es8xElasticsearchClient.indices().create(requestBuilder.build());
log.info("索引[{}]创建成功,响应状态:{}", indexName, response.acknowledged());
return response.acknowledged();
}
/**
* 2. 判断索引是否存在
*/
public boolean isIndexExist(String indexName) throws IOException {
ExistsRequest request = ExistsRequest.of(b -> b.index(indexName));
boolean exists = es8xElasticsearchClient.indices().exists(request).value();
log.debug("索引[{}]存在状态:{}", indexName, exists);
return exists;
}
/**
* 3. 删除索引
*/
public boolean deleteIndex(String indexName) throws IOException {
if (!isIndexExist(indexName)) {
log.warn("索引[{}]不存在,无需删除", indexName);
return false;
}
DeleteIndexRequest request = DeleteIndexRequest.of(b -> b.index(indexName));
es8xElasticsearchClient.indices().delete(request);
log.info("索引[{}]删除成功", indexName);
return true;
}
/**
* 4. 添加文档(自动生成docId)
*/
public String addDoc(String indexName, Object data) throws IOException {
return addDoc(indexName, UUID.randomUUID().toString().replace("-", "").toUpperCase(), data);
}
/**
* 4. 重载:添加文档(自定义docId)
*/
public String addDoc(String indexName, String docId, Object data) throws IOException {
// 构建索引请求
IndexRequest<Object> request = IndexRequest.of(b -> b
.index(indexName)
.id(docId)
.document(data)
.timeout(DEFAULT_TIMEOUT)
);
IndexResponse response = es8xElasticsearchClient.index(request);
log.info("文档添加成功:索引[{}],docId[{}],响应状态[{}]",
indexName, response.id(), response.result().jsonValue());
return response.id();
}
/**
* 5. 根据docId删除文档
*/
public boolean deleteDocByDocId(String indexName, String docId) throws IOException {
if (!isDocExist(indexName, docId)) {
log.warn("文档不存在:索引[{}],docId[{}]", indexName, docId);
return false;
}
DeleteRequest request = DeleteRequest.of(b -> b
.index(indexName)
.id(docId)
.timeout(DEFAULT_TIMEOUT)
);
DeleteResponse response = es8xElasticsearchClient.delete(request);
log.info("文档删除成功:索引[{}],docId[{}],响应状态[{}]",
indexName, docId, response.result().jsonValue());
return true;
}
/**
* 6. 根据docId更新文档(部分更新)
*/
public boolean updateDocByDocId(String indexName, String docId, Map<String, Object> updateData) throws IOException {
if (!isDocExist(indexName, docId)) {
log.warn("文档不存在:索引[{}],docId[{}],无法更新", indexName, docId);
return false;
}
// 构建更新请求(传入Map类型的更新数据)
UpdateRequest<Object, Object> request = UpdateRequest.of(b -> b
.index(indexName)
.id(docId)
.doc(updateData)
.timeout(DEFAULT_TIMEOUT)
.refresh(r -> r.waitFor(true)) // 实时刷新
);
UpdateResponse<Object> response = es8xElasticsearchClient.update(request, Object.class);
log.info("文档更新成功:索引[{}],docId[{}],响应状态[{}]",
indexName, docId, response.result().jsonValue());
return true;
}
/**
* 7. 根据docId查询文档(返回Map)
*/
public Map<String, JsonData> getDocByDocId(String indexName, String docId) throws IOException {
if (!isDocExist(indexName, docId)) {
log.warn("文档不存在:索引[{}],docId[{}]", indexName, docId);
return null;
}
GetRequest request = GetRequest.of(b -> b
.index(indexName)
.id(docId)
);
GetResponse<Object> response = es8xElasticsearchClient.get(request, Object.class);
// 转换为Map<String, JsonData>(方便解析)
return response.sourceAsMap();
}
/**
* 8. 判断文档是否存在
*/
public boolean isDocExist(String indexName, String docId) throws IOException {
ExistsRequest request = ExistsRequest.of(b -> b
.index(indexName)
.id(docId)
);
boolean exists = es8xElasticsearchClient.exists(request).value();
log.debug("文档存在状态:索引[{}],docId[{}],存在[{}]", indexName, docId, exists);
return exists;
}
/**
* 9. 高亮查询(姓名模糊匹配,分页排序)
*/
public <T> List<T> searchHighlight(String indexName,
String keyword,
Integer pageNum,
Integer pageSize,
String sortField,
Class<T> clazz) throws IOException {
// 1. 构建查询条件(matchQuery)
Function<MatchQuery.Builder, ObjectBuilder<MatchQuery>> matchQuery = q -> q
.field("name")
.query(FieldValue.of(keyword))
.analyzer("ik_smart");
Query query = Query.of(b -> b.match(matchQuery));
// 2. 构建高亮配置
HighlightField highlightField = HighlightField.of(b -> b
.preTags("<span style='color:red'>")
.postTags("</span>")
.requireFieldMatch(false)
);
Highlight highlight = Highlight.of(b -> b
.fields("name", highlightField)
);
// 3. 构建搜索请求
SearchRequest request = SearchRequest.of(b -> b
.index(indexName)
.query(query)
.highlight(highlight)
.from((pageNum - 1) * pageSize)
.size(pageSize)
// 配置排序
.sort(s -> {
if (StringUtils.isNotBlank(sortField)) {
String sortFieldWithSuffix = sortField.contains(".") ? sortField : sortField + KEYWORD_SUFFIX;
s.field(f -> f.field(sortFieldWithSuffix).order(SortOrder.Asc));
}
return s;
})
);
// 4. 执行查询并解析结果
SearchResponse<T> response = es8xElasticsearchClient.search(request, clazz);
List<Hit<T>> hits = response.hits().hits();
List<T> resultList = new ArrayList<>();
for (Hit<T> hit : hits) {
T doc = hit.source();
// 处理高亮(此处需根据实体类字段手动设置,8.x Java Client需显式映射)
if (hit.highlight() != null && hit.highlight().containsKey("name")) {
// 假设实体类有setName方法,通过反射或FastJSON设置高亮值
String highlightName = hit.highlight().get("name").get(0);
if (doc instanceof Map) {
((Map<String, Object>) doc).put("name", highlightName);
} else {
// 若为POJO,可通过FastJSON转换后设置
String json = com.alibaba.fastjson.JSON.toJSONString(doc);
Map<String, Object> map = com.alibaba.fastjson.JSON.parseObject(json);
map.put("name", highlightName);
doc = com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson.JSON.toJSONString(map), clazz);
}
}
resultList.add(doc);
}
log.info("高亮查询完成:索引[{}],匹配总数[{}],分页[{}页/{}条]",
indexName, response.hits().total().value(), pageNum, pageSize);
return resultList;
}
}
3.3 实体类(Employee8xJavaClient)
与前两种方式一致,仅用于数据传输。
java
package com.es.demo.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 员工实体(用于ES 8.x Java Client)
*/
@Data
public class Employee8xJavaClient {
private String docId; // 文档ID
private String jobNo; // 工号
private String name; // 姓名
private String job; // 岗位
private Integer age; // 年龄
private BigDecimal salary; // 薪资
private Date jobDay; // 入职时间
private String remark; // 备注
}
3.4 Controller层(测试接口)
调用8.x工具类实现测试接口,注意处理SSL和认证相关异常。
环境提示:
- 开发/测试环境:创建索引时可暂不传mapping,利用ES动态映射快速验证接口连通性;
- 生产环境:必须传入自定义mapping(当前代码已配置),明确字段类型、分词器及格式规范,且需提前在集群中预创建索引并关闭动态映射,避免线上数据结构混乱。
java
package com.es.demo.controller;
import com.es.demo.entity.Employee8xJavaClient;
import com.es.demo.util.Es8xJavaClientUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/java-client8x/employee")
@RequiredArgsConstructor
public class Employee8xJavaClientController {
private final Es8xJavaClientUtil es8xJavaClientUtil;
private static final String EMPLOYEE_INDEX = "employee_java_client_8x";
/**
* 1. 创建员工索引(带mapping)
*/
@GetMapping("/index/create")
public String createIndex() {
String mapping = "{\n" +
" \"properties\": {\n" +
" \"jobNo\": {\"type\": \"keyword\"},\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"job\": {\"type\": \"keyword\"},\n" +
" \"age\": {\"type\": \"integer\"},\n" +
" \"salary\": {\"type\": \"double\"},\n" +
" \"jobDay\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd\"\n" +
" },\n" +
" \"remark\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_smart\"\n" +
" }\n" +
" }\n" +
"}";
try {
boolean result = es8xJavaClientUtil.createIndex(EMPLOYEE_INDEX, mapping);
return result ? "索引创建成功" : "索引已存在";
} catch (IOException e) {
log.error("创建索引失败", e);
return "索引创建失败:" + e.getMessage();
}
}
/**
* 2. 添加员工文档
*/
@PostMapping("/save")
public String save(@RequestBody Employee8xJavaClient employee) {
try {
String docId = es8xJavaClientUtil.addDoc(EMPLOYEE_INDEX, employee);
return "员工添加成功,docId:" + docId;
} catch (IOException e) {
log.error("添加员工失败", e);
return "添加员工失败:" + e.getMessage();
}
}
/**
* 3. 高亮查询员工
*/
@GetMapping("/search-highlight")
public List<Employee8xJavaClient> searchHighlight(
@RequestParam String name,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
// 调用工具类高亮查询(按jobNo排序)
return es8xJavaClientUtil.searchHighlight(
EMPLOYEE_INDEX,
name,
pageNum,
pageSize,
"jobNo", // 排序字段
Employee8xJavaClient.class // 返回实体类类型
);
} catch (IOException e) {
log.error("高亮查询员工失败(姓名关键词:{})", name, e);
return null;
}
}
/**
* 4. 测试示例:添加单个员工
*/
@GetMapping("/test/save")
public String testSave(
@RequestParam String jobNo,
@RequestParam String name,
@RequestParam String job,
@RequestParam Integer age,
@RequestParam BigDecimal salary,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date jobDay,
@RequestParam(required = false) String remark) {
Employee8xJavaClient employee = new Employee8xJavaClient();
// 赋值请求参数到员工实体
employee.setJobNo(jobNo);
employee.setName(name);
employee.setJob(job);
employee.setAge(age);
employee.setSalary(salary);
employee.setJobDay(jobDay);
employee.setRemark(remark);
// docId留空,由工具类自动生成UUID
try {
String docId = es8xJavaClientUtil.addDoc(EMPLOYEE_INDEX, employee);
return "测试添加员工成功,docId:" + docId;
} catch (IOException e) {
log.error("测试添加员工失败", e);
return "测试添加员工失败:" + e.getMessage();
}
}
}
4. 测试步骤
- 启动ES 8.x服务器(确保9200端口可访问,且已记录初始化时生成的elastic账号密码,需与application.yml配置一致);
- 启动SpringBoot项目(若启动时出现SSL相关异常,检查application.yml中
ssl.disable-verification: true配置是否生效); - 调用创建索引接口:访问http://localhost:8083/api/java-client8x/employee/index/create,返回"索引创建成功"即可;
- 调用测试添加接口:访问http://localhost:8083/api/java-client8x/employee/test/save?jobNo=2024003\&name=王五\&job=后端开发\&age=30\&salary=28000\&jobDay=2022-05-10\&remark=架构师,获取返回的docId;
- 调用高亮查询接口:访问http://localhost:8083/api/java-client8x/employee/search-highlight?name=王\&pageNum=1\&pageSize=10,验证返回结果中`name`字段是否有红色高亮标签,且数据匹配正确。