【云岚到家】-day04-2-索引同步-搜索接口

【云岚到家】-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 小结

编写同步程序的步骤:

  1. 根据数据库同步表的结构,创建索引结构。
  2. 同步程序监听MQ的同步队列
  3. 同步程序收到数据同步消息写入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 测试

非常完美

相关推荐
杉之3 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码23 分钟前
Spring Task 定时任务
java·前端·spring
俏布斯35 分钟前
算法日常记录
java·算法·leetcode
276695829240 分钟前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息41 分钟前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen1 小时前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
qr9j422331 小时前
elasticsearch 如果按照日期进行筛选
大数据·elasticsearch·jenkins
DavidSoCool1 小时前
es分页边界数据重复问题处理
大数据·elasticsearch·搜索引擎
松韬1 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
绝顶少年2 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端