课程搜索
课程发布关联改动
课程发布时,同步索引库数据
CoursePubRepository
1 2 3 4 5 6 7
| package com.xuecheng.manage_course.dao;
import com.xuecheng.framework.domain.course.CoursePub; import org.springframework.data.jpa.repository.JpaRepository;
public interface CoursePubRepository extends JpaRepository<CoursePub, String> { }
|
CoursePubService
新增保存课程信息方法并在发布课程成功时调用该方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| @Autowired private CoursePubRepository coursePubRepository;
public CoursePub saveCoursePub(String id, CoursePub coursePub) { if (StringUtils.isBlank(id)) { ExceptionCast.cast(CourseCode.COURSE_PUBLISH_COURSEIDISNULL); } CoursePub coursePubNew = null; Optional<CoursePub> coursePubOptional = coursePubRepository.findById(id); if (coursePubOptional.isPresent()) { coursePubNew = coursePubOptional.get(); } if (coursePubNew == null) { coursePubNew = new CoursePub(); }
BeanUtils.copyProperties(coursePub, coursePubNew); coursePubNew.setId(id); coursePub.setTimestamp(new Date()); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY‐MM‐dd HH:mm:ss"); String date = simpleDateFormat.format(new Date()); coursePub.setPubTime(date); coursePubRepository.save(coursePub); return coursePub;
}
private CoursePub createCoursePub(String id) { CoursePub coursePub = new CoursePub(); coursePub.setId(id);
Optional<CourseBase> courseBaseOptional = courseBaseRepository.findById(id); if (courseBaseOptional.isPresent()) { CourseBase courseBase = courseBaseOptional.get(); BeanUtils.copyProperties(courseBase, coursePub); } 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 = coursePlanMapper.findList(id); String teachPlanString = JSON.toJSONString(teachplanNode); coursePub.setTeachplan(teachPlanString); return coursePub; }
|
ES环境构建
ES安装
参考下列文章:
Elasticsearch的介绍和安装)
Docker安装Elasticsearch)
Docker搭建Elasticsearch集群
创建索引
使用HTTP工具发送请求
请求URL:http://192.168.136.110:9200/xc_course
请求方式:PUT
请求体:
1 2 3 4 5 6 7 8
| { "settings": { "index": { "number_of_shards": "1", "number_of_replicas": "0" } } }
|
创建Mapping
请求URL:http://192.168.136.110:9200/xc_course/doc/_mapping
请求方式:POST
请求体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| { "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-dd HH:mm:ss" }, "pub_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "start_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "end_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" } } }
|
数据导入(省略)
logstash脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| input { stdin { } jdbc { jdbc_connection_string => "jdbc:mysql://192.168.136.110:3306/xc_course?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC" # 数据库连接信息 jdbc_user => "root" jdbc_password => "123456" # 驱动 jdbc_driver_library => "/usr/share/logstash/config/mysql-connector-java-8.0.13.jar" # 驱动类 jdbc_driver_class => "com.mysql.cj.jdbc.Driver" jdbc_paging_enabled => "true" jdbc_page_size => "50000" # 要执行的sql文件 #statement_filepath => "/conf/course.sql" # 执行SQL statement => "select * from course_pub where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)" #定时配置,依次为:分、时、天、月和年,语法与cron表达式类似 schedule => "0 0 * * *" record_last_run => true last_run_metadata_path => "/usr/share/logstash/config/logstash_metadata" } }
filter{ json{ source => "message" remove_field => ["message"] } }
output { elasticsearch { # ES的ip地址和端口 hosts => "192.168.136.110:9200" # 集群配置 #hosts => ["localhost:9200","localhost:9202","localhost:9203"] # ES索引库名称 index => "xc_course" document_id => "%{id}" document_type => "doc" template => "/usr/share/logstash/config/xc_course_template.json" template_name => "xc_course" template_overwrite => "true" } stdout { # 日志输出 codec => json_lines } }
|
课程搜索实现
需求分析
- 根据分类搜索课程信息。
- 根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、课程内容。
- 根据难度等级搜索课程。
- 搜索结点分页显示。
创建搜索微服务
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>xc-framework-parent</artifactId> <groupId>com.xuecheng</groupId> <version>1.0-SNAPSHOT</version> <relativePath>../xc-framework-parent/pom.xml</relativePath> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>xc-service-search</artifactId>
<dependencies> <dependency> <groupId>com.xuecheng</groupId> <artifactId>xc-framework-model</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.xuecheng</groupId> <artifactId>xc-framework-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.xuecheng</groupId> <artifactId>xc-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-io</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>
</project>
|
注意:
我这里使用的客户端是Spring Data Elasticsearch
,和教程上不一样。
application.yml
1 2 3 4 5 6 7 8 9 10 11
| server: port: 40100 spring: application: name: xc‐service‐search data: elasticsearch: cluster-nodes: 192.168.136.110:9300 cluster-name: docker-cluster elasticsearch: source_field: id,name,grade,mt,st,charge,valid,pic,qq,price,price_old,status,studymodel,teachmode,expires,pub_time,start_time,end_time
|
启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package com.xuecheng.search;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication @EntityScan("com.xuecheng.framework.domain.search") @ComponentScan(basePackages={"com.xuecheng.api"}) @ComponentScan(basePackages={"com.xuecheng.search"}) @ComponentScan(basePackages={"com.xuecheng.framework"}) public class SearchApplication { public static void main(String[] args) { SpringApplication.run(SearchApplication.class, args); } }
|
实体类
由于是使用的Spring Data Elasticsearch
,所以需要定义实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| package com.xuecheng.framework.domain.search;
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field;
import java.math.BigDecimal; import java.util.Date;
@Data @AllArgsConstructor @NoArgsConstructor @Document(indexName = "xc_course", type = "doc", shards = 1) public class EsCoursePub {
private static final long serialVersionUID = -916357110051689487L;
@Id private String id;
@Field private String name; @Field private String users; @Field private String mt; @Field private String st; @Field private String grade; @Field private String studymodel; @Field private String teachmode; @Field private String description; @Field private String pic; @Field private Date timestamp; @Field private String charge; @Field private String valid; @Field private String qq; @Field private BigDecimal price; @Field private BigDecimal price_old; @Field private String expires; @Field private String teachplan; @Field private String pub_time;
}
|
注意:
字段必须和索引库中一模一样,比如:pub_time
基础查询实现
EsCourseControllerApi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.xuecheng.api.search;
import com.xuecheng.framework.domain.search.CourseSearchParam; import com.xuecheng.framework.model.response.QueryResponseResult; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation;
@Api(value = "课程搜索", description = "课程搜索", tags = {"课程搜索"}) public interface EsCourseControllerApi {
@ApiOperation("课程搜索") QueryResponseResult list(int page, int size, CourseSearchParam courseSearchParam);
}
|
EsCourseController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.xuecheng.search.controller;
import com.xuecheng.api.search.EsCourseControllerApi; import com.xuecheng.framework.domain.search.CourseSearchParam; import com.xuecheng.framework.model.response.QueryResponseResult; import com.xuecheng.framework.web.BaseController; import com.xuecheng.search.service.EsCourseService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("search/course") public class EsCourseController extends BaseController implements EsCourseControllerApi {
@Autowired private EsCourseService esCourseService;
@Override @GetMapping("list/{page}/{size}") public QueryResponseResult list(@PathVariable int page, @PathVariable int size, CourseSearchParam courseSearchParam) { return esCourseService.findList(page, size, courseSearchParam); } }
|
EsCourseService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| package com.xuecheng.search.service;
import com.xuecheng.framework.domain.search.CourseSearchParam; import com.xuecheng.framework.domain.search.EsCoursePub; import com.xuecheng.framework.model.response.CommonCode; import com.xuecheng.framework.model.response.QueryResponseResult; import com.xuecheng.framework.model.response.QueryResult; import com.xuecheng.framework.service.BaseService; import com.xuecheng.search.config.ElasticsearchConfig; import com.xuecheng.search.dao.CourseRepository; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MultiMatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.stereotype.Service;
@Slf4j @Service public class EsCourseService extends BaseService {
@Autowired private CourseRepository courseRepository;
@Autowired private ElasticsearchConfig elasticsearchConfig;
public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) { if (page < 0) { page = 1; } page = page - 1;
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilder.withPageable(PageRequest.of(page, size));
nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(elasticsearchConfig.getSourceField().split(","), null));
QueryBuilder queryBuilder = buildBasicQuery(courseSearchParam); nativeSearchQueryBuilder.withQuery(queryBuilder);
Page<EsCoursePub> esCoursePubPage = courseRepository.search(nativeSearchQueryBuilder.build());
QueryResult<EsCoursePub> esCoursePubQueryResult = new QueryResult<>(esCoursePubPage.getContent(), esCoursePubPage.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS, esCoursePubQueryResult); }
private QueryBuilder buildBasicQuery(CourseSearchParam courseSearchParam) { BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
if (StringUtils.isNotBlank(courseSearchParam.getKeyword())) { MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders .multiMatchQuery(courseSearchParam.getKeyword(), "name", "teachplan", "description") .minimumShouldMatch("70%") .field("name", 10); queryBuilder.must(multiMatchQueryBuilder); }
return queryBuilder; }
}
|
CourseRepository
1 2 3 4 5 6 7
| package com.xuecheng.search.dao;
import com.xuecheng.framework.domain.search.EsCoursePub; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface CourseRepository extends ElasticsearchRepository<EsCoursePub, String> { }
|
测试
查询关键字JAVA,总记录数为:4
按分类和难度等级过滤
EsCourseService
修改EsCourseService
中的buildBasicQuery
方法,修改后的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
private QueryBuilder buildBasicQuery(CourseSearchParam courseSearchParam) { BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
if (StringUtils.isNotBlank(courseSearchParam.getKeyword())) { MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders .multiMatchQuery(courseSearchParam.getKeyword(), "name", "teachplan", "description") .minimumShouldMatch("70%") .field("name", 10); queryBuilder.must(multiMatchQueryBuilder); }
if (StringUtils.isNotBlank(courseSearchParam.getMt())) { queryBuilder.filter(QueryBuilders.termQuery("mt", courseSearchParam.getMt())); } if (StringUtils.isNotBlank(courseSearchParam.getSt())) { queryBuilder.filter(QueryBuilders.termQuery("st", courseSearchParam.getSt())); }
if (StringUtils.isNotBlank(courseSearchParam.getGrade())) { queryBuilder.filter(QueryBuilders.termQuery("grade", courseSearchParam.getGrade())); }
return queryBuilder; }
|
设置高亮
EsCourseService
Spring Data Elasticsearch的高亮就比较麻烦了
修改EsCourseService
中的findList
方法,加入高亮设置,修改后的代码内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) { if (page < 0) { page = 1; } page = page - 1;
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); nativeSearchQueryBuilder.withPageable(PageRequest.of(page, size));
nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(elasticsearchConfig.getSourceField().split(","), null));
QueryBuilder queryBuilder = buildBasicQuery(courseSearchParam); nativeSearchQueryBuilder.withQuery(queryBuilder);
nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("name").preTags("<font class='eslight'>").postTags("</font>"));
AggregatedPage<EsCoursePub> esCoursePubPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), EsCoursePub.class, new SearchResultMapper() { @Override public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) { List<EsCoursePub> chunk = new ArrayList<>(); for (SearchHit searchHit : response.getHits()) { if (response.getHits().getHits().length <= 0) { return null; } EsCoursePub coursePub = JSON.parseObject(JSON.toJSONString(searchHit.getSource()), EsCoursePub.class); HighlightField nameField = searchHit.getHighlightFields().get("name"); if (nameField != null) { coursePub.setName(nameField.fragments()[0].toString()); }
chunk.add(coursePub); } if (chunk.size() > 0) { return new AggregatedPageImpl<>((List<T>) chunk); } return null; } });
QueryResult<EsCoursePub> esCoursePubQueryResult = new QueryResult<>(esCoursePubPage.getContent(), esCoursePubPage.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS, esCoursePubQueryResult); }
|
代码获取
代码获取