sl-express-ms-search使用手册
文章目录
1.说明
该模块为搜索相关微服务,主要将ElasticSearch搜索技术相关的使用代码进行统一,所以其相关的服务功能均抽取到了本工程。
该工程主要提供ES搜索与数据同步服务,主要功能有:
- 快递员端搜索功能(搜索字段:运单号、姓名、电话)
- 新增/全量修改快递员任务
- 根据取派件id查询快递员任务
- mq消费者对快递员任务进行新增或更新
2.使用
2.1导入依赖
如需使用搜索微服务功能,需要引入搜索微服务依赖:
<dependency>
<groupId>com.sl-express.ms.search</groupId>
<artifactId>sl-express-ms-search-api</artifactId>
<version>1.1-SNAPSHOT</version>
</dependency>
2.2feign方法
@ApiIgnore
@FeignClient(value = "sl-express-ms-search", contextId = "CourierTask", path = "courierSearch")
public interface CourierTaskFeign {
/**
* 分页查询
*
* @param pageQueryDTO 分页查询条件
* @return 分页查询结果
*/
@PostMapping("pageQuery")
PageResponse<CourierTaskDTO> pageQuery(@RequestBody CourierTaskPageQueryDTO pageQueryDTO);
/**
* 新增快递员任务
*
* @param courierTaskDTO 快递员任务
*/
@PostMapping
void saveOrUpdate(@RequestBody CourierTaskDTO courierTaskDTO);
/**
* 根据取派件id查询快递员任务
*
* @param id 取派件id
* @return 快递员任务
*/
@GetMapping("/{id}")
CourierTaskDTO findById(@PathVariable("id") Long id);
}
2.3 es数据同步
2.3.1 es初始化同步数据
初始化同步数据功能已经做成测试用例,参考test目录下的 InitTest 代码学习,可直接运行进行初始化数据同步。
2.3.2 es数据新增与更新
当调用某个接口使得数据发生新增或者更新时,需要通过mq发送新增或更新消息到队列,然后搜索微服务中的mq消费者处理消息。 需要注意的是创建运单之后,需要将运单号同步到es当中,也是通过mq消息来异步通知的。创建运单的方法里已经发送了mq消息,消息格式已经确定,需要另外写消费者实现逻辑。
//发送消息,同步更新快递员任务
CourierTaskDTO courierTaskDTO = courierTaskFeign.findById(pickupDispatchTaskDTO.getId());
//构建快递员任务更新内容
..........................................
mqFeign.sendMsg(Constants.MQ.Exchanges.COURIER_TASK, Constants.MQ.RoutingKeys.COURIER_TASK_SAVE_OR_UPDATE, JSONUtil.toJsonStr(courierTaskDTO));
3.技术方案
3.1环境部署
该工程基于ES提供服务,所以使用前需进行环境部署。主要部署搜索引擎Elasticsearch、可视化操作平台kibana,项目中统一使用7.17.5版本。主要介绍通过docker部署的方式。
3.1.1 Elasticsearch部署
注意:虚拟机中elasticsearch服务和kibana服务已经部署完毕,但是不是开机自启动,自己使用docker命令启动即可。
- 部署命令
#拉取es镜像
docker pull elasticsearch:7.17.5
#运行es(注意如需kibana与es同一网络,还需提前创建docker网络)
docker run -d \
--name elasticsearch \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.17.5
- 启动成功页面
docker启动后访问:http://自己的ip:9200
,出现以下页面即为成功
3.1.2 kibana部署
- 部署命令
#拉取kibana镜像
docker pull kibana:7.17.5
#运行kibana
docker run -d \
--name kibana \
# 我们基于没有docker网络进行的部署,所以需要配置es的ip和端口
-e ELASTICSEARCH_HOSTS=http://es的ip:端口 \
-p 5601:5601 \
kibana:7.17.5
- 启动成功
启动成功后可以访问:http://你的ip:5601
3.1.3 ik分词器安装
拼音分词器安装同样操作,地址:https://github.com/medcl/elasticsearch-analysis-pinyin/releases/tag/v7.17.5
es自带的standard对中文的分词效果并不理想,所以我们需要安装ik分词器,更好的支持中文分词。
- 在线安装ik分词器
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.17.5.zip
#退出
exit
#重启容器
docker restart elasticsearch
- 离线安装
国内访问github很慢,在线安装大多数情况会失败,我们推荐离线安装
(1)查看es的plugins挂载的数据卷位置
docker volume inspect es-plugins
显示结果:
[
{
"CreatedAt": "2022-07-28T12:53:21+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data这个目录中。
(2)下载对应版本(7.17.5)的ik插件
https://github.com/medcl/elasticsearch-analysis-ik/releases
(3)上传插件到数据卷
首先将下载下来的压缩包解压,重命名为ik;然后将文件夹上传到(1)中查到的数据卷目录
拼音分词器的文件夹名称命名为
pinyin
(4)重启容器
docker restart elasticsearch
3.2 springboot集成es
springBoot集成es有三种方法:java api、rest client、data-es,对于java api这种方式,官方已经明确表示在ES 7.0版本中将弃用TransportClient客户端,同时在7.15版本以后,官方宣布弃用了RestHighLevelClient
所以我们只引用其他两种方式:
3.2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>7.17.5</version>
</dependency>
3.2.2 es-api文档
- es官方开发文档:
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/index.html
3.2.3 es数据同步
本项目中我们采用的数据同步技术方案为mq方式
- 技术架构图
- 同步操作
- 数据发生新增或更新发送mq消息
//发送消息,同步更新快递员任务
CourierTaskDTO courierTaskDTO = courierTaskFeign.findById(pickupDispatchTaskDTO.getId());
//构建快递员任务更新内容
..........................................
mqFeign.sendMsg(Constants.MQ.Exchanges.COURIER_TASK, Constants.MQ.RoutingKeys.COURIER_TASK_SAVE_OR_UPDATE, JSONUtil.toJsonStr(courierTaskDTO));
3.2.4 es数据初始化同步
初始化同步数据功能已经做成测试用例,参考test目录下的 InitTest 代码学习,可直接运行进行初始化数据同步。
package com.sl.ms.search;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.BulkRequest;
import com.sl.ms.oms.api.OrderFeign;
import com.sl.ms.oms.dto.OrderDTO;
import com.sl.ms.search.entity.CourierTaskEntity;
import com.sl.ms.work.api.PickupDispatchTaskFeign;
import com.sl.ms.work.api.TransportOrderFeign;
import com.sl.ms.work.domain.dto.PickupDispatchTaskDTO;
import com.sl.ms.work.domain.dto.TransportOrderDTO;
import com.sl.ms.work.domain.enums.pickupDispatchtask.PickupDispatchTaskStatus;
import com.sl.ms.work.domain.enums.pickupDispatchtask.PickupDispatchTaskType;
import com.sl.transport.common.exception.SLException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ES初始化数据
*
* @Author itcast
* @Create 2023/3/7 13:55
**/
@Slf4j
@SpringBootTest
class InitTest {
@Resource
private ElasticsearchClient client;
@Resource
private OrderFeign orderFeign;
@Resource
private PickupDispatchTaskFeign pickupDispatchTaskFeign;
@Resource
private TransportOrderFeign transportOrderFeign;
@Value("${sl.es.index_name}")
private String indexName;
/**
* 初始化同步数据到es
*/
@Test
void init() throws IOException {
// 1.删除所有文档(不需要删除的话,注释这部分代码)
client.deleteByQuery(d -> d
.index(indexName)
.query(q -> q
.matchAll(m -> m)));
// 2.查询全部取派件任务
List<PickupDispatchTaskDTO> items = pickupDispatchTaskFeign.findAll(null, null, null, null);
// 3.根据订单id列表批量查询订单
List<String> orderIds = items.stream().map(item -> item.getOrderId().toString()).collect(Collectors.toList());
List<OrderDTO> orderDTOS = orderFeign.findByIds(orderIds);
Map<Long, OrderDTO> map = orderDTOS.stream().collect(Collectors.toMap(OrderDTO::getId, dto -> dto));
// 4.数据封装为entity
List<CourierTaskEntity> entities = new ArrayList<>();
for (PickupDispatchTaskDTO item : items) {
// 封装快递员任务对象
CourierTaskEntity courierTaskEntity = this.getCourierTaskEntity(item, map);
// 封装好的对象,添加到快递员任务列表
entities.add(courierTaskEntity);
}
// 5.循环添加新增操作
BulkRequest.Builder br = new BulkRequest.Builder();
for (CourierTaskEntity entity : entities) {
br.operations(op -> op.index(idx -> idx
.index(indexName)
.id(String.valueOf(entity.getId()))
.document(entity)));
}
// 6.批量新增
client.bulk(br.build());
}
/**
* 封装快递员任务对象
*
* @param pickupDispatchTaskDTO 取派件任务
* @return 快递员任务
*/
private CourierTaskEntity getCourierTaskEntity(PickupDispatchTaskDTO pickupDispatchTaskDTO, Map<Long, OrderDTO> map) {
// 1.取派件任务字段复制到快递员任务
CourierTaskEntity courierTaskEntity = BeanUtil.toBean(pickupDispatchTaskDTO, CourierTaskEntity.class);
courierTaskEntity.setTaskType(pickupDispatchTaskDTO.getTaskType().getCode());
courierTaskEntity.setStatus(pickupDispatchTaskDTO.getStatus().getCode());
courierTaskEntity.setIsDeleted(pickupDispatchTaskDTO.getIsDeleted().getCode());
// 2.根据id查询订单
OrderDTO orderDTO = map.get(pickupDispatchTaskDTO.getOrderId());
if (ObjectUtil.isEmpty(orderDTO)) {
String errorMsg = CharSequenceUtil.format("id为{}的取派件任务的订单不存在!", pickupDispatchTaskDTO.getId());
throw new SLException(errorMsg);
}
// 3.设置快递员任务的姓名、电话、地址字段
this.setNameAndPhoneAndAddress(pickupDispatchTaskDTO.getTaskType(), orderDTO, courierTaskEntity);
// 为快递员任务set运单号:取件任务新任务状态和已取消状态不查运单
if (ObjectUtil.equal(PickupDispatchTaskType.DISPATCH, pickupDispatchTaskDTO.getTaskType()) || ObjectUtil.equal(PickupDispatchTaskStatus.COMPLETED, pickupDispatchTaskDTO.getStatus())) {
TransportOrderDTO transportOrderDTO = transportOrderFeign.findByOrderId(pickupDispatchTaskDTO.getOrderId());
courierTaskEntity.setTransportOrderId(transportOrderDTO.getId());// 运单id
}
return courierTaskEntity;
}
/**
* 设置快递员任务的姓名、电话、地址字段
*
* @param taskType 任务类型
* @param orderDTO 订单
* @param courierTaskEntity 快递员任务
*/
private void setNameAndPhoneAndAddress(PickupDispatchTaskType taskType, OrderDTO orderDTO, CourierTaskEntity courierTaskEntity) {
String name;
String phone;
String address;
if (taskType.equals(PickupDispatchTaskType.PICKUP)) {
name = orderDTO.getSenderName();
phone = orderDTO.getSenderPhone();
address = orderDTO.getSenderAddress();
} else {
name = orderDTO.getReceiverName();
phone = orderDTO.getReceiverPhone();
address = orderDTO.getReceiverAddress();
}
courierTaskEntity.setAddress(address);
courierTaskEntity.setName(name);
courierTaskEntity.setPhone(phone);
}
}