SpringBoot集成Elasticsearch | Elasticsearch 8.x专属Java Client
SpringBoot集成Elasticsearch | Elasticsearch 8.x专属Java Client
- 前言
- 1. 版本说明与Maven依赖
- 2. 配置文件(application.yml)
- 3. 核心代码实现
- 3.1 配置类(构建Java Client)
- 3.2 工具类(封装8.x Java Client操作)
- 3.3 实体类(Employee8xJavaClient)
- 3.4 Controller层(测试接口)
- 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不同。
<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验证,生产环境需配置证书)。
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: elasticpassword: 你的ES 8.x密码# 超时配置(毫秒)connect-timeout: 5000socket-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(异步),此处以同步客户端为例。
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配置@Datapublic 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);// 配置SSLif (ssl.isDisableVerification()) {try {// 创建信任所有证书的SSL上下文(开发用,生产禁用)SSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, new TrustManager[]{new X509TrustManager() {@Overridepublic X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}@Overridepublic void checkClientTrusted(X509Certificate[] certs, String authType) {}@Overridepublic 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的构建方式)。
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)
与前两种方式一致,仅用于数据传输。
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; // 文档IDprivate 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(当前代码已配置),明确字段类型、分词器及格式规范,且需提前在集群中预创建索引并关闭动态映射,避免线上数据结构混乱。
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留空,由工具类自动生成UUIDtry {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字段是否有红色高亮标签,且数据匹配正确。
