文章目录
课程搜索需求分析
需求分析
1.根据分类搜索课程信息
2.根据关键字搜索课程信息
3.根据难度等级搜索课程信息
4.搜索节点分页显示
搜索流程
1.课程管理服务将数据写到mysql
2.使用Logstash将Mysql数据库中的数据写到ES索引库中.
3.用户在前端搜索课程信息,请求到搜索服务器.
4.搜索服务请求ES搜索课程信息.
全文检索技术研究
课程索引
技术方案
如何维护索引信息?
1.在mysql中添加数据后,也要同时在ES中添加
(采用Logstach)实现,Logstash会从Mysql中将数据采集到es索引库中
2.在mysql中更新数据后同时在ES中更新该课程
采用Logstach实现
3.在mysql中删除数据后在ES中删除数据,
需要手动添加,在删除课程之后再索引库(ES)中删除
准备课程缩影信息
创建课程发布表
首先创建一个coursePubRepository
package com.xuecheng.manage_course.dao;
import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CoursePub;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Created by Administrator.
*/
public interface CoursePubRepository extends JpaRepository<CoursePub,String> {
}
Service:
// 课程发布
@Transactional
public CoursePublishResult publish(String id) {
// 1.这个接口实现什么功能?
CmsPage cmsPage = new CmsPage();
cmsPage.setSiteId(publish_siteId);
cmsPage.setDataUrl(publish_dataUrlPre + id);
cmsPage.setPageName(id + ".html");
cmsPage.setPageAliase(getCourseBaseById(id).getName());
cmsPage.setPagePhysicalPath(publish_page_physicalpath);
cmsPage.setPageWebPath(publish_page_webpath);
cmsPage.setTemplateId(publish_templateId);
// 1.调用cms一键发布接口,将课程详情页面发布到服务器中
CmsPostPageResult cmsPostPageResult = cmsPageClient.postPageQuick(cmsPage);
if (!cmsPostPageResult.isSuccess()) {
//如果不成功,就说明发布失败
return new CoursePublishResult(CommonCode.FAIL, null);
}
// 2.保存课程的发布状态为已发布
CourseBase courseBase = this.saveCoursePubState(id);
// 保存课程索引的一些信息
// 先创建一个CoursePub对象
// 这里调用下面的createCoursePub方法
CoursePub coursePub = createCoursePub(id);
// 将CoursePub对象,保存到数据库(现在外面定义一个单独的)
// 在下面再创建一个方法,将coursePub保存到数据库
// 写好之后,在这里调用
saveCoursePub(id,coursePub);
// 保存缓存的一些信息
//得到页面的url
String pageUrl = cmsPostPageResult.getPageUrl();
return new CoursePublishResult(CommonCode.SUCCESS, pageUrl);
}
// 将coursePub对象保存到数据库
private CoursePub saveCoursePub(String id, CoursePub coursePub) {
CoursePub coursePubNew = null;
// 根据课程id查询coursePub
Optional<CoursePub> coursePubOptional = coursePubRepository.findById(id);
if (coursePubOptional.isPresent()) {
coursePubNew = coursePubOptional.get();
} else {
// 如果查不到的话,那么就只能new一个了
// 如果数据库查到了,那么就用数据库里面的,如果没有查到,那么就new一个
coursePubNew = new CoursePub();
// 将coursePub中的对象,保存到CoursePubNew中
BeanUtils.copyProperties(coursePub, coursePubNew);
// 为了避免参数中的coursePub中的一些数据为空,这里还需要再设置一下
// 主键id
coursePubNew.setId(id);
// 时间戳,主要是给logstach使用
coursePubNew.setTimestamp(new Date());
// 发布时间
// 这里需要注意一下,因为pulish_time在存储中是一个字符串的形式,所以这里需要修改一下
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
String date = simpleDateFormat.format(new Date());
// 这样子的话,就将时间设置为varchar类型了
coursePubNew.setPubTime(date);
coursePubRepository.save(coursePubNew);
// 最后将这个对象返回给调用方
// 就完事了
}
return coursePubNew;
}
// 创建一个Pub对象,然后一会再去保存
private CoursePub createCoursePub(String id) {
CoursePub coursePub = new CoursePub();
// 根据id查询course_base
Optional<CourseBase> baseOptional = courseBaseRepository.findById(id);
if (baseOptional.isPresent()) {
CourseBase courseBase = baseOptional.get();
// 将CourseBase的属性写到CoursePub中
BeanUtils.copyProperties(courseBase, coursePub);
}
// 课程图片
// 根据id查询Course_pic
// 将对象,全部查询出来后,放入对象即可
Optional<CoursePic> picOptional = coursePicRepository.findById(id);
if (picOptional.isPresent()) {
CoursePic coursePic = picOptional.get();
BeanUtils.copyProperties(coursePic, coursePub);
}
// 课程营销信息
Optional<CourseMarket> marketOptional = courseMarketRepository.findById(id);
if (marketOptional.isPresent()) {
CourseMarket courseMarket = marketOptional.get();
BeanUtils.copyProperties(courseMarket, coursePub);
}
// 课程计划信息和上面的三个不大一样
TeachplanNode teachplanNode = teachplanMapper.selectList(id);
// 获得的是Json信息,将他转换成为一个字符串
String jsonString = JSON.toJSONString(teachplanNode);
// 将课程计划信息的json串保存到course_pub中
coursePub.setTeachplan(jsonString);
return coursePub;
}
创建课程发布表模型
修改课程发布
搭建ES环境
ES安装库
在postman中删除掉之前的数据,然后新建一个course_andrew数据,
创建索引库
创建映射
{
"properties":{
"description":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart",
"type":"text"},
"grade":{
"type":"keyword"},
"id":{
"type":"keyword"},
"mt":{
"type":"keyword"},
"name":{"analyzer":"ik_max_word",
"search_analyzer":"ik_smart","type":"text"
},"users":{
"index":false,"type":"text"
},
"charge":{
"type":"keyword"},
"valid":{
"type":"keyword"},
"pic":{
"index":false,"type":"keyword"
},
"qq":{
"index":false,"type":"keyword"
},
"price":{
"type":"float"},
"price_old":{"type":"float"
},
"st":{
"type":"keyword"},
"status":{
"type":"keyword"},
"studymodel":{"type":"keyword"
},"teachmode":{
"type":"keyword"},
"teachplan":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart",
"type":"text"},
"expires":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
},"pub_time":{
"type":"date",
"format":"yyyy‐MM‐ddHH:mm:ss"},
"start_time":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
},"end_time":{
"type":"date","format":"yyyy‐MM‐ddHH:mm:ss"
}}}
LogStash创建索引
Logstash是ES下的一款开源软件,它能够同时从多个来源采集数据,转换数据,然后将数据发送到Elasticsearch中创建索引.
本项目使用Logstash将Mysql中的数据采用到ES索引中
下载Logstash
(这里主要要和es的版本保持一致)
安装Logstash-input-jdbc
logstash-input-jdbc是ruby开发的,先下载ruby并安装下载地址:https://rubyinstaller.org/downloads/
下载2.5版本即可。
安装完成查看是否安装成功
Logstash5.x以上版本本身自带有logstash-input-jdbc,6.x版本本身不带logstash-input-jdbc插件,需要手动安装
安装成功后我们可以在logstash根目录下的以下目录查看对应的插件版本
创建模板文件
Logstash的工作是从MySQL中读取数据,向ES中创建索引,这里需要提前创建mapping的模板文件以便logstash使用。
在logstach的config目录创建xc_course_template.json,内容如下:
本教程的xc_course_template.json目录是:D:/ElasticSearch/logstash-6.2.1/config/xc_course_template.json
{
"mappings":{"doc":{
"properties":{"charge":{
"type":"keyword"},
"description":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"end_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"expires":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"grade":{
"type":"keyword"},
"id":{
"type":"keyword"},
"mt":{
"type":"keyword"},
"name":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"pic":{
"index":false,"type":"keyword"
},
"price":{
"type":"float"},
"price_old":{"type":"float"
},
"pub_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"qq":{
"index":false,"type":"keyword"
},
"st":{
"type":"keyword"},
"start_time":{
"format":"yyyy‐MM‐ddHH:mm:ss","type":"date"
},
"status":{
"type":"keyword"},
"studymodel":{"type":"keyword"
},
"teachmode":{"type":"keyword"
},
"teachplan":{
"analyzer":"ik_max_word","search_analyzer":"ik_smart","type":"text"
},
"users":{"index":false,"type":"text"
},
"valid":{
"type":"keyword"}
}}
},
"template":"xc_course"}
配置mysql.conf
在logstash的config目录下配置mysql.conf文件供logstash使用,logstash会根据mysql.conf文件的配置的地址从MySQL中读取数据向ES中写入索引。
参考https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html配置输入数据源和输出数据源。
input{
stdin{
}
jdbc{
jdbc_connection_string=>"jdbc:mysql://localhost:3306/xc_course?useUnicode=true&characterEncoding=utf‐8&useSSL=true&serverTimezone=UTC"
#theuserwewishtoexcuteourstatementas
jdbc_user=>"root"
jdbc_password=>mysql
#thepathtoourdownloadedjdbcdriver
jdbc_driver_library=>"F:/develop/maven/repository3/mysql/mysql‐connector‐java/5.1.41/mysql‐connector‐java‐5.1.41.jar"
#thenameofthedriverclassformysql
jdbc_driver_class=>"com.mysql.jdbc.Driver"
jdbc_paging_enabled=>"true"
jdbc_page_size=>"50000"
#要执行的sql文件
#statement_filepath=>"/conf/course.sql"
statement=>"select*fromcourse_pubwheretimestamp>date_add(:sql_last_value,INTERVAL8HOUR)"
#定时配置
schedule=>"*****"
record_last_run=>true
last_run_metadata_path=>"D:/ElasticSearch/logstash‐6.2.1/config/logstash_metadata"
}}
output{elasticsearch{#ES的ip地址和端口
hosts=>"localhost:9200"
#hosts=>["localhost:9200","localhost:9202","localhost:9203"]#ES索引库名称
index=>"xc_course"document_id=>"%{id}"
document_type=>"doc"
template=>"D:/ElasticSearch/logstash‐6.2.1/config/xc_course_template.json"template_name=>"xc_course"
template_overwrite=>"true"}
stdout{#日志输出
codec=>json_lines}
}
说明:
1、ES采用UTC时区问题
ES采用UTC时区,比北京时间早8小时,所以ES读取数据时让最后更新时间加8小时wheretimestamp>date_add(:sql_last_value,INTERVAL8HOUR)
2、logstash每个执行完成会在D:/ElasticSearch/logstash-6.2.1/config/logstash_metadata记录执行时间下次以此时间为基准进行增量同步数据到索引库。
测试
在logstash中的bin路径下执行命令
logstash.bat -f ../config/mysql.conf
我这里调用的是以前的项目,需要修改一下那个时间把>换成<,否则数据不会被选中
,不过在修改完成后需要再改回来,否则更新时间戳之后,就不会导入了
输入命令后采集到数据
课程搜索
需求分析
1.更具分类搜索课程信息
2.根据关键字搜索课程信息,全文检索,关键字匹配课程名称,课程内容
3.根据难度搜索课程信息
4.搜索节点分页显示.
技术分析:
1.根据关键字搜索,采用MultiMatchQuery,搜索name,description.teachplan
2.根据分类,课程等级搜索,采用过滤器实现.
3.分页查询.
4.高亮显示
创建搜索服务
API
再APi中添加search包,在其中建立ES接口
package com.xuecheng.api.search;
import com.xuecheng.framework.domain.course.CoursePub;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.QueryResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2022/12/28 18:39:34
*/
@Api(value = ("课程搜索功能"),description = "课程搜索",tags = {"课程搜索"})
public interface EsCourseControllerApi {
@ApiOperation(value = "课程综合搜索")
public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam);
}
Service
这里方法比较多,复杂,分成三部
package com.xuecheng.search.service;
import com.xuecheng.framework.domain.course.CoursePub;
import com.xuecheng.framework.domain.search.CourseSearchParam;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author Andrewer
* @version 1.0
* @project xcEduService01
* @description
* @date 2022/12/29 20:39:45
*/
@Service
public class EsCourseService {
@Value("${xuecheng.course.index}")
private String index;
@Value("${xuecheng.course.type}")
private String type;
@Value("${xuecheng.course.source.field}")
private String source_field;
@Autowired
RestHighLevelClient restHighLevelClient;
// 课程搜索
public QueryResponseResult<CoursePub> list(int page, int size, CourseSearchParam courseSearchParam) {
if (courseSearchParam == null) {
courseSearchParam = new CourseSearchParam();
}
// 创建请求对象
// 注意这里的索引库名称不要写死,后期变了怎么办?
SearchRequest searchRequest = new SearchRequest(index);
// 设置搜索的类型
searchRequest.types(type);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 指定过滤的原字段
String[] source_field = this.source_field.split(",");
searchSourceBuilder.fetchSource(source_field, new String[]{});
// 创建一个布尔查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 搜索条件,根据关键字搜索
if (StringUtils.isNotEmpty(courseSearchParam.getKeyword())) {
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeyword(), "name", "description", "teachplan")
.minimumShouldMatch("70%")
.field("name", 10);
boolQueryBuilder.must(multiMatchQueryBuilder);
}
// 根据分类
// .....
//
// 设置boolQueryBuilder到searchSourceBuilder
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
QueryResult<CoursePub> queryResult = new QueryResult();
List<CoursePub> list = new ArrayList<>();
// 执行搜索
// 这里需要用到客户端,这里要注入客户端
try {
SearchResponse search = restHighLevelClient.search(searchRequest);
// 获取响应结果
SearchHits hits = search.getHits();
// 匹配的总的记录数
long totalHits = hits.getTotalHits();
queryResult.setTotal(totalHits);
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit : hits1) {
CoursePub coursePub = new CoursePub();
// 原文档
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
// 取出name
String name = (String) sourceAsMap.get("name");
coursePub.setName(name);
// 图片
String pic = (String) sourceAsMap.get("pic");
coursePub.setPic(pic);
// 价格
Double price = null;
price = (Double) sourceAsMap.get("price");
coursePub.setPrice(price);
Double price_old = null;
price_old = (Double) sourceAsMap.get("price_old");
coursePub.setPrice_old(price_old);
// 将对象放入list
list.add(coursePub);
}
} catch (IOException e) {
e.printStackTrace();
}
queryResult.setList(list);
QueryResponseResult<CoursePub> responseResult = new QueryResponseResult<>(CommonCode.SUCCESS,queryResult);
return responseResult;
}
}
按关键字搜索
按分类难度和等级搜索
分页与高亮
//分页if(page<=0){
page=1;}if(size<=0){
size=20;}
intstart=(page‐1)*size;searchSourceBuilder.from(start);searchSourceBuilder.size(size);
//高亮设置
HighlightBuilderhighlightBuilder=newHighlightBuilder();highlightBuilder.preTags("<fontclass='eslight'>");highlightBuilder.postTags("</font>");
//设置高亮字段
highlightBuilder.fields().add(newHighlightBuilder.Field("name"));searchSourceBuilder.highlighter(highlightBuilder);
//请求搜索
//取出高亮字段内容
Map<String,HighlightField>highlightFields=hit.getHighlightFields();if(highlightFields!=null){
HighlightFieldnameField=highlightFields.get("name");if(nameField!=null){
Text[]fragments=nameField.getFragments();StringBufferstringBuffer=newStringBuffer();
for(Textstr:fragments){
stringBuffer.append(str.string());}
name=stringBuffer.toString();
}
}coursePub.setName(name);
Controller
@RestController@RequestMapping("/search/course")
publicclassEsCourseControllerimplementsEsCourseControllerApi{@Autowired
EsCourseServiceesCourseService;
@Override@GetMapping(value="/list/{page}/{size}")
publicQueryResponseResult<CoursePub>list(@PathVariable("page")intpage,
@PathVariable("size")intsize,CourseSearchParamcourseSearchParam)throwsIOException{returnesCourseService.list(page,size,courseSearchParam);
}}