WELCOMEBACK,Min|

MingHaiZ

园龄:8个月粉丝:2关注:2

在SpringBoot当中集合ElasticSearch进行搜索

前言

在学习easyLive项目当中接触到了elasticSearch中间件来进行搜索.本随笔记录一下项目中用到的操作. 这里就省略掉一些es的基础介绍和为什么要使用es了.

1. 准备阶段

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

在这里使用的是SpringData集合的elasticSearch,初始化方法和直接引入highLevelRestClient有所区别(新版本已经废弃掉这个方法了,现在还没学).
创建Config类,这里我们可以参考SpringData文档 中的操作来对es初始化,创建一个Config类
EsConfiguration

@Configuration
public class EsConfiguration extends AbstractElasticsearchConfiguration implements DisposableBean {
private RestHighLevelClient client;
@Resource
private AppConfig appConfig;
@Override
public void destroy() throws Exception {
if (client != null) {
client.close();
}
}
@Override
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo(appConfig.getEsHostPort())
.build();
client = RestClients.create(clientConfiguration).rest();
return client;
}
}

AppConfig类

//省去了getter和setter方法来节省空间,之后的随笔也是这样
@Configuration
public class AppConfig {
@Value("${project.folder:}")
private String projectFolder;
@Value("${admin.account:}")
private String adminAccount;
@Value("${admin.password:}")
private String adminPassword;
@Value("${showFFmpegLog:true}")
private boolean showFFmpegLog;
//这里还没琢磨过有没有AutoConfig类能直接在application.yml里直接配置,之后再看吧,其实这里写yml配置文件里更好,这样子docker部署的时候可以通过外置挂载卷来修改es地址
@Value("${es.host.port:127.0.0.1:9200}")
private String esHostPort;
@Value("${ex.index.video.name:easylive_video}")
private String esIndexVideoName;
}

在每次启动的时候初始化es而不是在es里好了再找,这样子在换运行环境的时候可以方便很多
所以我们创建一个esComponent

@Component("esSearchComponent")
public class EsSearchComponent {
@Resource
private AppConfig appConfig;
@Resource
private RestHighLevelClient restHighLevelClient;
private Boolean isExsitIndex() throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest(appConfig.getEsIndexVideoName());
return restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
}
public void createIndex() {
try {
if (isExsitIndex()) {
return;
}
CreateIndexRequest request = new CreateIndexRequest(appConfig.getEsIndexVideoName());
request.settings("{\"analysis\": {\n" +
" \"analyzer\": {\n" +
" \"comma\": {\n" +
" \"type\": \"pattern\",\n" +
" \"pattern\": \",\"\n" +
" }\n" +
" }\n" +
" }}", XContentType.JSON);
request.mapping("{\"properties\": {\n" +
" \"videoName\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"tags\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"comma\"\n" +
" },\n" +
" \"playCount\":{\n" +
" \"type\":\"integer\",\n" +
" \"index\":false\n" +
" },\n" +
" \"danmuCount\":{\n" +
" \"type\":\"integer\",\n" +
" \"index\":false\n" +
" },\n" +
" \"collectCount\":{\n" +
" \"type\":\"integer\",\n" +
" \"index\":false\n" +
" },\n" +
" \"createTime\":{\n" +
" \"type\":\"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss\",\n" +
" \"index\": false\n" +
" },\n" +
" \"videoId\":{\n" +
" \"type\":\"text\",\n" +
" \"index\": false\n" +
" }\n" +
" }" +
"}", XContentType.JSON);
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
Boolean acknowledged = createIndexResponse.isAcknowledged();
if (!acknowledged) {
throw new BusinessException("初始化ES失败");
}
} catch (IOException e) {
log.error("初始化ES失败", e);
throw new BusinessException("初始化ES失败");
}
}
}

因为Es是Docs存储的,所以这里的配置得用JS格式来发送请求到esClient,这里其实就是创建视频的搜索信息,约束和分词器.
在创建号Component之后启动SpringBoot程序,用一个实现了ApplicationRunner接口的类实现run回调函数

