学成在线笔记+踩坑(10)——课程搜索、课程发布时同步索引库。
导航:
【黑马Java笔记+踩坑汇总】JavaSE+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+牛客面试题_java黑马笔记
目录
1 【检索模块】需求分析
1.1 全文检索介绍
1.2 业务流程
1.2.1、课程发布时索引库里新增一条记录
1.2.2、课程搜索
2 准备环境
2.0 ES回顾
2.1 搭建ES,kibana
2.2 创建课程索引库
2.3 测试是否创建成功
2.4 初始化搜索模块
3 索引管理
3.1 文档操作回顾
3.1.1 添加文档
3.1.2 查询文档
3.1.3 更新文档
3.1.4 删除文档
3.2 课程索引准备
3.2.1 课程索引模型类
3.2.2 controller,添加课程索引
3.3 业务实现,添加课程索引
3.4 测试
4 课程搜索
4.1 需求分析
4.2 接口定义
4.2.1 搜索条件DTO类
4.2.2 搜索结果类
4.2.3 课程搜索controller
4.2.4 课程搜索service完整代码
4.3 基本查询实现
4.3.1 service
4.3.2 测试
4.4 根据条件搜索
4.5 聚合搜索,动态获取两级分类
4.5.1 实现
4.5.2 聚合搜索测试
4.6 高亮设置
4.6.1 实现
4.6.2 高亮设置测试
5 课程信息和索引同步
5.1 技术方案
5.1.1 实时性要求高的场景方案选择
5.1.2 中间件Canal介绍
5.1.3 实时性要求不高的方案(使用)
5.2 任务调度方式实现索引同步
5.2.1 内容服务远程调用搜索服务,并设置熔断降级
5.2.2 课程发布的任务类补充保存索引库方法
5.2.3 测试
1 【检索模块】需求分析
1.1 全文检索介绍
课程搜索是课程展示的渠道,用户通过课程搜索找到课程信息,进一步去查看课程的详细信息,进行选课、支付、学习。
本项目的课程搜索支持全文检索技术。
什么是全文检索?
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
全文检索可以简单理解为通过索引搜索文章。
全文检索的速度非常快,早期应用在搜索引擎技术中,比如:百度、google等,现在通常一些大型网站的搜索功能都是采用全文检索技术。
课程搜索也要将课程信息建立索引,在课程发布时建立课程索引,索引建立好用户可通过搜索网页去查询课程信息。
所以,课程搜索模块包括两部分:课程索引、课程搜索。
课程索引是将课程信息建立索引。
课程搜索是通过前端网页,通过关键字等条件去搜索课程。
1.2 业务流程
根据模块介绍的内容,课程搜索模块包括课程索引、课程搜索两部分。
1.2.1、课程发布时索引库里新增一条记录
首先要建好ES索引库,然后在课程发布操作执行后通过消息处理方式索引库里新增一条记录,如下图:
课程发布业务流程:
1、向内容管理数据库的课程发布表存储课程发布信息,更新课程基本信息表中发布状态为已发布。
2、向Redis存储课程缓存信息。
3、向Elasticsearch存储课程索引信息。
4、静态化课程预览页面并存储到文件系统minIO,实现快速浏览课程详情页面。
本项目使用elasticsearch作为索引及搜索服务。
1.2.2、课程搜索
课程索引创建完成,用户才可以通过前端搜索课程信息。
课程搜索可以从首页进入搜索页面。
下图是搜索界面,可以通过课程分类、课程难度等级等条件进行搜索。
2 准备环境
2.0 ES回顾
ElasticSearch基础1——索引和文档。Kibana,RestClient操作索引和文档+黑马旅游ES库导入_elasticsearch 文档 索引_vincewm的博客-CSDN博客
elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客ElasticSearch基础3——聚合、补全、集群。黑马旅游检索高亮+自定义分词器+自动补全+前后端消息同步_vincewm的博客-CSDN博客
2.1 搭建ES,kibana
1.docker搭建ES,kibana:
参考上面回顾文章第一篇。
kibana 是 ELK(Elasticsearch , Logstash, Kibana )之一,kibana 一款开源的数据分析和可视化平台,通过可视化界面访问elasticsearch的索引库,并可以生成一个数据报表。
开发中主要使用kibana通过api对elasticsearch进行索引和搜索操作,通过浏览器访问 http://192.168.101.65:5601/app/dev_tools#/console进入kibana的开发工具界面。
2.设置ES,kibana容器开机启动
修改虚拟机中的启动脚本restart.sh添加
docker stop elasticsearch
docker stop kibanadocker start elasticsearch
docker start kibana
可通过命令:GET /_cat/indices?v 查看所有的索引,通过此命令判断kibana是否正常连接elasticsearch。
索引相当于MySQL中的表,Elasticsearch与MySQL之间概念的对应关系见下表:
要使用elasticsearch需要建立索引,Mapping相当于表结构,Mapping创建后其字段不能删除,如果要删除需要删除整个索引,下边介绍创建索引、查询索引、删除索引的方法:
2.2 创建课程索引库
创建索引,并指定Mapping。
PUT /course-publish
{"settings": {"number_of_shards": 1,"number_of_replicas": 0},"mappings": {"properties": {"id": {"type": "keyword"},"companyId": {"type": "keyword"},"companyName": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"name": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"users": {"index": false,"type": "text"},"tags": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"mt": {"type": "keyword"},"mtName": {"type": "keyword"},"st": {"type": "keyword"},"stName": {"type": "keyword"},"grade": {"type": "keyword"},"teachmode": {"type": "keyword"},"pic": {"index": false,"type": "text"},"description": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"createDate": {"format": "yyyy-MM-dd HH:mm:ss","type": "date"},"status": {"type": "keyword"},"remark": {"index": false,"type": "text"},"charge": {"type": "keyword"},"price": {"type": "scaled_float","scaling_factor": 100},"originalPrice": {"type": "scaled_float","scaling_factor": 100},"validDays": {"type": "integer"}}}
}
删除索引
如果发现创建的course-publish不正确可以删除重新创建。
删除索引后当中的文档数据也同时删除,一定要谨慎操作!
删除索引命令:DELETE /course-publish
2.3 测试是否创建成功
通过 GET /_cat/indices?v 查询所有的索引,查找course-publish是否创建成功。
通过GET /course-publish/_mapping 查询course-publish的索引结构。
{"course-publish" : {"mappings" : {"properties" : {"charge" : {"type" : "keyword"},"companyId" : {"type" : "keyword"},"companyName" : {"type" : "text","analyzer" : "ik_max_word","search_analyzer" : "ik_smart"},"createDate" : {"type" : "date","format" : "yyyy-MM-dd HH:mm:ss"},"description" : {"type" : "text","analyzer" : "ik_max_word","search_analyzer" : "ik_smart"},"grade" : {"type" : "keyword"},"id" : {"type" : "keyword"},"mt" : {"type" : "keyword"},"mtName" : {"type" : "keyword"},"name" : {"type" : "text","analyzer" : "ik_max_word","search_analyzer" : "ik_smart"},"originalPrice" : {"type" : "scaled_float","scaling_factor" : 100.0},"pic" : {"type" : "text","index" : false},"price" : {"type" : "scaled_float","scaling_factor" : 100.0},"remark" : {"type" : "text","index" : false},"st" : {"type" : "keyword"},"stName" : {"type" : "keyword"},"status" : {"type" : "keyword"},"tags" : {"type" : "text","analyzer" : "ik_max_word","search_analyzer" : "ik_smart"},"teachmode" : {"type" : "keyword"},"users" : {"type" : "text","index" : false},"validDays" : {"type" : "integer"}}}}
}
2.4 初始化搜索模块
创建搜索模块:
修改bootstrap.xml中nacos的namespace为自己的命名空间。
启动网关服务、搜索服务。
部署完成通过httpclient进行测试
### 添加课程索引
POST {{search_host}}/search/index/course
Content-Type: application/json{"charge" : "201000","companyId" : 100000,"companyName" : "北京黑马程序","createDate" : "2022-09-25 09:36:11","description" : "《Spring编程思想》是2007年6月1日机械工业出版社出版的图书,作者是埃克尔,译者是陈昊鹏。主要内容本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作","grade" : "204001","id" : 102,"mt" : "1-3","mtName" : "编程开发","name" : "Spring编程思想","originalPrice" : 200.0,"pic" : "/mediafiles/2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png","price" : 100.0,"remark" : "没有备注","st" : "1-3-2","stName" : "Java语言","status" : "203002","tags" : "没有标签","teachmode" : "200002","validDays" : 222
}### 搜索课程
GET {{search_host}}/search/course/list?pageNo=1&keywords=spring
Content-Type: application/json
进入前端搜索界面http://www.51xuecheng.cn/course/search.html
3 索引管理
3.1 文档操作回顾
3.1.1 添加文档
索引创建好就可以向其中添加文档,此时elasticsearch会根据索引的mapping配置对有些字段进行分词。
这里我们要向course_publish中添加课程信息。
使用rest api进行测试,如下:
使用post请求,/course-publish/_doc/103 第一部分为索引名称,_doc固定,103为文档的主键id,这里为课程id。
课程内容使用json表示。
POST /course-publish/_doc/103
{"charge" : "201001","companyId" : 100000,"companyName" : "北京黑马程序","createDate" : "2022-09-25 09:36:11","description" : "HTML/CSS","grade" : "204001","id" : 102,"mt" : "1-1","mtName" : "前端开发","name" : "Html参考大全","originalPrice" : 200.0,"pic" : "/mediafiles/2022/09/20/e726b71ba99c70e8c9d2850c2a7019d7.jpg","price" : 100.0,"remark" : "没有备注","st" : "1-1-1","stName" : "HTML/CSS","status" : "203002","tags" : "没有标签","teachmode" : "200002","validDays" : 222
}
如果要修改文档的内容可以使用上边相同的方法,如果没有则添加,如果存在则更新。
3.1.2 查询文档
添加文档成功后可以通过主键id查询该文档的信息。
语法如下:
GET /{索引库名称}/_doc/{id}
3.1.3 更新文档
更新文档分为全量更新和局部更新。
全量更新是指先删除再更新,语法如下:
PUT /{索引库名}/_doc/文档id
{"字段1": "值1","字段2": "值2",// ... 略
}
局部更新语法如下:
POST /{索引库名}/_update/文档id
{"doc": {"字段名": "新的值",}
}
3.1.4 删除文档
删除文档将从索引中删除文档的记录。
语法如下:
DELETE /{索引库名}/_doc/id值
3.2 课程索引准备
当课程发布时请求添加课程接口添加课程信息到索引,当课程下架时请求删除课程接口从索引中删除课程信息。
3.2.1 课程索引模型类
根据索引的mapping结构创建po类:
package com.xuecheng.search.po;/*** <p>* 课程索引信息* </p>** @author itcast*/
@Data
public class CourseIndex implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/private Long id;/*** 机构ID*/private Long companyId;/*** 公司名称*/private String companyName;/*** 课程名称*/private String name;/*** 适用人群*/private String users;/*** 标签*/private String tags;/*** 大分类*/private String mt;/*** 大分类名称*/private String mtName;/*** 小分类*/private String st;/*** 小分类名称*/private String stName;/*** 课程等级*/private String grade;/*** 教育模式*/private String teachmode;/*** 课程图片*/private String pic;/*** 课程介绍*/private String description;/*** 发布时间*/@JSONField(format="yyyy-MM-dd HH:mm:ss")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createDate;/*** 状态*/private String status;/*** 备注*/private String remark;/*** 收费规则,对应数据字典--203*/private String charge;/*** 现价*/private Float price;/*** 原价*/private Float originalPrice;/*** 课程有效期天数*/private Integer validDays;}
3.2.2 controller,添加课程索引
创建索引接口如下:
package com.xuecheng.search.controller;/*** @description 课程索引接口* @date 2022/9/24 22:31*/
@Api(value = "课程信息索引接口", tags = "课程信息索引接口")
@RestController
@RequestMapping("/index")
public class CourseIndexController {@Value("${elasticsearch.course.index}")private String courseIndexStore;@AutowiredIndexService indexService;@ApiOperation("添加课程索引")@PostMapping("course")public Boolean add(@RequestBody CourseIndex courseIndex) {Long id = courseIndex.getId();if(id==null){XueChengPlusException.cast("课程id为空");}Boolean result = indexService.addCourseIndex(courseIndexStore, String.valueOf(id), courseIndex);if(!result){XueChengPlusException.cast("添加课程索引失败");}return result;}
}
3.3 业务实现,添加课程索引
定义service接口,请求elasticsearch添加课程信息。
注意:为了适应其它文档信息,将添加文档定义为通用的添加文档接口,此接口不仅应添加课程还应添加其它信息。
package com.xuecheng.search.service.impl;/*** @description 课程索引管理接口实现*/
@Slf4j
@Service
public class IndexServiceImpl implements IndexService {@AutowiredRestHighLevelClient client;@Overridepublic Boolean addCourseIndex(String indexName,String id,Object object) {String jsonString = JSON.toJSONString(object);IndexRequest indexRequest = new IndexRequest(indexName).id(id);//指定索引文档内容indexRequest.source(jsonString,XContentType.JSON);//索引响应对象IndexResponse indexResponse = null;try {indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);} catch (IOException e) {log.error("添加索引出错:{}",e.getMessage());e.printStackTrace();XueChengPlusException.cast("添加索引出错");}String name = indexResponse.getResult().name();System.out.println(name);return name.equalsIgnoreCase("created") || name.equalsIgnoreCase("updated");}}
3.4 测试
使用httpclient进行测试
### 添加课程索引
POST {{search_host}}/search/index/course
Content-Type: application/json{"charge" : "201000","companyId" : 100000,"companyName" : "北京黑马程序员","createDate" : "2022-09-25 09:36:11","description" : "《Java编程思想》是2007年6月1日机械工业出版社出版的图书,作者是埃克尔,译者是陈昊鹏。主要内容本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作","grade" : "204001","id" : 102,"mt" : "1-3","mtName" : "编程开发","name" : "Java编程思想","originalPrice" : 200.0,"pic" : "/mediafiles/2022/09/20/1d0f0e6ed8a0c4a89bfd304b84599d9c.png","price" : 100.0,"remark" : "没有备注","st" : "1-3-2","stName" : "Java语言","status" : "203002","tags" : "没有标签","teachmode" : "200002","validDays" : 222
}
4 课程搜索
4.1 需求分析
索引信息维护完成下一步定义搜索接口搜索课程信息,首先需要搞清楚搜索功能的需求。
进入搜索界面,如下图:
根据搜索界面可知需求如下:
1、根据一级分类、二级分类搜索课程信息。
2、根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容。
3、根据难度等级搜索课程。
4、搜索结点分页显示。
技术点:
1、整体采用布尔查询。
2、根据关键字搜索,采用MultiMatchQuery,搜索name、description字段。
3、根据分类、课程等级搜索采用过滤器实现。
4、分页查询。
5、高亮显示。
为什么课程分类、课程等级等查询使用过滤器方式?
使用关键字查询需要计算相关度得分,根据课程分类、课程等级去查询不需要计算相关度得分,使用过滤器实现根据课程分类、课程等级查询的过程不会计算相关度得分,效率更高。
4.2 接口定义
4.2.1 搜索条件DTO类
package com.xuecheng.search.dto;/*** @description 搜索课程参数dtl* @author Mr.M* @date 2022/9/24 22:36* @version 1.0*/@Data@ToString
public class SearchCourseParamDto {//关键字private String keywords;//大分类private String mt;//小分类private String st;//难度等级private String grade;}
4.2.2 搜索结果类
为了适应后期的扩展,定义搜索结果类,让它继承PageResult
package com.xuecheng.search.dto;@Data
@ToString
public class SearchPageResultDto<T> extends PageResult {//大分类列表List<String> mtList;//小分类列表List<String> stList;public SearchPageResultDto(List<T> items, long counts, long page, long pageSize) {super(items, counts, page, pageSize);}}
4.2.3 课程搜索controller
package com.xuecheng.search.controller;/*** @description 课程搜索接口*/
@Api(value = "课程搜索接口",tags = "课程搜索接口")@RestController@RequestMapping("/course")
public class CourseSearchController {@AutowiredCourseSearchService courseSearchService;@ApiOperation("课程搜索列表")@GetMapping("/list")public SearchPageResultDto<CourseIndex> list(PageParams pageParams, SearchCourseParamDto searchCourseParamDto){return courseSearchService.queryCoursePubIndex(pageParams,searchCourseParamDto);}
}
4.2.4 课程搜索service完整代码
@Overridepublic SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});if(courseSearchParam==null){courseSearchParam = new SearchCourseParamDto();}//关键字if(StringUtils.isNotEmpty(courseSearchParam.getKeywords())){//匹配关键字MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeywords(), "name", "description");//设置匹配占比multiMatchQueryBuilder.minimumShouldMatch("70%");//提升另个字段的Boost值multiMatchQueryBuilder.field("name",10);boolQueryBuilder.must(multiMatchQueryBuilder);}//过虑if(StringUtils.isNotEmpty(courseSearchParam.getMt())){boolQueryBuilder.filter(QueryBuilders.termQuery("mtName",courseSearchParam.getMt()));}if(StringUtils.isNotEmpty(courseSearchParam.getSt())){boolQueryBuilder.filter(QueryBuilders.termQuery("stName",courseSearchParam.getSt()));}if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));}//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//高亮设置HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.preTags("<font class='eslight'>");highlightBuilder.postTags("</font>");//设置高亮字段highlightBuilder.fields().add(new HighlightBuilder.Field("name"));searchSourceBuilder.highlighter(highlightBuilder);//请求搜索searchRequest.source(searchSourceBuilder);//聚合设置buildAggregation(searchRequest);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);//取出sourceMap<String, Object> sourceAsMap = hit.getSourceAsMap();//课程idLong id = courseIndex.getId();//取出名称String name = courseIndex.getName();//取出高亮字段内容Map<String, HighlightField> highlightFields = hit.getHighlightFields();if(highlightFields!=null){HighlightField nameField = highlightFields.get("name");if(nameField!=null){Text[] fragments = nameField.getFragments();StringBuffer stringBuffer = new StringBuffer();for (Text str : fragments) {stringBuffer.append(str.string());}name = stringBuffer.toString();}}courseIndex.setId(id);courseIndex.setName(name);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);//获取聚合结果List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");pageResult.setMtList(mtList);pageResult.setStList(stList);return pageResult;}
4.3 基本查询实现
4.3.1 service
搜索接口的内容较多,我们分几步实现,首先实现根据分页搜索,接口实现如下:
package com.xuecheng.search.service.impl;
/*** @author Mr.M* @version 1.0* @description 课程搜索service实现类* @date 2022/9/24 22:48*/
@Slf4j
@Service
public class CourseSearchServiceImpl implements CourseSearchService {@Value("${elasticsearch.course.index}")private String courseIndexStore;@Value("${elasticsearch.course.source_fields}")private String sourceFields;@AutowiredRestHighLevelClient client;/*** @description 搜索课程列表* @param pageParams 分页参数* @param searchCourseParamDto 搜索条件* @return PageResult<CourseIndex> 课程列表* @author Mr.M* @date 2022/9/24 22:45*/@Overridepublic SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//请求搜索searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);return pageResult;}}
4.3.2 测试
当输入查询条件时会查询全部课程信息并支持分页查询。
1、准备测试
启动nginx、网关、搜索服务。
使用kibana通过rest api向索引库添加课程信息,或通过httpclient添加课程信息,至少添加两条信息。
2、进入搜索界面
默认查询出刚才添加的课程信息。
3、修改分页参数测试分页
打开course/ search.html页面 ,找到如下图所示位置:
修改pageSize为1,即一页显示一条记录。
刷新搜索界面,每页显示一条记录,如下图:
4.4 根据条件搜索
下边实现根据关键、一级分类、二级分类、难度等级搜索。
@Override
public SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});if(courseSearchParam==null){courseSearchParam = new SearchCourseParamDto();}//关键字if(StringUtils.isNotEmpty(courseSearchParam.getKeywords())){//匹配关键字MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeywords(), "name", "description");//设置匹配占比multiMatchQueryBuilder.minimumShouldMatch("70%");//提升另个字段的Boost值multiMatchQueryBuilder.field("name",10);boolQueryBuilder.must(multiMatchQueryBuilder);}//过虑if(StringUtils.isNotEmpty(courseSearchParam.getMt())){boolQueryBuilder.filter(QueryBuilders.termQuery("mtName",courseSearchParam.getMt()));}if(StringUtils.isNotEmpty(courseSearchParam.getSt())){boolQueryBuilder.filter(QueryBuilders.termQuery("stName",courseSearchParam.getSt()));}if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));}//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//请求搜索searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);return pageResult;
}
测试
进入搜索界面,输入关键字进行测试。
一级分类、二级分类在下边的聚合搜索中测试。
4.5 聚合搜索,动态获取两级分类
4.5.1 实现
搜索界面上显示的一级分类、二级分类来源于搜索结果,使用聚合搜索实现获取搜索结果中的一级分类、二级分类:
搜索方法如下:
@Override
public SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});if(courseSearchParam==null){courseSearchParam = new SearchCourseParamDto();}//关键字if(StringUtils.isNotEmpty(courseSearchParam.getKeywords())){//匹配关键字MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeywords(), "name", "description");//设置匹配占比multiMatchQueryBuilder.minimumShouldMatch("70%");//提升另个字段的Boost值multiMatchQueryBuilder.field("name",10);boolQueryBuilder.must(multiMatchQueryBuilder);}//过虑if(StringUtils.isNotEmpty(courseSearchParam.getMt())){boolQueryBuilder.filter(QueryBuilders.termQuery("mtName",courseSearchParam.getMt()));}if(StringUtils.isNotEmpty(courseSearchParam.getSt())){boolQueryBuilder.filter(QueryBuilders.termQuery("stName",courseSearchParam.getSt()));}if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));}//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//请求搜索searchRequest.source(searchSourceBuilder);//聚合设置buildAggregation(searchRequest);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);//获取聚合结果List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");pageResult.setMtList(mtList);pageResult.setStList(stList);return pageResult;
}
4.5.2 聚合搜索测试
进入搜索界面,观察搜索请求的响应内容中是否存在mtList和stList.
观察页面一级分类、二级分类是否有分类信息。
注意:当选中一个一级分类时才会显示二级分类。
4.6 高亮设置
4.6.1 实现
最后实现关键词在课程名称中高亮显示。
@Override
public SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});if(courseSearchParam==null){courseSearchParam = new SearchCourseParamDto();}//关键字if(StringUtils.isNotEmpty(courseSearchParam.getKeywords())){//匹配关键字MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeywords(), "name", "description");//设置匹配占比multiMatchQueryBuilder.minimumShouldMatch("70%");//提升另个字段的Boost值multiMatchQueryBuilder.field("name",10);boolQueryBuilder.must(multiMatchQueryBuilder);}//过虑if(StringUtils.isNotEmpty(courseSearchParam.getMt())){boolQueryBuilder.filter(QueryBuilders.termQuery("mtName",courseSearchParam.getMt()));}if(StringUtils.isNotEmpty(courseSearchParam.getSt())){boolQueryBuilder.filter(QueryBuilders.termQuery("stName",courseSearchParam.getSt()));}if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));}//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//高亮设置HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.preTags("<font class='eslight'>");highlightBuilder.postTags("</font>");//设置高亮字段highlightBuilder.fields().add(new HighlightBuilder.Field("name"));searchSourceBuilder.highlighter(highlightBuilder);//请求搜索searchRequest.source(searchSourceBuilder);//聚合设置buildAggregation(searchRequest);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);//取出sourceMap<String, Object> sourceAsMap = hit.getSourceAsMap();//课程idLong id = courseIndex.getId();//取出名称String name = courseIndex.getName();//取出高亮字段内容Map<String, HighlightField> highlightFields = hit.getHighlightFields();if(highlightFields!=null){HighlightField nameField = highlightFields.get("name");if(nameField!=null){Text[] fragments = nameField.getFragments();StringBuffer stringBuffer = new StringBuffer();for (Text str : fragments) {stringBuffer.append(str.string());}name = stringBuffer.toString();}}courseIndex.setId(id);courseIndex.setName(name);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);//获取聚合结果List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");pageResult.setMtList(mtList);pageResult.setStList(stList);return pageResult;
}
4.6.2 高亮设置测试
输入关键字,观察搜索结果,标题中是否对关键字信息进行高亮显示。
5 课程信息和索引同步
5.1 技术方案
5.1.1 实时性要求高的场景方案选择
通过向索引中添加课程信息最终实现了课程的搜索,我们发现课程信息是先保存在关系数据库中,而后再写入索引,这个过程是将关系数据中的数据同步到elasticsearch索引中的过程,可以简单成为索引同步。
通常项目中使用elasticsearch需要完成索引同步.
针对实时性非常高的场景需要满足数据的及时同步,可以同步调用,或使用Canal去实现。
1)同步调用:同步调用即在向MySQL写数据后远程调用搜索服务的接口写入索引,此方法简单但是耦合代码太高。
2)canal:可以使用一个中间的软件canal解决耦合性的问题,但存在学习与维护成本。
5.1.2 中间件Canal介绍
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,实现将MySQL的数据同步到消息队列、Elasticsearch、其它数据库等,应用场景十分丰富。
它的地址:
github地址:https://github.com/alibaba/canal
版本下载地址:https://github.com/alibaba/canal/releases
文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart
Canal基于mysql的binlog技术实现数据同步,什么是binlog,它是一个文件,二进制格式,记录了对数据库更新的SQL语句,向数据库写数据的同时向binlog文件里记录对应的sql语句。当数据库服务器发生了故障就可以使用binlog文件对数据库进行恢复。
所以,使用canal是需要开启mysql的binlog写入功能,Canal工作原理如下:
1、canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump
协议
2、MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
3、canal 解析 binary log 对象(原始为 byte 流)
详细使用Canal进行索引同步的步骤参考:Canal实现索引同步.pdf
5.1.3 实时性要求不高的方案(使用)
当索引同步的实时性要求不高时可用的技术比较多,比如:MQ、Logstash、任务调度等。
MQ:向mysql写数据的时候向mq写入消息,搜索服务监听MQ,收到消息后写入索引。使用MQ的优势是代码解耦,但是需要处理消息可靠性的问题有一定的技术成本,做到消息可靠性需要做到生产者投递成功、消息持久化以及消费者消费成功三个方面,另外还要做好消息幂等性问题。
Logstash: 开源实时日志分析平台 ELK包括Elasticsearch、Kibana、Logstash,Logstash负责收集、解析和转换日志信息,可以实现MySQL与Elasticsearch之间的数据同步。也可以实现解耦合并且是官方推荐,但需要增加学习与维护成本。
任务调度(使用):向mysql写数据的时候记录修改记录,开启一个定时任务根据修改记录将数据同步到Elasticsearch。
根据本项目的需求,课程发布后信息同步的实时性要求不高,从提交审核到发布成功一般两个工作日完成。综合比较以上技术方案本项目的索引同步技术使用任务调度的方法。
如下图:
1、课程发布向消息表插入记录。
2、由任务调度程序通过消息处理SDK对消息记录进行处理。
3、向elasticsearch索引中保存课程信息。
如何向向elasticsearch索引中保存课程信息?
执行流程如下:
由内容管理服务远程调用搜索服务添加课程信息索引,搜索服务再请求elasticsearch向课程索引中添加文档。
5.2 任务调度方式实现索引同步
首先拷贝CourseIndex 模型类到内容管理model 工程的dto包下。
5.2.1 内容服务远程调用搜索服务,并设置熔断降级
在内容管理服务中添加FeignClient
package com.xuecheng.content.feignclient;/*** @description 搜索服务远程接口* @author Mr.M* @date 2022/9/20 20:29* @version 1.0*/
@FeignClient(value = "search",fallbackFactory = SearchServiceClientFallbackFactory.class)
public interface SearchServiceClient {@PostMapping("/search/index/course")public Boolean add(@RequestBody CourseIndex courseIndex);
}
定义SearchServiceClientFallbackFactory :
@Slf4j
@Component
public class SearchServiceClientFallbackFactory implements FallbackFactory<SearchServiceClient> {@Overridepublic SearchServiceClient create(Throwable throwable) {return new SearchServiceClient() {@Overridepublic Boolean add(CourseIndex courseIndex) {throwable.printStackTrace();log.debug("调用搜索发生熔断走降级方法,熔断异常:", throwable.getMessage());return false;}};}
}
5.2.2 课程发布的任务类补充保存索引库方法
编写课程索引任务执行方法
完善CoursePublishTask类中的saveCourseIndex方法
//保存课程索引信息
public void saveCourseIndex(MqMessage mqMessage,long courseId){log.debug("保存课程索引信息,课程id:{}",courseId);//消息idLong id = mqMessage.getId();//消息处理的serviceMqMessageService mqMessageService = this.getMqMessageService();//消息幂等性处理int stageTwo = mqMessageService.getStageTwo(id);if(stageTwo > 0){log.debug("课程索引已处理直接返回,课程id:{}",courseId);return ;}Boolean result = saveCourseIndex(courseId);if(result){//保存第一阶段状态mqMessageService.completedStageTwo(id);}
}private Boolean saveCourseIndex(Long courseId) {//取出课程发布信息CoursePublish coursePublish = coursePublishMapper.selectById(courseId);//拷贝至课程索引对象CourseIndex courseIndex = new CourseIndex();BeanUtils.copyProperties(coursePublish,courseIndex);//远程调用搜索服务api添加课程信息到索引Boolean add = searchServiceClient.add(courseIndex);if(!add){XueChengPlusException.cast("添加索引失败");}return add;}
5.2.3 测试
测试流程如下:
1、启动elasticsearch、kibana。
2、启动网关、内容管理、搜索服务、nginx。
3、启动xxl-job调度中心。
4、在任务调度中心开始课程发布任务。
5、发布一门课程,页面提示操作成功,查看发布课程任务是否写到任务表。
6、经过任务调度将课程信息写入索引。
7、通过门户进入搜索页面,查看课程信息是否展示。
相关文章:

学成在线笔记+踩坑(10)——课程搜索、课程发布时同步索引库。
导航: 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线牛客面试题_java黑马笔记 目录 1 【检索模块】需求分析 1.1 全文检索介绍 1.2 业务流程 1.2.1、课程发布时索引库里新增一条记录 1.2.2、课程搜索 2 准…...

某应用虚拟化系统远程代码执行
漏洞简介 微步在线漏洞团队通过“X漏洞奖励计划”获取到瑞友天翼应用虚拟化系统远程代码执行漏洞情报(0day),攻击者可以通过该漏洞执行任意代码,导致系统被攻击与控制。瑞友天翼应用虚拟化系统是基于服务器计算架构的应用虚拟化平台,它将用户…...

solaris-Oracle11g于linux-mysql相连
Oracle11g(solaris64sparc)mysql(linux)实验 此实验目的,实现公司ebs R12 连mysql上的短信平台.预警和提示ebs中信息, 一,环境 主机名 ip 平台 数据库 dbname ebs234 192.168.1.234 …...

大厂齐出海:字节忙种草,网易爱社交
配图来自Canva可画 随着国内移动互联网红利逐渐触顶,互联网市场日趋饱和,国内各互联网企业之间的竞争便愈发激烈起来。在此背景下,广阔的海外市场就成为了腾讯、阿里、字节、京东、拼多多、百度、网易、快手、B站等互联网公司关注和争夺的重…...

几个实用的正则表达式
1到100之间的正整数正则 表达式:^[1-9]\d?$|^100$ 解释: ^表示匹配字符串开始位置 [1-9]表示数字1-9中的任意一个 \d表示任意一个数字 ?表示前面一个字符或子表达式出现0或1次 $表示匹配字符串结束位置 |表示或 最终的解释为:匹配满…...

python实战应用讲解-【numpy数组篇】常用函数(八)(附python示例代码)
目录 Python Numpy MaskedArray.cumprod()函数 Python Numpy MaskedArray.cumsum()函数 Python Numpy MaskedArray.default_fill_value()函数 Python Numpy MaskedArray.flatten()函数 Python Numpy MaskedArray.masked_equal()函数 Python Numpy MaskedArray.cumprod()函…...

Speech and Language Processing-之N-gram语言模型
正如一句老话所说,预测是困难的,尤其是预测未来。但是,如何预测一些看起来容易得多的事情,比如某人接下来要说的几句话后面可能跟着哪个单词。 希望你们大多数人都能总结出一个很可能的词是in,或者可能是over&#x…...

【AI】Python 安装时启用长路径支持
文章目录 场景:解释:关于文件长路径:计算方法: 场景: Python 安装时,会出现 Disable path length limit 的提示。 解释: 在 Windows 操作系统中,文件路径的长度是有限制的。在早期…...

深入理解Go语言中的接口编程【17】
文章目录 接口接口接口类型为什么要使用接口接口的定义实现接口的条件接口类型变量值接收者和指针接收者实现接口的区别值接收者实现接口指针接收者实现接口下面的代码是一个比较好的面试题 类型与接口的关系一个类型实现多个接口多个类型实现同一接口接口嵌套 空接口空接口的定…...

“数字中国·福启海丝”多屏互动光影艺术秀27日在福州举办
作为深化“数字海丝”的核心区、海上丝绸之路的枢纽城市,为喜迎第六届数字中国建设峰会盛大召开之际,福州市人民政府特此举办“数字中国福启海丝”多屏互动光影秀活动。本次光影秀活动是由福建省文化和旅游厅指导,福州市人民政府主办…...

Docker安装mysql8.0文档
第一步需要安装Docker基础环境,具体可以看看这篇 docker基础篇 第二步,拉取mysql8.0的镜像 docker pull mysql:8.0 第三步,镜像启动和文件挂载 复制下面命令执行,33006是对外访问暴露的端口,当然你也可以设置为3306…...

在函数中使用变量
shell脚本编程系列 向函数传递参数 函数可以使用标准的位置变量来表示在命令行中传给函数的任何参数。其中函数名保存在$0变量中,函数参数则依次保存在$1、$2等变量当中,也可以使用特殊变量$#来确定参数的个数 在脚本中调用函数时,必须将参…...

python算法中的深度学习算法之自编码器(详解)
目录 学习目标: 学习内容: 自编码器 Ⅰ. 编码器(Encoder) Ⅱ. 解码器(Decoder)...

Python入门(一)Python概述与环境搭建
Python概述与环境搭建 1.概述1.1版本及下载1.2 Python 特点 2.环境搭建3.第一个程序“hello,world”4.可能会存在的问题 1.概述 Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的可读性,相比其他语言…...

02_Lock锁
首先看一下JUC的重磅武器——锁(Lock) 相比同步锁,JUC包中的Lock锁的功能更加强大,它提供了各种各样的锁(公平锁,非公平锁,共享锁,独占锁……),所以使用起来…...

面试总结,4年经验
小伙伴你好,我是田哥。 本文内容是一位星球朋友昨天面试遇到的问题,我把核心的问题整理出来了。 1:Java 层面的锁有用过吗?除了分布式锁以外 是的,Java中提供了多种锁机制来保证并发访问数据的安全性和一致性。常见的J…...

享受简单上传体验:将Maven仓库迁移到GitHub
前言:我为什么放弃了Maven Central 之前我写过一篇《Android手把手,发布开源组件至 MavenCentral仓库》,文中详细介绍了如何发布组件到Maven Central中供所有开发者共用。但是最近使用下来,发现Sonatype JIRA 的Maven Center上传…...

R语言 | 进阶字符串的处理
目录 一、语句的分割 二、修改字符串的大小写 三、unique()函数的使用 四、字符串的连接 4.1 使用paste()函数常见的失败案例1 4.2 使用paste()函数常见的失败案例2 4.3 字符串的成功连接与collapse参数 4.4 再谈paste()函数 4.5 扑克牌向量有趣的应用 五、字符串数据的…...

【MySQL高级】——InnoDB索引MyISAM索引
一、索引概述 MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。 索引的本质:索引是数据结构。你可以简单理解为“排好序的快速查找数据结构”,满足特定查找算法。 这些数据结构以某种方式指向…...

电影《灌篮高手》观后
上周和同学一起看了电影《灌篮高手》这部电影,个人以前没有看过相关漫画和动画,但记得,看过海报和一些宣传物品,有的衣服上,有文具盒上,也都出现过,而且是在自己小时候,可见当时的影…...

C# .Net 中的同步上下文
.Net 中的同步上下文 【文 / 张赐荣】 什么是同步上下文? 同步上下文(SynchronizationContext)是一个抽象类,它提供了一个基本的功能,用于在不同的同步模型中传播一个同步操作。 同步上下文表示一个代码执行的位置&a…...

3分钟入门:Flex 布局
flex 布局原理 全称 flexible box,弹性布局。 如何开启:为元素添加 display: flex。 开启 flex 布局的元素,称为 flex 容器(flex container),其子元素成为容器成员,称为 flex 项目。 flex 布…...

我想知道,就目前形势而言,学java好还是C++好?
前言 就现实点看看,可以对比现在Java和C的市场占有率,可以看到,到目前为止,Java在国内编程语言的市场仍然是占据着大头,在招聘当中Java的人数占有率仍然是遥遥领先于C,Java目前开阔的市场以及其巨大的岗位…...

Mysql 管理
目录 0 课程视频 1 系统数据库 -> 安装完mysql ->自带四个数据库 2 常用工具 -> 写脚本用 2.1 mysql 客户端工具 2.2 mysqladmin 2.3 mysqlbinlog -> 二进制日志 -> 运维讲解 2.4 mysqlshow 2.5 mysqldump 备份用 ->导出 2.6 mysqlimport/source -…...

C#基础(算术运算符)
作用 算术运算符 是用于 数值类型变量计算的运算符 它的返回结果是数值 赋值符号 // // 关键知识点: // 先看右侧 再看左侧 把右侧的值赋值给左侧的值 int myAge 18; 算术运算符 加 // 用自己计算 先算右侧结果 在赋值给左侧变量 int i 1; i i 2; …...

BM43-包含min函数的栈
题目 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。 此栈包含的方法有: push(value):将value压入栈中pop():弹出栈顶元素top():获取…...

[学习笔记] [机器学习] 3. KNN( K-近邻算法)及练习案例
视频链接数据集下载地址:《3. KNN及练习案例》配套数据集 1. K-近邻算法(KNN)概念 学习目标: 掌握K-近邻算法实现过程知道K-近邻算法的距离公式知道K-近邻算法的超参数 K K K值以及取值问题知道kd树实现搜索的过程应用KNeighborsClassifier实现分类知…...

React Hooks 钩子函数错误用法,你还在犯这些错误吗
React Hooks 常见错误 前言 本片文章主要是在写react hooks的时候,遇到的常见错误的写法,和错误。也是一个对只是的巩固和总结。 错误一 上代码:正确写法 function TestReactHooksError() {const [test, setTest] useState(test);useEff…...

tpm2-tools源码分析之tpm2_evictcontrol.c(1)
TPM 2.0中的tpm2_evictcontrol命令对应的源文件就是tpm2_evictcontrol.c,该文件位于tpm2-tools/tools/下,一共有339行(版本5.5)。 tpm2_evictcontrol的功能是使一个被加载的密钥持久保存、或者从TPM中移除一个持久密钥。命令描述…...

SpringCloud_OpenFeign服务调用和Resilience4J断路器
文章目录 一、负载均衡概论1、服务器负载均衡2、客户端负载均衡3、客户端负载均衡策略(SpringCloudRibbon)4、客户端负载均衡策略(SpringCloudLoadBalancer) 二、SpringCloudOpenFeign服务调用1、OpenFeign服务调用的使用2、OpenFeign服务调用的日志增强3、OpenFeign服务调用超…...