SpringBoot集成Elasticsearch | Elasticsearch 7.x专属HLRC(High Level Rest Client)
SpringBoot集成Elasticsearch | Elasticsearch 7.x专属HLRC(High Level Rest Client)
- 前言
- 1. 版本说明与Maven依赖
- 2. 配置文件(application.yml)
- 3. 核心代码实现
- 3.1 配置类(构建HLRC客户端)
- 3.2 工具类(封装ES操作)
- 3.3 实体类(Employee7xHlrc)
- 3.4 Controller层(测试接口)
- 4. 测试步骤
SpringBoot集成Elasticsearch的三种核心方式,
Spring官方场景启动器、
Elasticsearch 7.x专属HLRC(High Level Rest Client)
Elasticsearch 8.x专属Java Client。
前言
Elasticsearch 7.x专属HLRC(High Level Rest Client)
HLRC是ES 7.x官方唯一推荐的高级客户端,稳定性与兼容性最优,支持所有ES 7.x特性,且版本需与ES服务器完全一致
1. 版本说明与Maven依赖
<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. Elasticsearch核心依赖(版本与ES服务器一致) --><dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId><version>7.15.2</version></dependency><!-- 2. HLRC底层依赖(低级REST客户端) --><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-client</artifactId><version>7.15.2</version></dependency><!-- 3. HLRC核心依赖(高级REST客户端) --><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.15.2</version></dependency>
</dependencies>
2. 配置文件(application.yml)
配置ES连接信息、超时时间、连接池参数(7.x默认未开启安全认证)。
server:port: 8082 # 与方式一区分端口# ES HLRC配置
elasticsearch:hlrc:cluster-name: es-cluster-7x # 集群名称(非必需,仅标识)hosts: 127.0.0.1:9200 # 服务器地址(集群用逗号分隔)scheme: http # 协议(7.x默认http,8.x默认https)# 安全认证(7.x默认关闭,开启需配置)# username: elastic# password: 你的ES密码# 超时配置(毫秒)connect-timeout: 1000socket-timeout: 30000connection-request-timeout: 500# 连接池配置max-connect-num: 100 # 总连接数max-connect-per-route: 50 # 单个节点最大连接数
3. 核心代码实现
3.1 配置类(构建HLRC客户端)
通过@ConfigurationProperties绑定配置,构建单例RestHighLevelClient(由Spring管理,避免重复创建连接)。
package com.es.demo.config;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.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.ArrayList;
import java.util.List;/*** ES 7.x HLRC客户端配置类*/
@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch.hlrc")
public class Es7xHlrcConfig {// 配置参数(与application.yml对应)private String clusterName;private String hosts;private String scheme;private String username;private String password;private int connectTimeout;private int socketTimeout;private int connectionRequestTimeout;private int maxConnectNum;private int maxConnectPerRoute;/*** 构建RestHighLevelClient实例(单例,Spring自动注入)*/@Bean(name = "es7xRestHighLevelClient")public RestHighLevelClient restHighLevelClient() {// 1. 解析ES服务器地址(host:port格式)List<HttpHost> httpHostList = new ArrayList<>();String[] hostArray = hosts.split(",");for (String host : hostArray) {String[] hostPort = host.split(":");if (hostPort.length != 2) {throw new RuntimeException("ES地址格式错误:" + host + "(正确格式:host:port)");}httpHostList.add(new HttpHost(hostPort[0], Integer.parseInt(hostPort[1]), scheme));}// 2. 构建RestClientBuilderRestClientBuilder builder = RestClient.builder(httpHostList.toArray(new HttpHost[0]));// 3. 配置安全认证(若开启)CredentialsProvider credentialsProvider = new BasicCredentialsProvider();if (username != null && password != null) {credentialsProvider.setCredentials(AuthScope.ANY,new UsernamePasswordCredentials(username, password));}// 4. 配置超时时间(连接、Socket、请求排队)builder.setRequestConfigCallback(requestConfigBuilder -> {requestConfigBuilder.setConnectTimeout(connectTimeout);requestConfigBuilder.setSocketTimeout(socketTimeout);requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeout);return requestConfigBuilder;});// 5. 配置连接池与认证builder.setHttpClientConfigCallback(httpClientBuilder -> {httpClientBuilder.setMaxConnTotal(maxConnectNum); // 总连接数httpClientBuilder.setMaxConnPerRoute(maxConnectPerRoute); // 单节点连接数if (username != null && password != null) {httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);}return httpClientBuilder;});// 6. 构建并返回客户端RestHighLevelClient client = new RestHighLevelClient(builder);log.info("ES 7.x HLRC客户端初始化完成,集群名称:{},服务器地址:{}", clusterName, hosts);return client;}
}
3.2 工具类(封装ES操作)
封装索引CRUD、文档CRUD、高亮查询等核心操作,避免重复代码。
package com.es.demo.util;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
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.support.master.AcknowledgedResponse;
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.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
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;/*** ES 7.x HLRC工具类(封装核心操作)*/
@Slf4j
@Component
@RequiredArgsConstructor
public class Es7xHlrcUtil {// 注入HLRC客户端(与配置类中Bean名称一致)private final RestHighLevelClient es7xRestHighLevelClient;// 常量定义public static final String KEYWORD_SUFFIX = ".keyword"; // 精确匹配后缀(text类型字段用)public static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueSeconds(3); // 默认超时时间/*** 1. 创建索引(若已存在则返回false)* @param indexName 索引名(需小写,无特殊字符)* @param mapping DSL(可选,如指定字段类型、分词器,null则用默认映射)*/public boolean createIndex(String indexName, String mapping) throws IOException {if (isIndexExist(indexName)) {log.warn("索引[{}]已存在,无需重复创建", indexName);return false;}CreateIndexRequest request = new CreateIndexRequest(indexName);// 若传入mapping,则设置索引映射if (StringUtils.isNotBlank(mapping)) {request.mapping(mapping, XContentType.JSON);}CreateIndexResponse response = es7xRestHighLevelClient.indices().create(request, RequestOptions.DEFAULT);log.info("索引[{}]创建成功,响应状态:{}", indexName, response.isAcknowledged());return response.isAcknowledged();}/*** 2. 判断索引是否存在*/public boolean isIndexExist(String indexName) throws IOException {GetIndexRequest request = new GetIndexRequest(indexName);boolean exists = es7xRestHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);log.debug("索引[{}]存在状态:{}", indexName, exists);return exists;}/*** 3. 删除索引(若不存在则返回false)*/public boolean deleteIndex(String indexName) throws IOException {if (!isIndexExist(indexName)) {log.warn("索引[{}]不存在,无需删除", indexName);return false;}DeleteIndexRequest request = new DeleteIndexRequest(indexName);AcknowledgedResponse response = es7xRestHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);log.info("索引[{}]删除成功,响应状态:{}", indexName, response.isAcknowledged());return response.isAcknowledged();}/*** 4. 添加文档(自定义docId,不存在则新增,存在则覆盖)* @param indexName 索引名* @param docId 文档ID(null则让ES自动生成)* @param data 文档数据(POJO或Map)*/public String addDoc(String indexName, String docId, Object data) throws IOException {// 转换数据为JSON字符串String jsonData = JSON.toJSONString(data);IndexRequest request = new IndexRequest(indexName);// 设置文档ID(null则自动生成)if (StringUtils.isNotBlank(docId)) {request.id(docId);}// 设置超时时间和数据request.timeout(DEFAULT_TIMEOUT);request.source(jsonData, XContentType.JSON);IndexResponse response = es7xRestHighLevelClient.index(request, RequestOptions.DEFAULT);log.info("文档添加成功:索引[{}],docId[{}],响应状态[{}]",indexName, response.getId(), response.status().getStatus());return response.getId(); // 返回实际的docId(自动生成或自定义)}/*** 4. 重载:添加文档(自动生成UUID作为docId)*/public String addDoc(String indexName, Object data) throws IOException {String autoDocId = UUID.randomUUID().toString().replace("-", "").toUpperCase();return addDoc(indexName, autoDocId, data);}/*** 5. 根据docId删除文档*/public boolean deleteDocByDocId(String indexName, String docId) throws IOException {if (!isDocExist(indexName, docId)) {log.warn("文档不存在:索引[{}],docId[{}]", indexName, docId);return false;}DeleteRequest request = new DeleteRequest(indexName, docId);request.timeout(DEFAULT_TIMEOUT);DeleteResponse response = es7xRestHighLevelClient.delete(request, RequestOptions.DEFAULT);log.info("文档删除成功:索引[{}],docId[{}],响应状态[{}]",indexName, docId, response.status().getStatus());return response.status().getStatus() == 200;}/*** 6. 根据docId更新文档(部分更新,仅修改传入字段)*/public boolean updateDocByDocId(String indexName, String docId, Object updateData) throws IOException {if (!isDocExist(indexName, docId)) {log.warn("文档不存在:索引[{}],docId[{}],无法更新", indexName, docId);return false;}// 转换更新数据为JSON字符串String jsonData = JSON.toJSONString(updateData);UpdateRequest request = new UpdateRequest(indexName, docId);request.timeout(DEFAULT_TIMEOUT);request.doc(jsonData, XContentType.JSON);// 实时刷新(wait_for:等待刷新完成后返回,确保更新后可立即查询,性能略低)request.setRefreshPolicy("wait_for");UpdateResponse response = es7xRestHighLevelClient.update(request, RequestOptions.DEFAULT);log.info("文档更新成功:索引[{}],docId[{}],响应状态[{}]",indexName, docId, response.status().getStatus());return response.status().getStatus() == 200;}/*** 7. 根据docId查询文档* @param fields 需要返回的字段(逗号分隔,如"name,age",null则返回所有字段)*/public Map<String, Object> getDocByDocId(String indexName, String docId, String fields) throws IOException {if (!isDocExist(indexName, docId)) {log.warn("文档不存在:索引[{}],docId[{}]", indexName, docId);return null;}GetRequest request = new GetRequest(indexName, docId);// 筛选返回字段if (StringUtils.isNotBlank(fields)) {request.fetchSourceContext(new FetchSourceContext(true,fields.split(","), // 需返回的字段Strings.EMPTY_ARRAY // 需排除的字段(空数组表示不排除)));}GetResponse response = es7xRestHighLevelClient.get(request, RequestOptions.DEFAULT);Map<String, Object> docData = response.getSource();// 补充docId到返回结果中(方便前端使用)if (docData != null) {docData.put("docId", response.getId());}return docData;}/*** 8. 判断文档是否存在(仅判断存在性,不返回数据,性能更高)*/public boolean isDocExist(String indexName, String docId) throws IOException {GetRequest request = new GetRequest(indexName, docId);// 不获取文档内容,仅判断存在性request.fetchSourceContext(new FetchSourceContext(false));request.storedFields("_none_");boolean exists = es7xRestHighLevelClient.exists(request, RequestOptions.DEFAULT);log.debug("文档存在状态:索引[{}],docId[{}],存在[{}]", indexName, docId, exists);return exists;}/*** 9. 高亮查询(支持分页、排序、字段筛选)* @param indexName 索引名* @param searchSourceBuilder 查询条件(需外部构建,如匹配、范围查询)* @param pageNum 页码(从1开始)* @param pageSize 每页条数* @param fields 返回字段(逗号分隔,null则返回所有)* @param sortField 排序字段(null则不排序,text类型需加.keyword)* @param highlightField 高亮字段(如"name")*/public List<Map<String, Object>> searchHighlight(String indexName,SearchSourceBuilder searchSourceBuilder,Integer pageNum,Integer pageSize,String fields,String sortField,String highlightField) throws IOException {// 1. 配置分页(from = (页码-1)*每页条数)int from = (pageNum - 1) * pageSize;searchSourceBuilder.from(from).size(pageSize);// 2. 配置排序(text类型字段需加.keyword才能精确排序)if (StringUtils.isNotBlank(sortField)) {if (sortField.contains(".")) {searchSourceBuilder.sort(sortField, SortOrder.ASC);} else {searchSourceBuilder.sort(sortField + KEYWORD_SUFFIX, SortOrder.ASC);}}// 3. 配置返回字段筛选if (StringUtils.isNotBlank(fields)) {searchSourceBuilder.fetchSource(new FetchSourceContext(true,fields.split(","),Strings.EMPTY_ARRAY));}// 4. 配置高亮(红色span标签)if (StringUtils.isNotBlank(highlightField)) {HighlightBuilder highlightBuilder = new HighlightBuilder();HighlightBuilder.Field field = new HighlightBuilder.Field(highlightField).preTags("<span style='color:red'>").postTags("</span>").requireFieldMatch(false); // 允许高亮多个字段highlightBuilder.field(field);searchSourceBuilder.highlighter(highlightBuilder);}// 5. 执行查询SearchRequest searchRequest = new SearchRequest(indexName);searchRequest.source(searchSourceBuilder);SearchResponse response = es7xRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);// 6. 解析查询结果(处理高亮字段)List<Map<String, Object>> resultList = new ArrayList<>();for (SearchHit hit : response.getHits().getHits()) {Map<String, Object> docData = hit.getSourceAsMap();// 补充docIddocData.put("docId", hit.getId());// 替换高亮字段(将原始字段值替换为高亮后的内容)if (StringUtils.isNotBlank(highlightField)) {HighlightField highlight = hit.getHighlightFields().get(highlightField);if (highlight != null && highlight.getFragments() != null) {// 拼接多片段(如字段内容过长,ES会拆分为多个片段)StringBuilder highlightValue = new StringBuilder();for (Text fragment : highlight.getFragments()) {highlightValue.append(fragment.string());}docData.put(highlightField, highlightValue.toString());}}resultList.add(docData);}log.info("高亮查询完成:索引[{}],匹配总数[{}],分页[{}页/{}条]",indexName, response.getHits().getTotalHits().value, pageNum, pageSize);return resultList;}
}
3.3 实体类(Employee7xHlrc)
与方式一类似,仅用于数据传输,无需ES注解(HLRC不依赖Spring Data注解)。
package com.es.demo.entity;import lombok.Data;import java.math.BigDecimal;
import java.util.Date;/*** 员工实体(用于ES 7.x HLRC)*/
@Data
public class Employee7xHlrc {private String docId; // 文档ID(可选,HLRC可自动生成)private String jobNo; // 工号private String name; // 姓名private String job; // 岗位private Integer age; // 年龄private BigDecimal salary; // 薪资private Date jobDay; // 入职时间private String remark; // 备注
}
3.4 Controller层(测试接口)
调用工具类实现业务接口,方便测试。
环境提示:
- 开发/测试环境:创建索引时可不传mapping参数,依赖ES动态映射快速验证接口功能;
- 生产环境:必须传入自定义mapping(如当前代码中指定字段类型和IK分词器的配置),精准控制字段属性,同时建议关闭动态映射并提前预创建索引。
package com.es.demo.controller;import com.es.demo.entity.Employee7xHlrc;
import com.es.demo.util.Es7xHlrcUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
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/hlrc7x/employee")
@RequiredArgsConstructor
public class Employee7xHlrcController {private final Es7xHlrcUtil es7xHlrcUtil;// 索引名(与实体类对应)private static final String EMPLOYEE_INDEX = "employee_hlrc_7x";/*** 1. 创建员工索引(带自定义mapping,指定字段类型和分词器)*/@GetMapping("/index/create")public String createIndex() {// 自定义mapping(指定字段类型、IK分词器)String mapping = "{\n" +" \"properties\": {\n" +" \"jobNo\": {\n" +" \"type\": \"keyword\"\n" +" },\n" +" \"name\": {\n" +" \"type\": \"text\",\n" +" \"analyzer\": \"ik_max_word\",\n" +" \"search_analyzer\": \"ik_smart\"\n" +" },\n" +" \"job\": {\n" +" \"type\": \"keyword\"\n" +" },\n" +" \"age\": {\n" +" \"type\": \"integer\"\n" +" },\n" +" \"salary\": {\n" +" \"type\": \"double\"\n" +" },\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 = es7xHlrcUtil.createIndex(EMPLOYEE_INDEX, mapping);return result ? "索引创建成功" : "索引已存在";} catch (IOException e) {log.error("创建索引失败", e);return "索引创建失败:" + e.getMessage();}}/*** 2. 删除员工索引*/@GetMapping("/index/delete")public String deleteIndex() {try {boolean result = es7xHlrcUtil.deleteIndex(EMPLOYEE_INDEX);return result ? "索引删除成功" : "索引不存在";} catch (IOException e) {log.error("删除索引失败", e);return "索引删除失败:" + e.getMessage();}}/*** 3. 添加员工文档(自动生成docId)*/@PostMapping("/save")public String save(@RequestBody Employee7xHlrc employee) {try {String docId = es7xHlrcUtil.addDoc(EMPLOYEE_INDEX, employee);return "员工添加成功,docId:" + docId;} catch (IOException e) {log.error("添加员工失败", e);return "添加员工失败:" + e.getMessage();}}/*** 4. 根据docId查询员工*/@GetMapping("/get/{docId}")public Map<String, Object> get(@PathVariable String docId) {try {// 仅返回name、job、salary、jobDay字段return es7xHlrcUtil.getDocByDocId(EMPLOYEE_INDEX, docId, "name,job,salary,jobDay");} catch (IOException e) {log.error("查询员工失败(docId:{})", docId, e);return null;}}/*** 5. 高亮查询员工(姓名模糊匹配)*/@GetMapping("/search-highlight")public List<Map<String, Object>> searchHighlight(@RequestParam String name,@RequestParam(defaultValue = "1") Integer pageNum,@RequestParam(defaultValue = "10") Integer pageSize) {try {// 构建查询条件:姓名模糊匹配(matchQuery支持分词)SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();searchSourceBuilder.query(QueryBuilders.matchQuery("name", name));// 调用工具类高亮查询(按jobNo升序排序,高亮name字段)return es7xHlrcUtil.searchHighlight(EMPLOYEE_INDEX,searchSourceBuilder,pageNum,pageSize,"name,jobNo,job,age,salary", // 返回字段"jobNo", // 排序字段(keyword类型,无需加后缀)"name" // 高亮字段);} catch (IOException e) {log.error("高亮查询员工失败(姓名关键词:{})", name, e);return null;}}/*** 6. 测试示例:添加单个员工(手动传参)*/@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) {Employee7xHlrc employee = new Employee7xHlrc();employee.setJobNo(jobNo);employee.setName(name);employee.setJob(job);employee.setAge(age);employee.setSalary(salary);employee.setJobDay(jobDay);employee.setRemark(remark);try {String docId = es7xHlrcUtil.addDoc(EMPLOYEE_INDEX, employee);return "测试添加成功,docId:" + docId;} catch (IOException e) {log.error("测试添加员工失败", e);return "测试添加失败:" + e.getMessage();}}
}
4. 测试步骤
- 启动ES 7.15.2服务器;
- 调用创建索引接口:
http://localhost:8082/api/hlrc7x/employee/index/create; - 调用测试添加接口:
http://localhost:8082/api/hlrc7x/employee/test/save?jobNo=2024002&name=李四&job=前端开发&age=26&salary=22000&jobDay=2023-03-20&remark=UI专家; - 调用高亮查询接口:
http://localhost:8082/api/hlrc7x/employee/search-highlight?name=李&pageNum=1&pageSize=10,验证高亮效果。
