在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 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律