Elasticsearch

如何将数据库的数据上传到Elasticsearch 【ES】里面

一、首先我们先了解ES

1.  应用场景
  • 独立数据库系统(存储非结构化 / 半结构化数据)
  • 搭建全文搜索系统(常与 MySQL 配合,MySQL 存业务数据,ES 存检索数据)
  • 搭建日志分析系统(如 ELK 栈)
  • 搭建实时数据分析系统(支持复杂聚合查询)
2. 倒排索引原理
  1. 定义:不同于数据库的 “正排索引”(按文档 ID 查内容),倒排索引是 “按内容查文档 ID”。例如:将文档中的 “关键词” 作为索引键,关联包含该关键词的所有文档 ID 及位置信息。
  2. 结构:由 “词项词典(Term Dictionary)” 和 “倒排表(Posting List)” 组成:
    • 词项词典:存储所有去重后的关键词(如 “专辑”“音乐”)。
    • 倒排表:每个关键词对应包含它的文档 ID 列表(如 “专辑”→ [1,3,5])。
3. 倒排索引的查询过程
  1. 用户输入---> 解析查询 ---> 词项归一化(构建和查询都要归一) ---> 倒排列表查找 --->

合并结果 ---> 计算相关得分 ---> 排序 ---> 返回最终结果

4.  优缺点
  1. 优点: 高效的关键词搜索,可扩展性,灵活的查询能力

  2. 缺点: 存储空间占用较大,准实时性较弱

5. 其他语法什么的就省略了

二、在Java Api操作ES

1. 引入依赖
<!-- Spring Data Elasticsearch  starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. 定义 ES 实体类(映射文档)
  • @Document指定索引名(相当于数据库的 “表”);
  • @Id绑定文档唯一标识(对应数据库主键);
  • @Field指定字段类型、分词器等(如text类型支持分词,keyword类型不分词)。
@Data
@Document(indexName = "albuminfo") // 索引名(小写,不能有特殊字符)
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略JSON中未定义的字段,避免转换报错
public class AlbumInfoIndex implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id // 绑定ES文档的_id(对应数据库的album_id)
    private Long id;

    // 分词字段:类型text,使用ik_max_word分词器(最大化分词)
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String albumTitle; // 专辑标题(需要全文检索)

    // 不分词字段:类型keyword,用于精确匹配(如筛选)
    @Field(type = FieldType.Keyword)
    private String authorName; // 作者名(无需分词,支持精确查询)

    // 数值型字段:用于范围查询(如价格、时间)
    @Field(type = FieldType.Long)
    private Long publishTime; // 发布时间戳
}
3. 通过Spring Data Elasticsearch提供的CrudRepositoryElasticsearchRepository简化 CRUD 操作:
  • 根据业务需求在这个接口定义方法 访问权限是public,默认不用管。方法名必须是findXXX()或者getxxx(),确定好方法的返回值类型,不需要实现,就可以被调用

@Repository 注解标识该接口为数据访问层组件,供 Spring 自动扫描管理并处理数据访问异常;继承 CrudRepository 是为了直接复用其内置的增删改查方法,无需手动实现基础数据操作,简化与 Elasticsearch 的交互。

@Repository // 标识为数据访问层组件
public interface AlbumInfoIndexRepository extends CrudRepository<AlbumInfoIndex,Long> {
  // 继承后直接使用内置方法:save(单条保存)、saveAll(批量保存)、findById(查询)、delete(删除)等
}
4. 复杂查询(构建 DSL)

对于高级查询(如模糊匹配、范围筛选、聚合),需手动构建 DSL 语句(ES 的查询语法):

SearchRequest.Builder  searchRequestBuilder = new SearchRequest.Builder();
        SearchRequest searchRequest = searchRequestBuilder
                .index("my_index")
                .query(q -> q.match(b -> b.field("title").query("华为")))
                .build();
5. 检索
SearchResponse<Object> search = elasticsearchClient.search(searchRequest, Object.class);
6. 解析数据

根据检索生成的响应体,一步步解析出自己需要的数据:例如简单的

//        3.解析数据
        List<Hit<Object>> hits = search.hits().hits();
        for (Hit<Object> hit : hits) {
            System.out.println(hit.source());
        }

简单的ES使用思路我们就讲完了下面是关于,如何将数据库的数据提供给ES里

