Spring 集成 ElasticSearch8.6.0(不使用 spring-boot-starter-data-elasticsearch)

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);
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值