public void run(ApplicationArguments args) throws Exception {
esSearchComponent.createIndex();
}

一切正常没有报错的话就进去下一步

2.使用阶段

在此之前,我们需要创建一个和es里docs存储mapping一致的DTO类,因为当有多余属性的话,es就会自动添加一些属性进去,虽然目前没什么问题,但这是预期之外的为了防止问题就先解决掉.

public class VideoInfoEsDto {
/**
* 视频ID
*/
private String videoId;
/**
* 视频封面
*/
private String videoCover;
/**
* 视频名称
*/
private String videoName;
/**
* 用户ID
*/
private String userId;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 标签
*/
private String tags;
/**
* 播放数量
*/
private Integer playCount;
/**
* 弹幕数量
*/
private Integer danmuCount;
/**
* 收藏数量
*/
private Integer collectCount;

这里需要注意一点的就是createTime上的注解JsonFormat是Jackson的格式化,我们用的是fastJson所以得使用fastJson的格式化注解@JsonField 不然没有序列化进去就不是我们预期的效果了

1.保存对象Dto到es当中

在我们的后台审核视频业务当中,我们在通过视频审核之后就要将VideoInfo保存到es当中,在auditVideo方法的最后才使用saveDocs方法

这是考虑到事务的问题,因为es不支持Transactional的rollback,也就是说当es里写入成功之后如果后面抛出异常的话那es里的信息其实是不会回滚的

public void saveDoc(VideoInfo videoInfo) {
VideoInfoEsDto videoInfoEsDto = CopyTools.copy(videoInfo, VideoInfoEsDto.class);
try {
if (docExist(videoInfo.getVideoId())) {
updateDoc(videoInfoEsDto);
} else {
insertDoc(videoInfoEsDto);
}
} catch (IOException e) {
log.error("保存到Es失败", e);
throw new BusinessException("保存到Es失败");
}
}
private void updateDoc(VideoInfoEsDto videoInfoEsDto) throws IOException {
try {
videoInfoEsDto.setCreateTime(null);
HashMap<String, Object> dataMap = new HashMap<>();
Field[] fields = videoInfoEsDto.getClass().getDeclaredFields();
for (Field field : fields) {
String methodName = "get" + StringTools.upperCaseFirstLetter(field.getName());
Method method = videoInfoEsDto.getClass().getMethod(methodName);
Object invoke = method.invoke(videoInfoEsDto);
if (Objects.nonNull(invoke) && invoke instanceof String && !StringTools.isEmpty(invoke.toString()) || Objects.nonNull(invoke) && !(invoke instanceof String)) {
dataMap.put(field.getName(), invoke);
}
if (dataMap.isEmpty()) {
return;
}
UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexVideoName(), videoInfoEsDto.getVideoId());
updateRequest.doc(dataMap);
restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
}
} catch (Exception e) {
log.error("保存视频失败", e);
throw new BusinessException("保存视频失败");
}
}
private void insertDoc(VideoInfoEsDto videoInfoEsDto) throws IOException {
videoInfoEsDto.setCollectCount(0);
videoInfoEsDto.setDanmuCount(0);
videoInfoEsDto.setPlayCount(0);
IndexRequest indexRequest = new IndexRequest(appConfig.getEsIndexVideoName());
indexRequest.id(videoInfoEsDto.getVideoId()).source(JsonUtils.convertObj2Json(videoInfoEsDto), XContentType.JSON);
restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
}
private Boolean docExist(String id) throws IOException {
GetRequest getIndexRequest = new GetRequest(appConfig.getEsIndexVideoName(), id);
GetResponse getResponse = restHighLevelClient.get(getIndexRequest, RequestOptions.DEFAULT);
return getResponse.isExists();
}

2. 更新数量到ES

public void updateDocCount(String videoId, String fieldName, Integer count) {
try {
UpdateRequest updateRequest = new UpdateRequest(appConfig.getEsIndexVideoName(), videoId);
//这里类似于SQL里的行锁,来防止高并发的情况下导致数据出现错误
Script script = new Script(ScriptType.INLINE, "painless", "ctx._source." + fieldName + " += params.count", Collections.singletonMap("count", count));
updateRequest.script(script);
restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("保存视频失败", e);
throw new BusinessException("保存视频失败");
}
}

