Spring 集成 ElasticSearch8.6.0
(不使用 spring-boot-starter-data-elasticsearch)
1. 引入依赖
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.8.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.0.1</version>
</dependency>
2. 配置文件
2.1 创建 API 密钥
2.1.1 进入 Stack Management 控制台找到创建 API 密钥入口
- 进入 Stack Management 控制台
- 在左侧菜单中选择「Security」→「API 密钥」→「创建 API 密钥」
或者直接访问地址:http://[服务器地址]:5601/app/management/security/api_keys/


2.1.2 配置 API 密钥
-
输入自定义名称(如
test) -
点击「创建 API 密钥」

2.1.3 保存 API 密钥
密钥仅创建时展示,需立即复制保存,丢失需重新创建

2.2 yml 文件加入配置
spring:
elasticsearch:
uris: http://[服务器地址]:9200
username: [es登录用户名]
password: [es登录密码]
apikey: [你的API密钥]
2.3 创建配置类
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 org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Elasticsearch 配置信息
*/
@Configuration
@ConfigurationProperties(prefix = "spring.elasticsearch")
public class ElasticsearchConfig {
/**
* 服务器地址
*/
private String uris;
/**
* API key
*/
private String apikey;
public String getUris() {
return uris;
}
public void setUris(String uris) {
this.uris = uris;
}
public String getApikey() {
return apikey;
}
public void setApikey(String apikey) {
this.apikey = apikey;
}
@Bean
public ElasticsearchClient elasticsearchClient() {
// 1. 创建底层 RestClient
RestClient restClient = RestClient
.builder(HttpHost.create(uris))
.setDefaultHeaders(new Header[]{
new BasicHeader("Authorization", "ApiKey " + apikey)
})
.build();
// 2. 创建 Transport 层
ElasticsearchTransport transport = new RestClientTransport(
restClient,
new JacksonJsonpMapper());
// 3. 创建 API 客户端
return new ElasticsearchClient(transport);
}
}
3. 自定义注解
3.1 ElasticId(标识主键字段)
import java.lang.annotation.*;
/**
* 自定义elasticsearch注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ElasticId {
}
3.2 ElasticField(标识字段映射属性)
import co.elastic.clients.elasticsearch._types.mapping.FieldType;
import java.lang.annotation.*;
/**
* 自定义elasticsearch注解
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ElasticField {
/**
* 索引类型
*/
FieldType type() default FieldType.None;
/**
* 搜索时分词器
*/
String searchAnalyzer() default "";
/**
* 索引时分词器
*/
String analyzer() default "";
}
3.3 ElasticDocument(标识文档与索引映射)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义elasticsearch注解
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ElasticDocument {
/**
* 索引名称
*/
String indexName();
/**
* 是否创建索引
*/
boolean createIndex() default true;
}
4. 核心工具类
4.1 表格分页数据对象(TableDataInfo)
import java.io.Serializable;
import java.util.List;
/**
* 表格分页数据对象
*
* @author ruoyi
*/
public class TableDataInfo implements Serializable
{
private static final long serialVersionUID = 1L;
/** 总记录数 */
private long total;
/** 列表数据 */
private List<?> rows;
/** 消息状态码 */
private int code;
/** 消息内容 */
private String msg;
/**
* 表格数据对象
*/
public TableDataInfo()
{
}
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public TableDataInfo(List<?> list, long total)
{
this.rows = list;
this.total = total;
}
public long getTotal()
{
return total;
}
public void setTotal(long total)
{
this.total = total;
}
public List<?> getRows()
{
return rows;
}
public void setRows(List<?> rows)
{
this.rows = rows;
}
public int getCode()
{
return code;
}
public void setCode(int code)
{
this.code = code;
}
public String getMsg()
{
return msg;
}
public void setMsg(String msg)
{
this.msg = msg;
}
}
4.2 ElasticSearch 处理工具类(ElasticService)
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.mapping.*;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery;
import co.elastic.clients.elasticsearch.core.BulkRequest;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.bulk.BulkOperation;
import co.elastic.clients.elasticsearch.core.bulk.IndexOperation;
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.ElasticsearchIndicesClient;
import co.elastic.clients.elasticsearch.indices.ExistsRequest;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import TableDataInfo;
import annotation.ElasticDocument;
import annotation.ElasticField;
import annotation.ElasticId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ElasticSearch相关处理
*/
public class ElasticService<T> {
private static final Logger log = LoggerFactory.getLogger(ElasticService.class);
/**
* 实体对象
*/
public Class<T> clazz;
/**
* Elasticsearch客户端
*/
public ElasticsearchClient esClient;
/**
* 索引名称
*/
public String indexName;
/**
* 索引主键名称
*/
public String idName = "id";
/**
* ElasticsearchUtil实例构造器
*
* @param esClient elasticsearch客户端
* @param clazz 操作类
*/
public ElasticService(ElasticsearchClient esClient, Class<T> clazz) {
this.clazz = clazz;
this.esClient = esClient;
try {
this.init();
} catch (IOException e) {
log.error("初始化Elasticsearch索引失败", e);
}
}
/**
* 批量更新或保存实体对象列表到Elasticsearch
*
* @param list 待批量操作的实体对象列表
* @return 批量更新或保存响应结果
*/
public BulkResponse bulkIndex(List<T> list) {
// 参数校验
if (list == null || list.isEmpty()) {
return null;
}
// 获取ID字段
Field idField = null;
boolean originalAccessible = false;
if (idName != null && !idName.isEmpty()) {
try {
idField = clazz.getDeclaredField(idName);
} catch (NoSuchFieldException e) {
log.error("ID字段解析异常", e);
return null;
}
// 设置字段可访问
originalAccessible = idField.isAccessible();
idField.setAccessible(true);
}
try {
List<BulkOperation> operations = this.buildIndexOperations(list, idField);
// 创建批量请求
BulkRequest bulkRequest = BulkRequest.of(b -> b
.operations(operations)
);
// 执行批量操作
return esClient.bulk(bulkRequest);
} catch (IllegalAccessException e) {
log.error("id字段访问异常", e);
} catch (IOException e) {
log.error("批量更新或保存失败", e);
} finally {
// 恢复字段访问权限
if (idField != null) {
idField.setAccessible(originalAccessible);
}
}
return null;
}
/**
* 多条件匹配查询
*
* @param searchMap 查询条件
* @param pageNum 当前页码(从0开始)
* @param pageSize 每页显示条数
* @return 匹配分页结果
*/
public TableDataInfo searchByMultiField(Map<String, String> searchMap, int pageNum, int pageSize) {
// 创建查询请求
SearchRequest request = SearchRequest.of(r -> r
.index(indexName)
.query(q -> {
// 如果没有查询条件,则返回所有数据
if (searchMap == null || searchMap.isEmpty()) {
return q.matchAll(MatchAllQuery.of(t -> t));
}
// 如果存在查询条件,则进行多条件匹配查询
return q
.bool(b -> b
.must(m -> m
.match(t -> {
searchMap.forEach((k, v) -> {
t.field(k);
t.query(v);
});
return t;
}
)
)
);
}
)
.from(pageNum)
.size(pageSize)
);
// 打印dsl语句
log.info("[searchByMultiField]执行查询:{}", request.toString());
TableDataInfo rspData = new TableDataInfo();
try {
// 执行查询
SearchResponse<T> response = esClient.search(request, clazz);
// 获取查询结果列表
List<T> list = response.hits().hits().stream().map(Hit::source).collect(Collectors.toList());
// 获取查询结果总数
long total = response.hits().total().value();
rspData.setTotal(total);
rspData.setRows(list);
rspData.setCode(200);
rspData.setMsg("查询成功");
} catch (Exception e) {
rspData.setCode(500);
rspData.setMsg("查询失败!" + e.getMessage());
log.error("查询失败", e);
}
return rspData;
}
/**
* 初始化Elasticsearch索引,如果索引不存在则根据实体类注解创建新索引
*
* @throws IOException 当Elasticsearch操作出现IO异常时抛出
*/
private void init() throws IOException {
// 获取索引注解
String indexName = clazz.getAnnotation(ElasticDocument.class).indexName();
this.indexName = indexName;
// 构建Elasticsearch索引的类型映射
TypeMapping.Builder typeMapping = this.getTypeMapping();
// 查询索引是否存在
ElasticsearchIndicesClient indices = esClient.indices();
BooleanResponse existsResponse = indices.exists(ExistsRequest.of(t -> t.index(indexName)));
if (existsResponse.value()) {
return;
}
// 创建索引
CreateIndexResponse createIndexResponse = indices.create(CreateIndexRequest.of(r -> r
.mappings(m -> typeMapping)
.index(indexName))
);
log.info("索引创建成功:{}", createIndexResponse.index());
}
/**
* 根据实体类的注解信息构建Elasticsearch索引的类型映射
*
* @return Elasticsearch类型映射构建器
*/
private TypeMapping.Builder getTypeMapping() {
// 收集类及其父类的所有字段
List<Field> tempFields = new ArrayList<>();
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
TypeMapping.Builder mappingBuilder = new TypeMapping.Builder();
// 遍历所有字段
for (Field field : tempFields) {
// 存储主键id的字段名
if (field.isAnnotationPresent(ElasticId.class)) {
this.idName = field.getName();
}
// Elasticsearch属性映射配置
if (field.isAnnotationPresent(ElasticField.class)) {
Property property = this.getProperty(field);
mappingBuilder.properties(field.getName(), property);
}
}
return mappingBuilder;
}
/**
* 根据字段注解信息获取Elasticsearch属性映射配置
*
* @param field 包含ElasticField注解的Java字段
* @return 对应的Elasticsearch属性映射配置
*/
private Property getProperty(Field field) {
ElasticField elasticField = field.getAnnotation(ElasticField.class);
FieldType type = elasticField.type();
String analyzer = elasticField.analyzer();
String searchAnalyzer = elasticField.searchAnalyzer();
Property property;
// 处理文本类型字段
if (FieldType.Text.equals(type)) {
property = TextProperty.of(p -> p
.analyzer(analyzer)
.searchAnalyzer(searchAnalyzer)
)._toProperty();
}
// 处理Keyword类型字段
else if (FieldType.Keyword.equals(type)) {
property = KeywordProperty.of(p -> p)._toProperty();
}
// todo 可扩展其他类型
else {
property = Property.of(p -> p);
}
return property;
}
/**
* 将实体对象列表转换为Elasticsearch批量新增或修改列表
*
* @param list 实体对象列表
* @param idField 主键字段反射对象,用于获取实体对象的ID值
* @return 批量新增或修改列表
* @throws IllegalAccessException 当无法访问字段时抛出
*/
private List<BulkOperation> buildIndexOperations(List<T> list, Field idField) throws IllegalAccessException {
// 构建批量操作列表
List<BulkOperation> operations = new ArrayList<>();
for (T t : list) {
String id = idField == null ? null : idField.get(t).toString();
IndexOperation<T> operation = IndexOperation.of(i -> {
i.index(indexName).document(t);
if (id != null) {
i.id(id);
}
return i;
});
operations.add(operation._toBulkOperation());
}
return operations;
}
}
5. 调用示例
5.1 服务接口(SkuInfoService)
import TableDataInfo;
/**
* 商品搜索服务Service
*/
public interface SkuInfoService {
/**
* 导入商品数据到 es
*
*/
void addAll();
/**
* 多条件匹配查询
*
* @param skuInfo 查询条件
* @param pageNum 当前页码
* @param pageSize 每页显示条数
* @return 匹配分页结果
*/
TableDataInfo searchByMultiField(SkuInfo skuInfo, Integer pageNum, Integer pageSize);
}
5.2 服务实现类(SkuInfoServiceImpl)
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.BulkResponse;
import com.alibaba.fastjson.JSON;
import TableDataInfo;
import service.ElasticService;
import SkuInfo;
import service.SkuInfoService;
import ShopService;
import Sku;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* 商品搜索服务ServiceImpl
*/
@Service
public class SkuInfoServiceImpl implements SkuInfoService {
private static final Logger log = LoggerFactory.getLogger(SkuInfoServiceImpl.class);
@Resource
private ShopService shopService;
@Resource
private ElasticsearchClient esClient;
private ElasticService<SkuInfo> elasticService;
@PostConstruct
public void init() {
this.elasticService = new ElasticService<>(esClient, SkuInfo.class);
}
/**
* 导入商品数据到 es
*/
@Override
public void addAll() {
// 限定批次为1000
int batchSize = 1000;
// 查询商品页数
int page = (shopService.selectCount() / batchSize) + 1;
log.info("商品总页数:{}", page);
// 分批查询商品
for (int i = 1; i <= page; ++i) {
List<Sku> list = shopService.selectList(i, batchSize);
log.info("开始查询第{}页数据,查询结果数量={}", i, list.size());
// 转换格式
List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(list), SkuInfo.class);
// 调用批量更新或保存方法
BulkResponse bulkResponse = elasticService.bulkIndex(skuInfos);
if (bulkResponse == null || bulkResponse.errors()) {
log.error("批量更新或保存失败:{}", bulkResponse);
}
}
}
/**
* 多条件匹配查询
*
* @param searchMap查询条件
* @param pageNum 当前页码
* @param pageSize 每页显示条数
* @return 匹配分页结果
*/
@Override
public TableDataInfo searchByMultiField(Map<String, String> searchMap
, Integer pageNum, Integer pageSize) {
return elasticService.searchByMultiField(searchMap, pageNum, pageSize);
}
}
4317

被折叠的 条评论
为什么被折叠?



