如何将数据库的数据上传到Elasticsearch 【ES】里面
一、首先我们先了解ES
1. 应用场景
- 独立数据库系统(存储非结构化 / 半结构化数据)
- 搭建全文搜索系统(常与 MySQL 配合,MySQL 存业务数据,ES 存检索数据)
- 搭建日志分析系统(如 ELK 栈)
- 搭建实时数据分析系统(支持复杂聚合查询)
2. 倒排索引原理
- 定义:不同于数据库的 “正排索引”(按文档 ID 查内容),倒排索引是 “按内容查文档 ID”。例如:将文档中的 “关键词” 作为索引键,关联包含该关键词的所有文档 ID 及位置信息。
- 结构:由 “词项词典(Term Dictionary)” 和 “倒排表(Posting List)” 组成:
- 词项词典:存储所有去重后的关键词(如 “专辑”“音乐”)。
- 倒排表:每个关键词对应包含它的文档 ID 列表(如 “专辑”→ [1,3,5])。
3. 倒排索引的查询过程
-
用户输入---> 解析查询 ---> 词项归一化(构建和查询都要归一) ---> 倒排列表查找 --->
合并结果 ---> 计算相关得分 ---> 排序 ---> 返回最终结果
4. 优缺点
-
优点: 高效的关键词搜索,可扩展性,灵活的查询能力
-
缺点: 存储空间占用较大,准实时性较弱
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提供的CrudRepository或ElasticsearchRepository简化 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-confirm和publisher-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);
}
}
3056

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



