学成在线笔记八:课程搜索
课程搜索

课程发布关联改动

课程发布时,同步索引库数据

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;

/**
* 保存课程信息
*
* @param id 课程ID
* @param coursePub 课程信息
* @return CoursePub
*/
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;

}

/**
* 创建
*
* @param id
* @return
*/
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);
//将课程计划转成json
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
}
}

课程搜索实现

需求分析

  1. 根据分类搜索课程信息。
  2. 根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、课程内容。
  3. 根据难度等级搜索课程。
  4. 搜索结点分页显示。

创建搜索微服务

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"})//扫描common下的所有类
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;

/**
* 查询课程索引
*
* @param page 当前页码
* @param size 每页记录数
* @param courseSearchParam 查询条件
* @return QueryResponseResult
*/
public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) {
if (page < 0) {
page = 1;
}
// Spring Data页码都是从0开始
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);
}

/**
* 构建查询
*
* @param courseSearchParam 查询条件
* @return QueryBuilder
*/
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);// 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
/**
* 构建查询
*
* @param courseSearchParam 查询条件
* @return QueryBuilder
*/
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);// 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
/**
* 查询课程索引
*
* @param page 当前页码
* @param size 每页记录数
* @param courseSearchParam 查询条件
* @return QueryResponseResult
*/
public QueryResponseResult findList(int page, int size, CourseSearchParam courseSearchParam) {
if (page < 0) {
page = 1;
}
// Spring Data页码都是从0开始
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);
}

代码获取

代码获取

文章作者: imxushuai
文章链接: https://www.imxushuai.com/2020/06/10/26.学成在线笔记八:课程搜索/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 imxushuai
支付宝打赏
微信打赏