3. 从es里删除视频

public void delDoc(String videoId) {
try {
DeleteRequest deleteRequest = new DeleteRequest(appConfig.getEsIndexVideoName(), videoId);
restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("删除视频失败", e);
throw new BusinessException("删除视频失败");
}
}

4. 搜索

public PaginationResultVO<VideoInfo> search(Boolean highLight, String keyWord, Integer orderType, Integer pageNo, Integer pageSize) {
try {
SearchOrderTypeEnum searchOrderTypeEnum = SearchOrderTypeEnum.getByType(orderType);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.multiMatchQuery(keyWord, "videoName", "tags"));
// 高亮
if (highLight) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("videoName");
// <span class = 'highlight'> { 关键字 } </span>
highlightBuilder.preTags("<span class = 'highlight'>");
highlightBuilder.postTags("</span>");
searchSourceBuilder.highlighter(highlightBuilder);
}
// 排序
searchSourceBuilder.sort("_score", SortOrder.ASC);
if (Objects.nonNull(orderType)) {
searchSourceBuilder.sort(searchOrderTypeEnum.getField(), SortOrder.DESC);
}
pageNo = pageNo == null ? Constants.ONE : pageNo;
pageSize = pageSize == null ? PageSize.SIZE20.getSize() : pageSize;
searchSourceBuilder.size(pageSize);
searchSourceBuilder.from((pageNo - 1) * pageSize);
SearchRequest searchRequest = new SearchRequest(appConfig.getEsIndexVideoName());
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits searchHits = searchResponse.getHits();
Integer totalCount = (int) searchHits.getTotalHits().value;
List<VideoInfo> videoInfoList = new ArrayList<>();
List<String> userIdList = new ArrayList<>();
for (SearchHit searchHit : searchHits.getHits()) {
VideoInfo videoInfo = JsonUtils.convertJson2Obj(searchHit.getSourceAsString(), VideoInfo.class);
if (Objects.nonNull(searchHit.getHighlightFields().get("videoName"))) {
videoInfo.setVideoName(searchHit.getHighlightFields().get("videoName").fragments()[0].string());
}
videoInfoList.add(videoInfo);
userIdList.add(videoInfo.getUserId());
}
UserInfoQuery userInfoQuery = new UserInfoQuery();
userInfoQuery.setUserIdList(userIdList);
List<UserInfo> userInfoList = userInfoMapper.selectList(userInfoQuery);
Map<String, UserInfo> userInfoMap = userInfoList.stream()
.collect(Collectors.toMap(item -> item.getUserId(), Function.identity(), (data1, data2) -> data2));
videoInfoList.forEach(item -> {
item.setNickName(userInfoMap.get(item.getUserId()).getNickName());
});
SimplePage simplePage = new SimplePage(pageNo, totalCount, pageSize);
PaginationResultVO<VideoInfo> resultVO = new PaginationResultVO<>(totalCount, simplePage.getPageSize(), simplePage.getPageNo(), simplePage.getPageTotal(), videoInfoList);
return resultVO;
} catch (IOException e) {
log.error("视频搜索失败", e);
throw new BusinessException("视频搜索失败");
}
}

3. 总结

在这段学习的时间里查看文档了解了一些elasticSearch的API方法,以及最重要的一句话:"注释不是让你写每行代码逐字逐句的去解释,而是让你一眼就明白这代码的业务功能".这个问题我再之前的filePost文件上传的时候犯了一次,因为我逐字逐句的解释每行代码的功能从而导致后续找Bug的时候阅读到那段代码及其吃力,因为我现在已经有了阅读代码的能力不需要注释这么多荣誉的注解,有这个时间不如提高一下代码的可阅读性.

本文作者:MingHaiZ

本文链接:https://www.cnblogs.com/MingHaiZ/p/18584878

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   MingHaiZ  阅读(140)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起