赞
踩
搜索功能是一个系统的重要功能,是信息查询的方式。课程搜索是课程展示的渠道,用户通过课程搜索找到课程信息,进一步去查看课程的详细信息,进行选课、支付、学习。
本项目的课程搜索支持全文检索技术,什么是全文检索?
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。
全文检索可以简单理解为通过索引搜索文章。
全文检索的速度非常快,早期应用在搜索引擎技术中,比如:百度、google等,现在通常一些大型网站的搜索功能都是采用全文检索技术。
课程搜索也要将课程信息建立索引,在课程发布时建立课程索引,索引建立好用户可通过搜索网页去查询课程信息。
所以,课程搜索模块包括两部分:课程索引、课程搜索。
课程索引是将课程信息建立索引。
课程搜索是通过前端网页,通过关键字等条件去搜索课程。
根据模块介绍的内容,课程搜索模块包括课程索引、课程搜索两部分。
1、课程索引
在课程发布操作执行后通过消息处理方式创建课程索引,如下图:
本项目使用elasticsearch作为索引及搜索服务。
2、课程搜索
课程索引创建完成,用户才可以通过前端搜索课程信息。
课程搜索可以从首页进入搜索页面。
下图是搜索界面,可以通过课程分类、课程难度等级等条件进行搜索。
在课前下发的虚拟中已经在docker容器中安装了elasticsearch和kibana。
kibana 是 ELK(Elasticsearch , Logstash, Kibana )之一,kibana 一款开源的数据分析和可视化平台,通过可视化界面访问elasticsearch的索引库,并可以生成一个数据报表。
开发中主要使用kibana通过api对elasticsearch进行索引和搜索操作,通过浏览器访问 http://192.168.101.65:5601/app/dev_tools#/console进入kibana的开发工具界面。
可通过命令:GET /_cat/indices?v 查看所有的索引,通过此命令判断kibana是否正常连接elasticsearch。
下边创建搜索工程,此工程作为项目的搜索服务,提供索引和搜索两部分的功能。
pom.xml如下:
| XML_<?_xml version="1.0" encoding="UTF-8"_?>_
](https://maven.apache.org/xsd/maven-4.0.0.xsd%22%3E)
4.0.0
xuecheng-plus-parent
com.xuecheng
0.0.1-SNAPSHOT
…/xuecheng-plus-parent
xuecheng-plus-search
<dependencies> <dependency> <groupId>com.xuecheng</groupId> <artifactId>xuecheng-plus-base</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </dependency> <!-- Spring Boot 的 Spring Web MVC 集成 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 排除 Spring Boot 依赖的日志包冲突 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- Spring Boot 集成 Junit --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot 集成 log4j2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <!-- Spring Boot 集成 swagger --> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
bootstrap.yml配置如下:
| YAMLspring:
application:
name: search
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery:
namespace: ${spring.profiles.active}
group: xuecheng-plus-project
config:
namespace:
s
p
r
i
n
g
.
p
r
o
f
i
l
e
s
.
a
c
t
i
v
e
g
r
o
u
p
:
x
u
e
c
h
e
n
g
−
p
l
u
s
−
p
r
o
j
e
c
t
f
i
l
e
−
e
x
t
e
n
s
i
o
n
:
y
a
m
l
r
e
f
r
e
s
h
−
e
n
a
b
l
e
d
:
t
r
u
e
s
h
a
r
e
d
−
c
o
n
f
i
g
s
:
−
d
a
t
a
−
i
d
:
s
w
a
g
g
e
r
−
{spring.profiles.active} group: xuecheng-plus-project file-extension: yaml refresh-enabled: true shared-configs: - data-id: swagger-
spring.profiles.activegroup:xuecheng−plus−projectfile−extension:yamlrefresh−enabled:trueshared−configs:−data−id:swagger−{spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
- data-id: logging-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
profiles:
active: dev |
| — |
在nacos添加search-dev.yaml
| YAMLserver:
servlet:
context-path: /search
port: 63080
elasticsearch:
hostlist: 192.168.101.65:9200 #多个结点中间用逗号分隔
course:
index: course-publish
source_fields: id,name,grade,mt,st,charge,pic,price,originalPrice,teachmode,validDays,createDate |
| — |
编写elasticsearch配置类ElasticsearchConfig
| Javapackage com.xuecheng.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.hostlist}") private String hostlist; @Bean public RestHighLevelClient restHighLevelClient(){ //解析hostlist配置信息 String[] split = hostlist.split(","); //创建HttpHost数组,其中存放es主机和端口的配置信息 HttpHost[] httpHostArray = new HttpHost[split.length]; for(int i=0;i<split.length;i++){ String item = split[i]; httpHostArray[i] = new HttpHost(item.split(":")[0], Integer._parseInt_(item.split(":")[1]), "http"); } //创建RestHighLevelClient客户端 return new RestHighLevelClient(RestClient._builder_(httpHostArray)); }
} |
---|
要使用elasticsearch需要建立索引,索引相当于MySQL中的表,Elasticsearch与MySQL之间概念的对应关系见下表:
1、创建索引,并指定Mapping。
PUT /course-publish
| JSON{
“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”
}
}
}
} |
---|
2、查询索引
通过 GET /_cat/indices?v 查询所有的索引,查找course-publish是否创建成功。
通过GET /course-publish/_mapping 查询course-publish的索引结构。
| JSON{
“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”
}
}
}
}
} |
---|
3、删除索引
如果发现创建的course-publish不正确可以删除重新创建。
删除索引后当中的文档数据也同时删除,一定要谨慎操作!
删除索引命令:DELETE /course-publish
索引创建好就可以向其它添加文档,此时elasticsearch会根据索引的mapping配置对有些字段进行分词。
这里我们要向course_publish中添加课程信息。
使用rest api进行测试,如下:
使用post请求,/course-publish/_doc/103 第一部分为索引名称,_doc固定,103为文档的主键id,这里为课程id。
课程内容使用json表示。
| JSONPOST /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
} |
---|
如果要修改文档的内容可以使用上边相同的方法,如果没有则添加,如果存在则更新。
添加文档成功后可以通过主键id查询该文档的信息。
语法如下:
JSONGET /{索引库名称}/_doc/{id} |
---|
更新文档分为全量更新和局部更新。
全量更新是指先删除再更新,语法如下:
| JSONPUT /{索引库名}/_doc/文档id
{
“字段1”: “值1”,
“字段2”: “值2”,
// … 略
} |
---|
局部更新语法如下:
| JSONPOST /{索引库名}/_update/文档id
{
“doc”: {
“字段名”: “新的值”,
}
} |
---|
删除文档将从索引中删除文档的记录。
语法如下:
JSONDELETE /{索引库名}/_doc/id值 |
---|
当课程发布时请求添加课程接口添加课程信息到索引,当课程下架时请求删除课程接口从索引中删除课程信息,这里先实现添加课程接口。
根据索引的mapping结构创建po类:
| Javapackage com.xuecheng.search.po;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
课程索引信息
@author itcast
*/
@Data
public class CourseIndex implements Serializable {
private static final long _serialVersionUID _= 1L;
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
/**
} |
---|
创建索引接口如下:
| Javapackage com.xuecheng.search.controller;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
@author Mr.M
@version 1.0
@description 课程索引接口
@date 2022/9/24 22:31
*/
@Api(value = “课程信息索引接口”, tags = “课程信息索引接口”)
@RestController
@RequestMapping(“/index”)
public class CourseIndexController {
@ApiOperation(“添加课程索引”)
@PostMapping(“course”)
public Boolean add(@RequestBody CourseIndex courseIndex) {
}
} |
| — |
定义service接口,请求elasticsearch添加课程信息。
注意:为了适应其它文档信息,将添加文档定义为通用的添加文档接口,此接口不仅适应添加课程还适应添加其它信息。
| Javapackage com.xuecheng.search.service;
import com.xuecheng.search.po.CourseIndex;
/**
@author Mr.M
@version 1.0
@description 课程索引service
@date 2022/9/24 22:40
*/
public interface IndexService {
/**
} |
---|
接口实现如下:
| Javapackage com.xuecheng.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
@Autowired
RestHighLevelClient client;
@Override
public 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”);
}
} |
---|
完善接口:
| Javapackage com.xuecheng.search.controller;
import com.xuecheng.base.execption.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
@author Mr.M
@version 1.0
@description 课程索引接口
@date 2022/9/24 22:31
*/
@Api(value = “课程信息索引接口”, tags = “课程信息索引接口”)
@RestController
@RequestMapping(“/index”)
public class CourseIndexController {
@Value(“${elasticsearch.course.index}”)
private String courseIndexStore;
@Autowired
IndexService 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;
}
} |
| — |
使用httpclient进行测试
| JSON### 添加课程索引
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
} |
---|
索引信息维护完成下一步定义搜索接口搜索课程信息,首先需要搞清楚搜索功能的需求。
进入搜索界面,如下图:
根据搜索界面可知需求如下:
1、根据一级分类、二级分类搜索课程信息。
2、根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容。
3、根据难度等级搜索课程。
4、搜索结点分页显示。
技术点:
1、整体采用布尔查询。
2、根据关键字搜索,采用MultiMatchQuery,搜索name、description字段。
3、根据分类、课程等级搜索采用过虑器实现。
4、分页查询。
5、高亮显示。
为什么课程分类、课程等待等查询使用过虑器方式?
1、定义搜索条件DTO类
| Javapackage com.xuecheng.search.dto;
import lombok.Data;
import lombok.ToString;
/**
//关键字
private String keywords;
//大分类
private String mt;
//小分类
private String st;
//难度等级
private String grade;
} |
---|
2、为了适应后期的扩展,定义搜索结果类,让它继承PageResult
| Javapackage com.xuecheng.search.dto;
import com.xuecheng.base.model.PageResult;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
@author Mr.M
@version 1.0
@description TODO
_ _* @date 2022/9/25 17:51
*/
@Data
@ToString
public class SearchPageResultDto extends PageResult {
public SearchPageResultDto(List items, long counts, long page, long pageSize) {
super(items, counts, page, pageSize);
}
} |
---|
接口定义如下:
| Javapackage com.xuecheng.search.controller;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
@ApiOperation(“课程搜索列表”)
@GetMapping(“/list”)
public PageResult list(PageParams pageParams, SearchCourseParamDto searchCourseParamDto){
}
} |
---|
定义service接口,如下:
| Javapackage com.xuecheng.search.service;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;
/**
@description 课程搜索service
@author Mr.M
@date 2022/9/24 22:40
@version 1.0
*/
public interface CourseSearchService {
/**
} |
---|
搜索接口的内容较多,我们分几步实现,首先实现根据分页搜索,接口实现如下:
| Javapackage com.xuecheng.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
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.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
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 Mr.M
@version 1.0
@description 课程搜索service实现类
@date 2022/9/24 22:48
*/
@Slf4j
@Service
public class CourseSearchServiceImpl implements CourseSearchService {
@Value(“
e
l
a
s
t
i
c
s
e
a
r
c
h
.
c
o
u
r
s
e
.
i
n
d
e
x
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
c
o
u
r
s
e
I
n
d
e
x
S
t
o
r
e
;
@
V
a
l
u
e
(
"
{elasticsearch.course.index}") private String courseIndexStore; @Value("
elasticsearch.course.index")privateStringcourseIndexStore;@Value("{elasticsearch.course.source_fields}”)
private String sourceFields;
@Autowired
RestHighLevelClient client;
@Override
public SearchPageResultDto 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;
}
} |
---|
当输入查询条件时会查询全部课程信息并支持分页查询。
1、准备测试
启动nginx、网关、搜索服务。
使用kibana通过rest api向索引库添加课程信息,或通过httpclient添加课程信息,至少添加两条信息。
2、进入搜索界面
默认查询出刚才添加的课程信息。
3、修改分页参数测试分页
打开course/ search.html页面 ,找到如下图所示位置:
修改pageSize为1,即一页显示一条记录。
刷新搜索界面,每页显示一条记录,如下图:
下边实现根据关键、一级分类、二级分类、难度等级搜索。
| Java@Override
public SearchPageResultDto 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;
} |
---|
进入搜索界面,输入关键字进行测试。
一级分类、二级分类在下边的聚合搜索中测试。
搜索界面上显示的一级分类、二级分类来源于搜索结果,使用聚合搜索实现找到搜索结果中的一级分类、二级分类。
1、首先在搜索结构DTO中添加一级分类、二级分类列表
| Javapackage com.xuecheng.search.dto;
import com.xuecheng.base.model.PageResult;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
@author Mr.M
@version 1.0
@description TODO
_ _* @date 2022/9/25 17:51
*/
@Data
@ToString
public class SearchPageResultDto extends PageResult {
//大分类列表
List mtList;
//小分类列表
List stList;
public SearchPageResultDto(List items, long counts, long page, long pageSize) {
super(items, counts, page, pageSize);
}
} |
---|
2、搜索方法如下:
| Java@Override
public SearchPageResultDto 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;
} |
---|
进入搜索界面,观察搜索请求的响应内容中是否存在mtList和stList.
观察页面一级分类、二级分类是否有分类信息。
注意:当选中一个一级分类时才会显示二级分类。
最后实现关键词在课程名称中高亮显示。
| Java@Override
public SearchPageResultDto 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); //取出source Map<String, Object> sourceAsMap = hit.getSourceAsMap(); //课程id Long 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;
} |
---|
输入关键字,观察搜索结果,标题中是否对关键字信息进行高亮显示。
5.5 课程发布任务完善
5.5.1 需求分析
执行课程发布操作后,由消息处理机制向elasticsearch索引保存课程信息,本节实现该操作。
执行流程如下:
由内容管理服务远程调用搜索服务,添加课程信息索引。
搜索服务请求elasticsearch添加课程信息。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。