三、上传数据到ES里面

 1. 业务流程设计(基于分层架构与异步通信)

在分布式系统中,数据上传(如同步到 Elasticsearch)的流程需遵循分层架构思想,并通过异步通信提升效率,具体拆解如下:

(1)分层职责划分

  • Service 接口:定义业务规范,明确 “数据上传” 需包含的入参(如专辑 ID)和预期结果(无返回值或成功标识),仅声明 “要做什么”。
  • ServiceImpl 实现类:作为接口的具体落地,负责协调资源完成核心逻辑。例如,在 “专辑上架” 场景中,ServiceImpl 先完成数据库存储(主业务),再通过消息队列触发异步数据上传(非主业务)。

(2)异步通信设计(基于 RabbitMQ)

由于数据上传(如同步到 ES)无需实时性,为避免阻塞主流程(如用户等待页面响应),采用 “生产者 - 消费者” 模式异步处理:

  • 上游服务(生产者):在 ServiceImpl 中完成主业务(从MySQL读取数据)后,将数据上传所需信息(如专辑 ID)封装为消息,发送至 RabbitMQ 的指定交换机和队列,随即返回主流程结果(不等待上传完成)。
  • 下游服务(消费者):监听对应队列,获取消息后异步执行数据上传逻辑(获取上发的消息,解析消息获取专辑id,再从 MySQL 查询专辑详情,转换为 ES 文档并保存)。

通过这种设计,主业务流程无需等待数据上传完成即可继续执行其他任务,既提升了用户体验(减少等待时间),又通过消息队列实现了上下游服务的解耦(上游无需关心下游如何处理,只需确保消息正确发送),同时提高了系统的并发处理能力。

2. Elasticsearch 文档实体类设计

需定义与 ES 索引映射的实体类,明确字段类型、分词策略等,确保检索效率。以专辑数据为例:

@Data
@Document(indexName = "albuminfo")  // 绑定ES索引名(相当于数据库表名)
@JsonIgnoreProperties(ignoreUnknown = true) //目的:防止json字符串转成实体对象时因未识别字段报错
public class AlbumInfoIndex implements Serializable {

    private static final long serialVersionUID = 1L;

    // 专辑Id
    @Id        // 绑定ES文档唯一标识(对应数据库主键)
    private Long id;

    //  es 中能分词的字段,这个字段数据类型必须是 text!keyword 不分词! analyzer = "ik_max_word"
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String albumTitle;

    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String albumIntro;

    @Field(type = FieldType.Keyword)
    private String announcerName;

    //专辑封面
    @Field(type = FieldType.Keyword, index = false)
    private String coverUrl;

    //专辑包含声音总数
    @Field(type = FieldType.Long, index = false)
    private Integer includeTrackCount;

    //专辑是否完结:0-否;1-完结
    @Field(type = FieldType.Long, index = false)
    private String isFinished;

    //付费类型:免费、vip免费、付费
    @Field(type = FieldType.Keyword, index = false)
    private String payType;

    @Field(type = FieldType.Date,format = DateFormat.date_time, pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime; //

    @Field(type = FieldType.Long)
    private Long category1Id;

    @Field(type = FieldType.Long)
    private Long category2Id;

    @Field(type = FieldType.Long)
    private Long category3Id;

    //播放量
    @Field(type = FieldType.Integer)
    private Integer playStatNum = 0;

    //订阅量
    @Field(type = FieldType.Integer)
    private Integer subscribeStatNum = 0;

    //购买量
    @Field(type = FieldType.Integer)
    private Integer buyStatNum = 0;

    //评论数
    @Field(type = FieldType.Integer)
    private Integer commentStatNum = 0;

    //  商品的热度!
    @Field(type = FieldType.Double)
    private Double hotScore = 0d;

    //专辑属性值
    // Nested 支持嵌套查询
    @Field(type = FieldType.Nested)
    private List<AttributeValueIndex> attributeValueIndexList;

}

设计要点

