【云岚到家】-day04-2-索引同步-搜索接口
- [1 索引同步](#1 索引同步)
-
- [1.1 编写同步程序](#1.1 编写同步程序)
-
- [1.1.1 创建索引结构](#1.1.1 创建索引结构)
- [1.1.2 编写同步程序](#1.1.2 编写同步程序)
-
- [1.1.2.1 添加依赖](#1.1.2.1 添加依赖)
- [1.1.2.2 配置连接ES](#1.1.2.2 配置连接ES)
- [1.1.2.3 编写同步程序](#1.1.2.3 编写同步程序)
- [1.1.3 测试](#1.1.3 测试)
- [1.1.4 小结](#1.1.4 小结)
-
- [1.1.4.1 如何保证Canal+MQ同步消息的顺序性?](#1.1.4.1 如何保证Canal+MQ同步消息的顺序性?)
- [1.1.4.2 如何保证只有一个消费者接收消息?](#1.1.4.2 如何保证只有一个消费者接收消息?)
- [1.2 管理同步表](#1.2 管理同步表)
-
- [1.2.1 管理同步表需求](#1.2.1 管理同步表需求)
- [1.2.2 代码实现](#1.2.2 代码实现)
-
- [1.2.2.1 区域服务上架向serve_sync表添加记录](#1.2.2.1 区域服务上架向serve_sync表添加记录)
- [1.2.2.2 区域服务下架从serve_sync表删除记录](#1.2.2.2 区域服务下架从serve_sync表删除记录)
- [1.2.2.3 修改服务项修改serve_sync的记录](#1.2.2.3 修改服务项修改serve_sync的记录)
- [1.2.2.4 修改服务分类修改serve_sync的记录](#1.2.2.4 修改服务分类修改serve_sync的记录)
- [1.2.3 测试](#1.2.3 测试)
- [2 搜索接口](#2 搜索接口)
-
- [2.1 定义接口](#2.1 定义接口)
- [2.2 搜索方法](#2.2 搜索方法)
- [2.3 service](#2.3 service)
- [2.4 controller](#2.4 controller)
- [2.5 测试](#2.5 测试)
- [3 完善搜索接口-实战](#3 完善搜索接口-实战)
-
- [3.1 完善es搜索条件](#3.1 完善es搜索条件)
- [3.2 测试](#3.2 测试)
1 索引同步
1.1 编写同步程序
刚才通过配置Canal+MQ的数据同步环境实现了Canal从数据库读取binlog并且将数据写入MQ。
下边编写同步程序监听MQ,收到消息后向ES创建索引。
1.1.1 创建索引结构
启动ES和kibana:
安装完成后进行启动:
shell
docker start elasticsearch7.17.7
docker start kibana7.17.7
下边创建索引serve_aggregation,serve_aggregation索引的结构与jzo2o-foundations数据库的serve_sync表结构对应。
首先通过下边的命令查询索引
elm
GET /_cat/indices?v
如果需要修改索引结构需要删除重新创建:
elm
DELETE 索引名
查询索引结构
elm
GET /索引名/_mapping
因为我们canal同步的是serve_sync 表,所以按照serve_sync表创建serve_aggregation 索引 (已经存在无法重复创建)
elm
PUT /serve_aggregation
{
"mappings" : {
"properties" : {
"city_code" : {
"type" : "keyword"
},
"detail_img" : {
"type" : "text",
"index" : false
},
"hot_time_stamp" : {
"type" : "long"
},
"id" : {
"type" : "keyword"
},
"is_hot" : {
"type" : "short"
},
"price" : {
"type" : "double"
},
"serve_item_icon" : {
"type" : "text",
"index" : false
},
"serve_item_id" : {
"type" : "keyword"
},
"serve_item_img" : {
"type" : "text",
"index" : false
},
"serve_item_name" : {
"type" : "text",
"analyzer": "ik_max_word",
"search_analyzer":"ik_smart"
},
"serve_item_sort_num" : {
"type" : "short"
},
"serve_type_icon" : {
"type" : "text",
"index" : false
},
"serve_type_id" : {
"type" : "keyword"
},
"serve_type_img" : {
"type" : "text",
"index" : false
},
"serve_type_name" : {
"type" : "text",
"analyzer": "ik_max_word",
"search_analyzer":"ik_smart"
},
"serve_type_sort_num" : {
"type" : "short"
}
}
}
}
1.1.2 编写同步程序
1.1.2.1 添加依赖
首先在foundations工程添加下边的依赖
xml
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-canal-sync</artifactId>
</dependency>
<dependency>
<groupId>com.jzo2o</groupId>
<artifactId>jzo2o-es</artifactId>
</dependency>
1.1.2.2 配置连接ES
修改foundations的配置文件:
修改nacos中es的配置文件
修改nacos中rabbitmq的配置文件
1.1.2.3 编写同步程序
创建com.jzo2o.foundations.handler.ServeCanalDataSyncHandler类,同步程序继承AbstractCanalRabbitMqMsgListener类,泛型中指定同步表对应的类型。
不仅可以同步mysql与es,也可以同步mysql和redis
根据数据同步环境去配置监听MQ:
java
@Component
public class ServeCanalDataSyncHandler extends AbstractCanalRabbitMqMsgListener<ServeSync> {
@Resource
private ElasticSearchTemplate elasticSearchTemplate;
//@RabbitListener(queues = "canal-mq-jzo2o-foundations", concurrency = "1")
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "canal-mq-jzo2o-foundations"),
exchange = @Exchange(name = "exchange.canal-jzo2o", type = ExchangeTypes.TOPIC),
key = "canal-mq-jzo2o-foundations"),
concurrency = "1"
)
public void onMessage(Message message) throws Exception {
}
concurrency = "1"
:表示消费线程数为1。
AbstractCanalRabbitMqMsgListener
是jzo2o-canal-sync模块中的,在同步程序中需要根据业务需求编写同步方法,当服务下架时会删除索引需要重写抽象类中的batchDelete(List<Long> ids)
方法,此方法是当删除Serve_sync表的记录时 对索引执行删除操作。
当服务上架后需要添加索引,当服务信息修改时需要修改索引,需要重写抽象类中的batchSave(List<ServeSync> data)
方法,此方法是当向Serve_sync表新增或修改记录时对索引执行添加及修改操作。
完整代码:
java
@Component
public class ServeCanalDataSyncHandler extends AbstractCanalRabbitMqMsgListener<ServeSync> {
@Resource
private ElasticSearchTemplate elasticSearchTemplate;
/**
* 监听canal-mq-jzo2o-foundations队列
* @param message
* @throws Exception
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "canal-mq-jzo2o-foundations"),
exchange = @Exchange(name = "exchange.canal-jzo2o", type = ExchangeTypes.TOPIC),
key = "canal-mq-jzo2o-foundations"),
concurrency = "1"
)
public void onMessage(Message message) throws Exception {
//解析消息后才会调用batchSave或batchDelete方法
parseMsg(message);
}
/**
* 向es中批量保存数据,解析binlog中的add、update都会调用此方法
* @param data
*/
@Override
public void batchSave(List<ServeSync> data) {
Boolean aBoolean = elasticSearchTemplate.opsForDoc().batchInsert(IndexConstants.SERVE, data);
if(!aBoolean){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
throw new RuntimeException("同步失败");
}
}
/**
* 向es中批量删除数据,解析binlog中的delete都会调用此方法
* @param ids
*/
@Override
public void batchDelete(List<Long> ids) {
Boolean aBoolean = elasticSearchTemplate.opsForDoc().batchDelete(IndexConstants.SERVE, ids);
if(!aBoolean){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
throw new RuntimeException("同步失败");
}
}
}
1.1.3 测试
启动jzo2o-foundations服务。
启动成功,jzo2o-foundations服务作为MQ的消费者和MQ建立通道,进入canal-mq-jzo2o-foundations队列的管理界面,查看是否建立了监听通道。
监听通道建立成功,下边在同步程序打断点:
手动修改jzo2o-foundations数据库的serve_sync表的记录,这里修改了服务项的名称
正常执行同步程序:
放行继续执行到batchSave方法:
保证ES服务正常,放行后吗,同步方法执行成功后进入Kibana查看
elm
GET /serve_aggregation/_search
{
}
查询服务信息与数据库serve_sync表中1686352662791016449记录的信息一致。
下边再将服务项名称恢复。
再进入Kibana查看索引的内容与数据库一致
1.1.4 小结
编写同步程序的步骤:
- 根据数据库同步表的结构,创建索引结构。
- 同步程序监听MQ的同步队列
- 同步程序收到数据同步消息写入Elasticsearch,写的失败抛出异常,消息回到MQ。
1.1.4.1 如何保证Canal+MQ同步消息的顺序性?
场景:
如下图:
首先明确Canal解析binlog日志信息按顺序发到MQ的队列中,现在是要保证消费端如何按顺序消费队列中的消息。
生产中同一个jzo2o-foundations服务会启动多个jvm进程,每个进程作为canal-mq-jzo2o-foundations的消费者,如下图:
现在对服务名称先修改为aa再修改为bb,在MQ中的有两个消息:
修改服务名称为aa
修改服务名称为bb
预期:最终将服务名称修改为bb
此时两条消息会被分发给两个jvm进程,假设"修改服务名称为aa"的消息发给jvm进程1,"修改服务名称为bb"的消息发给jvm进程2,两个进程分别去消费,此时无法控制两个消息的先后顺序,可能导致服务名称最终并非修改为bb。
解决方法:
多个jvm进程监听同一个队列保证只有消费者活跃,即只有一个消费者接收消息。
消费队列中的数据使用单线程。
1.1.4.2 如何保证只有一个消费者接收消息?
把原来的删了,队列需要增加x-single-active-consumer参数,表示否启用单一活动消费者模式。
配置完成查保证队列上存在SAC标识,如下图:
当有多个jvm进程都去监听该队列时,只有一个为活跃状态
如果使用x-single-active-consumer参数需要修改为如下代码:
在Queue中添加:
java
arguments={@Argument(name="x-single-active-consumer", value = "true", type = "java.lang.Boolean") }
如下所示:
java
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "canal-mq-jzo2o-foundations",arguments={@Argument(name="x-single-active-consumer", value = "true", type = "java.lang.Boolean") }),
exchange = @Exchange(name="exchange.canal-jzo2o",type = ExchangeTypes.TOPIC),
key="canal-mq-jzo2o-foundations"),
concurrency="1"
)
public void onMessage(Message message) throws Exception{
parseMsg(message);
}
concurrency="1"
表示保证一个jvm中消费线程为1。
arguments={@Argument(name="x-single-active-consumer", value = "true", type = "java.lang.Boolean") }
保证mq队列只让一个jvm干活
两个参数共同的保证了同步消息的顺序性。
1.2 管理同步表
通过测试Canal+MQ同步流程,只有当serve_sync表变化时才会触发同步,serve_sync表什么时候变化 ?
当服务信息变更时需要同时修改serve_sync表,下边先分析serve_sync的变化需求,再进行代码实现。
1.2.1 管理同步表需求
现在如何去维护serve_sync这张表呢?
根据serve_sync表的结构分析:
添加:区域服务上架向serve_sync表添加记录,同步程序新增索引记录。
删除:区域服务下架从serve_sync表删除记录,同步程序删除索引记录。
修改:
修改服务项修改serve_sync的记录。
修改服务分类修改serve_sync的记录。
修改服务价格修改serve_sync的记录。
设置热门/取消热门修改serve_sync的记录。
1.2.2 代码实现
1.2.2.1 区域服务上架向serve_sync表添加记录
在ServeServiceImpl增加私有方法,如下:
java
/**
* 新增服务同步数据
*
* @param serveId 服务id
*/
private void addServeSync(Long serveId) {
//服务信息
Serve serve = baseMapper.selectById(serveId);
//区域信息
Region region = regionMapper.selectById(serve.getRegionId());
//服务项信息
ServeItem serveItem = serveItemMapper.selectById(serve.getServeItemId());
//服务类型
ServeType serveType = serveTypeMapper.selectById(serveItem.getServeTypeId());
ServeSync serveSync = new ServeSync();
serveSync.setServeTypeId(serveType.getId());
serveSync.setServeTypeName(serveType.getName());
serveSync.setServeTypeIcon(serveType.getServeTypeIcon());
serveSync.setServeTypeImg(serveType.getImg());
serveSync.setServeTypeSortNum(serveType.getSortNum());
serveSync.setServeItemId(serveItem.getId());
serveSync.setServeItemIcon(serveItem.getServeItemIcon());
serveSync.setServeItemName(serveItem.getName());
serveSync.setServeItemImg(serveItem.getImg());
serveSync.setServeItemSortNum(serveItem.getSortNum());
serveSync.setUnit(serveItem.getUnit());
serveSync.setDetailImg(serveItem.getDetailImg());
serveSync.setPrice(serve.getPrice());
serveSync.setCityCode(region.getCityCode());
serveSync.setId(serve.getId());
serveSync.setIsHot(serve.getIsHot());
serveSyncMapper.insert(serveSync);
}
修改服务上架的方法:
java
@Override
@Transactional
@CachePut(value = RedisConstants.CacheName.SERVE, key = "#id", cacheManager = RedisConstants.CacheManager.ONE_DAY)
public Serve onSale(Long id){
...
//添加同步表
addServeSync(id);
return baseMapper.selectById(id);
}
1.2.2.2 区域服务下架从serve_sync表删除记录
1.2.2.3 修改服务项修改serve_sync的记录
在com.jzo2o.foundations.service.impl.ServeItemServiceImpl#update中
java
@Override
@Transactional
@CachePut(value = RedisConstants.CacheName.SERVE_ITEM, key = "#id", unless = "#result.activeStatus != 2", cacheManager = RedisConstants.CacheManager.ONE_DAY)
public ServeItem update(Long id, ServeItemUpsertReqDTO serveItemUpsertReqDTO) {
//1.更新服务项
ServeItem serveItem = BeanUtil.toBean(serveItemUpsertReqDTO, ServeItem.class);
serveItem.setId(id);
baseMapper.updateById(serveItem);
//2.同步数据到es
ServeSyncUpdateReqDTO serveSyncUpdateReqDTO = BeanUtil.toBean(serveItemUpsertReqDTO, ServeSyncUpdateReqDTO.class);
serveSyncUpdateReqDTO.setServeItemName(serveItemUpsertReqDTO.getName());
serveSyncUpdateReqDTO.setServeItemImg(serveItemUpsertReqDTO.getImg());
serveSyncUpdateReqDTO.setServeItemIcon(serveItemUpsertReqDTO.getServeItemIcon());
serveSyncUpdateReqDTO.setServeItemSortNum(serveItemUpsertReqDTO.getSortNum());
serveSyncService.updateByServeItemId(id, serveSyncUpdateReqDTO);
//用于更新缓存
return baseMapper.selectById(id);
}
1.2.2.4 修改服务分类修改serve_sync的记录
在com.jzo2o.foundations.service.impl.ServeTypeServiceImpl#update中
java
@Override
public void update(Long id, ServeTypeUpsertReqDTO serveTypeUpsertReqDTO) {
//1.更新服务类型
ServeType serveType = BeanUtil.toBean(serveTypeUpsertReqDTO, ServeType.class);
serveType.setId(id);
baseMapper.updateById(serveType);
//2.同步数据到es
ServeSyncUpdateReqDTO serveSyncUpdateReqDTO = new ServeSyncUpdateReqDTO();
serveSyncUpdateReqDTO.setServeTypeName(serveTypeUpsertReqDTO.getName());
serveSyncUpdateReqDTO.setServeTypeImg(serveTypeUpsertReqDTO.getImg());
serveSyncUpdateReqDTO.setServeTypeIcon(serveTypeUpsertReqDTO.getServeTypeIcon());
serveSyncUpdateReqDTO.setServeTypeSortNum(serveTypeUpsertReqDTO.getSortNum());
serveSyncService.updateByServeTypeId(id, serveSyncUpdateReqDTO);
}
1.2.3 测试
启动gateway、public、foundations、admin前端
在服务项中添加码农洗车
启用之后,再让某个区域添加该服务后上架,就会让canal同步到es,点击启用后让北京市添加进去并且上架
查看es,成功查询到码农洗车
2 搜索接口
2.1 定义接口
参数内容:区域编码,服务类型id、关键字
区域编码:用户定位成功前端记录区域编码(city_code),搜索时根据city_code搜索该区域的服务。
服务类型id:在全部服务界面选择一个服务类型查询其它下的服务列表。
关键字:输入关键字搜索服务项名称、服务类型名称。
接口名称:服务搜索接口
接口路径:GET/foundations/customer/serve/search
编写controller方法:
在com.jzo2o.foundations.controller.consumer.FirstPageServeController中
java
/**
* 首页服务搜索
* @param cityCode
* @param serveTypeId
* @param keyword
* @return
*/
@GetMapping("/search")
@ApiOperation("首页服务搜索")
@ApiImplicitParams({
@ApiImplicitParam(name = "cityCode", value = "城市编码", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "serveTypeId", value = "服务类型id", dataTypeClass = Long.class),
@ApiImplicitParam(name = "keyword", value = "关键词", dataTypeClass = String.class)
})
public List<ServeSimpleResDTO> findServeList(@RequestParam("cityCode") String cityCode,
@RequestParam(value = "serveTypeId", required = false) Long serveTypeId,
@RequestParam(value = "keyword", required = false) String keyword) {
return null;
}
2.2 搜索方法
首先通过ES的查询语言进行查询,如下
elm
GET /serve_aggregation/_search
{
"query" : {
"bool" : {
"must" : [
{
"term" : {
"city_code" : {
"value" : "010"
}
}
},
{
"multi_match" : {
"fields" : [ "serve_item_name", "serve_type_name" ],
"query" : "保洁"
}
}
]
}
},
"sort" : [
{
"serve_item_sort_num" : {
"order" : "asc"
}
}
]
}
2.3 service
下边按照ES查询语句编写service方法:
创建com.jzo2o.foundations.service.ServeAggregationService,定义service接口:
java
public interface ServeAggregationService {
/**
* 查询服务列表
*
* @param cityCode 城市编码
* @param serveTypeId 服务类型id
* @param keyword 关键词
* @return 服务列表
*/
List<ServeSimpleResDTO> findServeList(String cityCode, Long serveTypeId, String keyword);
}
service实现类
java
@Slf4j
@Service
public class ServeAggregationServiceImpl implements ServeAggregationService {
@Resource
private ElasticSearchTemplate elasticSearchTemplate;
/**
* 查询服务列表
*
* @param cityCode 城市编码
* @param serveTypeId 服务类型id
* @param keyword 关键词
* @return 服务列表
*/
@Override
public List<ServeSimpleResDTO> findServeList(String cityCode, Long serveTypeId, String keyword) {
// 构造查询条件
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.query(query->query.bool(bool->{
//匹配citycode
bool.must(must->
must.term(term->
term.field("city_code").value(cityCode)));
//todo 匹配服务类型
//匹配关键字
if(ObjectUtils.isNotEmpty(keyword)){
bool.must(must->
must.multiMatch(multiMatch->
multiMatch.fields("serve_item_name","serve_type_name").query(keyword)));
}
return bool;
}));
// 排序 按服务项的serveItemSortNum排序(升序)
List<SortOptions> sortOptions = new ArrayList<>();
sortOptions.add(SortOptions.of(sortOption -> sortOption.field(field->field.field("serve_item_sort_num").order(SortOrder.Asc))));
builder.sort(sortOptions);
//指定索引
builder.index("serve_aggregation");
//请求对象
SearchRequest searchRequest = builder.build();
// 检索数据
SearchResponse<ServeAggregation> searchResponse = elasticSearchTemplate.opsForDoc().search(searchRequest, ServeAggregation.class);
//如果搜索成功返回结果集
if (SearchResponseUtils.isSuccess(searchResponse)) {
List<ServeAggregation> collect = searchResponse.hits().hits()
.stream().map(hit -> {
ServeAggregation serve = hit.source();
return serve;
})
.collect(Collectors.toList());
List<ServeSimpleResDTO> serveSimpleResDTOS = BeanUtil.copyToList(collect, ServeSimpleResDTO.class);
return serveSimpleResDTOS;
}
return Collections.emptyList();
}
}
es新版本全部采用此方法编写查询语句。
2.4 controller
完善controller方法
java
@GetMapping("/search")
@ApiOperation("首页服务搜索")
@ApiImplicitParams({
@ApiImplicitParam(name = "cityCode", value = "城市编码", required = true, dataTypeClass = String.class),
@ApiImplicitParam(name = "serveTypeId", value = "服务类型id", dataTypeClass = Long.class),
@ApiImplicitParam(name = "keyword", value = "关键词", dataTypeClass = String.class)
})
public List<ServeSimpleResDTO> findServeList(@RequestParam("cityCode") String cityCode,
@RequestParam(value = "serveTypeId", required = false) Long serveTypeId,
@RequestParam(value = "keyword", required = false) String keyword) {
List<ServeSimpleResDTO> serveList = serveAggregationService.findServeList(cityCode, serveTypeId, keyword);
return serveList;
}
2.5 测试
启动public、customer、foundations、gateway、小程序
码农洗车赫然在列
搜索框输入保洁
测试成功
3 完善搜索接口-实战
3.1 完善es搜索条件
在com.jzo2o.foundations.service.impl.ServeAggregationServiceImpl#findServeList中匹配服务类型
java
@Override
public List<ServeSimpleResDTO> findServeList(String cityCode, Long serveTypeId, String keyword) {
// 构造查询条件
SearchRequest.Builder builder = new SearchRequest.Builder();
builder.query(query->query.bool(bool->{
//匹配citycode
bool.must(must->
must.term(term->
term.field("city_code").value(cityCode)));
//匹配服务类型
if(ObjectUtils.isNotEmpty(serveTypeId)){
bool.must(must->
must.term(term->
term.field("serve_type_id").value(serveTypeId)));
}
3.2 测试
非常完美