  • text类型 + 分词器:用于需要全文检索的字段(如标题、简介);
  • keyword类型:用于精确匹配的字段(如主播名、分类);
  • index=false:非检索字段关闭索引,减少存储和维护成本;
  • 嵌套类型(Nested):处理复杂结构(如专辑属性列表),支持嵌套查询。
2. 前端接口暴露(Controller 层)

Controller 作为前端入口,负责接收请求、参数校验,并调用 Service 层处理业务,不涉及具体逻辑:(对信息进行增删改的时候也对ES里面的数据进行增删改)下面是对保存做示例

@Tag(name = "专辑管理")
@RestController
@RequestMapping("api/album/albumInfo")
@SuppressWarnings({"unchecked", "rawtypes"})// 抑制泛型相关警告(如Result<T>的类型推断)
public class AlbumInfoApiController {

    @Autowired
    private AlbumInfoService albumInfoService;

    @PostMapping("/saveAlbumInfo")
    @CheckLogin // 自定义登录校验注解:触发切面拦截,验证前端请求是否携带有效Token,及Token是否被篡改(如签名验证);未通过则拦截请求,返回未登录或Token无效提示
    public Result saveAlbumInfo(@RequestBody AlbumInfoVo albumInfoVo) {
         albumInfoService.saveAlbumInfo(albumInfoVo);
         return Result.ok();
    }
3.RabbitMQ 核心配置解析(Spring Boot 环境)

通过配置确保消息从生产者到消费者的可靠传递,核心配置如下:


spring:
  rabbitmq:
  # 1. 基础连接配置
  host: 192.1xx.xxx.xxx  # RabbitMQ服务器IP地址
  port: 5672           # 通信端口(默认5672,管理界面端口为15672)
  username: xxxx      # 登录用户名xxx
  password: xxxx      # 登录密码xxxx

  # 2. 生产者可靠性配置(确保消息正确发送至MQ)
  publisher-confirm-type: CORRELATED  # 开启生产者确认机制(类型:关联回调)
  publisher-returns: true             # 开启消息路由失败返回机制

  # 3. 消费者可靠性配置(确保消息被正确处理)
  listener:
    simple:
      acknowledge-mode: manual  # 消费者确认模式:手动确认
      prefetch: 1               # 预取计数:单次最多处理1条未确认消息


配置目的

  • 生产者:通过publisher-confirmpublisher-returns确保消息到达交换机并正确路由,失败时可重试;
  • 消费者manual确认避免消息未处理完即丢失;prefetch=1实现负载均衡(消息均匀分配给消费者)。
5. 生产者消息发送(RabbitService)

封装消息发送工具类,实现消息构建、发送及可靠性保障(结合 Redis 记录消息):

package com.atguigu.tingshu.common.rabbit.service;


@Service
public class RabbitService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     *  发送消息
     * @param exchange 交换机
     * @param routingKey 路由键
     * @param message 消息
     */
    public boolean sendMessage(String exchange, String routingKey, Object message) {
        //1.创建自定义相关消息对象-包含业务数据本身,交换器名称,路由键,队列类型,延迟时间,重试次数
        GuiguCorrelationData correlationData = new GuiguCorrelationData();
        String uuid = "mq:" + UUID.randomUUID().toString().replaceAll("-", "");
        correlationData.setId(uuid);
        correlationData.setMessage(message);
        correlationData.setExchange(exchange);
        correlationData.setRoutingKey(routingKey);
        //2.将相关消息封装到发送消息方法中

        rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData);

        //3.将相关消息存入Redis  Key:UUID  相关消息对象  10 分钟
        redisTemplate.opsForValue().set(uuid, JSON.toJSONString(correlationData), 10, TimeUnit.MINUTES);
        return true;
    }

    /**
     * 发送延迟消息方法
     * @param exchange 交换机
     * @param routingKey 路由键
     * @param message 消息数据
     * @param delayTime 延迟时间,单位为:秒
     */
    public boolean sendDealyMessage(String exchange, String routingKey, Object message, int delayTime) {
        //1.创建自定义相关消息对象-包含业务数据本身,交换器名称,路由键,队列类型,延迟时间,重试次数
        GuiguCorrelationData correlationData = new GuiguCorrelationData();
        String uuid = "mq:" + UUID.randomUUID().toString().replaceAll("-", "");
        correlationData.setId(uuid);
        correlationData.setMessage(message);
        correlationData.setExchange(exchange);
        correlationData.setRoutingKey(routingKey);
        correlationData.setDelay(true);
        correlationData.setDelayTime(delayTime);

        //2.将相关消息封装到发送消息方法中
        rabbitTemplate.convertAndSend(exchange, routingKey, message,message1 -> {
            message1.getMessageProperties().setDelay(delayTime*1000);
            return message1;
        }, correlationData);

        //3.将相关消息存入Redis  Key:UUID  相关消息对象  10 分钟
        redisTemplate.opsForValue().set(uuid, JSON.toJSONString(correlationData), 10, TimeUnit.MINUTES);
        return true;

    }

}

核心逻辑

  • GuiguCorrelationData封装消息元数据(唯一 ID、交换机、路由键等),便于回调时关联;
  • 消息发送后存入 Redis,用于确认消息是否到达交换机(结合ConfirmCallback);
  • 延迟消息通过setDelay设置延迟时间,适配定时同步场景。
6. 生产者业务层调用(触发消息发送)

在 ServiceImpl 中完成主业务后,调用RabbitService发送消息,触发下游数据上传:

rabbitService.sendMessage(MqConst.EXCHANGE_ALBUM,MqConst.ROUTING_ALBUM_UPPER,albumInfo.getId().toString());
            log.info("上游专辑微服务发送消息:{}进行专辑的上架完成...",albumInfo.getId());
6. 队列、交换机及绑定关系的自动配置(程序启动时完成)
(1)队列 / 交换机自动声明

通过@RabbitListener(bindings = @QueueBinding(...))注解,Spring AMQP 会在程序启动时自动向 RabbitMQ 服务器声明队列、交换机,并建立绑定关系,无需手动在 RabbitMQ 控制台配置。

@Component
@Slf4j
public class SearchInfoReceiver {

    @Autowired
    private MqOpsService mqOpsService; // 数据上传业务类
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 监听专辑上架消息,同步数据到ES
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(
            value = MqConst.QUEUE_ALBUM_UPPER,  // 队列名
            durable = "true",  // 持久化(重启不丢失)
            exclusive = "false",  // 非排他(允许多消费者)
            autoDelete = "false"  // 不自动删除(无消费者时保留)
        ),
        exchange = @Exchange(value = MqConst.EXCHANGE_ALBUM),  // 交换机名(与生产者一致)
        key = MqConst.ROUTING_ALBUM_UPPER  // 路由键(与生产者发送时一致)
    ))
    @SneakyThrows
    public void upperAlbum(String content, Channel channel, Message message) {
        // 消息消费逻辑(见下文)
    }
}
(2)消息消费与重试机制

消费者接收消息后,执行数据上传,并通过手动确认和重试机制保证可靠性:

public void upperAlbum(String content, Channel channel, Message message) {
     log.info("下游账户微服务监听到{}队列的消息:{},准备开始消费", MqConst.QUEUE_ALBUM_UPPER, content);
        // 1.判断消息
        if (StringUtils.isEmpty(content)){
            return;
        }
//        2.获取消息以及消息属性
        String msg = content;
        MessageProperties messageProperties = message.getMessageProperties();
        long deliveryTag = messageProperties.getDeliveryTag();
//        3.消费消息
        try {
            mqOpsService.upperAlbum(msg);
//        4.手动应答
            channel.basicAck(deliveryTag, false);//false是否批量签收,
        } catch (GuiguException e) {
            //  消费消息出现了异常,可以进行3次重试。计数器:内存中的变量(int count ++) 分布式变量++(incr)就充当计数器

            String msgRetryKey = "msg:retry:" + content;
            Long count = redisTemplate.opsForValue().increment(msgRetryKey);//分布式变量   ,原子性递增
            if(count>3){  // 1.发送告警平台 2.写入到错误日志文件:公司实际做法
                log.error("消息:{} 已经达到了最大重试次数,{}",content,(count-1));
                channel.basicNack(deliveryTag,false,false);//1.否批量签收,2.否进行 requeue
            }else{
                log.info("消息:{} 进行第 {} 次重试...",content,(count));
                channel.basicNack(deliveryTag,false,true);
            }
        }
}

消费者通过调用mqOpsService.upperAlbum(msg);来实现,

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class SearchServiceImpl implements SearchService {
    @Autowired
    private AlbumInfoIndexRepository albumInfoIndexRepository;
@Override
    public void upperAlbum(Long albumId) {
。。。。。。。。。。。。。。。。组装消息。。。。。。。。。。。。。。。。。
        albumInfoIndexRepository.save(albumInfoIndex);
        log.info("专辑{}同步到ElasticSearch成功!", albumId);

    }

}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

榨菜U

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值