Spring Boot 操作 Elasticsearch
Spring Data简介
是spring提供的一套连接各种第三方数据源的框架集
其中包括了我们经常使用的mysql\redis\ES等多种数据源软件的连接功能
SpringData也是一个框架集,我们需要选择对应数据源的框架来使用
官方网站:https://spring.io/projects/spring-data
我们可以看到几乎包含了所有我们开发过程中会连接的所有数据源软件
之前我们连接redis其实使用的就是SpringData,只是因为比较简单,没有明确提出
现在我们要来用作简化操作ES
如果没有SpringData,我们需要自己编写socket程序,向ES服务器发送请求并解析响应非常的麻烦
添加依赖
knows-search模块来负责连接和操作ES
后面的搜索功能也会编写在这个模块中
所以我们在这个文件中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
application.properties配置文件
server.port=8003
spring.application.name=search-service
# ES的日志门槛
logging.level.cn.tedu.knows.search=debug
logging.level.org.elasticsearch.client.RestClient=debug
# 指定连接的ES的地址和端口
spring.elasticsearch.rest.uris=http://localhost:920
创建商品类用于测试ES
数据库表会对应一个实体类进行查询等操作
ES也会创建一个对应文档(document)的类似实体类的类型
创建vo包
包中创建Item类,以便之后通过SpringDataElasticsearch对ES进行操作
Item类代码如下
@Data
@Accessors(chain = true)
@AllArgsConstructor // 生成全参构造
@NoArgsConstructor // 生成无参构造
// @Document是SpringData提供的注解
// 指定当前类对应ES中索引的名称,如果对ES操作时
// 指定名称的索引没有创建,SpringData会自动创建该索引
@Document(indexName = "items")
public class Item implements Serializable {
// SpringData框架标记主键的注解@Id
@Id
private Long id;
// type = FieldType.Text表示要分词的字符串
@Field(type = FieldType.Text,
analyzer = "ik_max_word",
searchAnalyzer = "ik_max_word")
private String title;
// type = FieldType.Keyword表示不需要分词的字符串
@Field(type = FieldType.Keyword)
private String category; // 分类
@Field(type = FieldType.Keyword)
private String brand; // 品牌
@Field(type = FieldType.Double)
private Double price; // 价格
// 2022/03/28/hdsjjhsajd.jpg
// 因为当前商品图片地址不会成为搜索条件,所以编写index = false
// 实现只保存信息,不生成索引,来节省一定空间
@Field(type = FieldType.Keyword,index = false)
private String image; // 图片地址
}
创建ES的数据访问层
Mybatis(plus)框架需要编写Mapper接口来实现数据访问层的开发
SpringData也是类似的操作,只是创建的数据访问层接口名称以Repository命名结尾
所以我们创建一个repository包,包中创建ItemRepository接口
代码如下
// Spring家族的框架下都将数据访问层称之为Repository
@Repository
public interface ItemRepository
extends ElasticsearchRepository<Item,Long> {
// ElasticsearchRepository接口的泛型<[ES数据封装类],[主键类型]>
// 一旦我们编写的接口继承了这个父接口
// 我们的ItemRepository就自带了基本的增删改查功能
}
测试代码
测试ItemRepository的基本功能
打开测试类编写代码如下
@Resource
ItemRepository itemRepository;
// 单增
@Test
void addOne() {
Item item=new Item()
.setId(1L)
.setTitle("罗技激光无线游戏鼠标")
.setCategory("鼠标")
.setBrand("罗技")
.setPrice(148.0)
.setImage("/1.jpg");
// 执行新增操作的方法 save()
itemRepository.save(item);
System.out.println("ok");
}
// 按id查询
@Test
void getOne(){
// SpringData自带按id查询对象的方法
// Optional是一个包装类型,能够保存查询出的结果
Optional<Item> optional=itemRepository.findById(1L);
// 需要查询结果内容时,从包装类中取出(get方法)即可
System.out.println(optional.get());
}
// 批量增
@Test
void addList(){
// 实例化一个List对象
List<Item> list=new ArrayList<>();
// 向List中添加Item对象
list.add(new Item(2L,"罗技激光有线办公鼠标",
"鼠标","罗技",68.0,
"/2.jpg"));
list.add(new Item(3L,"雷蛇机械无线游戏键盘",
"键盘","雷蛇",318.0,
"/3.jpg"));
list.add(new Item(4L,"微软有线静音办公鼠标",
"鼠标","微软",128.0,
"/4.jpg"));
list.add(new Item(5L,"罗技有线机械背光键盘",
"键盘","罗技",236.0,
"/5.jpg"));
// 批量新增List
itemRepository.saveAll(list);
System.out.println("ok");
}
// 全查
@Test
void getAll(){
Iterable<Item> items=itemRepository.findAll();
/*for (Item item:items){
System.out.println(item);
}*/
items.forEach(item-> System.out.println(item));
}
SpringData自定义查询
一般情况下,我们能够使用SpringData自带的新增和查询方法直接使用即可
但是如果出现自定义条件的查询,SpringData是不可能默认提供的(例如按某个列进行模糊查询的需求)
我们下面分步骤逐步讲解
单条件查询
我们先来设计一个简单的单条件查询
查询商品名称中包含"游戏"词汇的商品信息
如果这样的查询写出sql语句
select * from item where title like '%游戏%'
但是这样的查询在关系型数据库中效率很低
我们需要在ES中进行查询来优化
那么ES中又怎么实现这个效果呢?
ItemRepository接口中自定义方法
// Spring家族的框架下都将数据访问层称之为Repository
@Repository
public interface ItemRepository
extends ElasticsearchRepository<Item,Long> {
// ElasticsearchRepository接口的泛型<[ES数据封装类],[主键类型]>
// 一旦我们编写的接口继承了这个父接口
// 我们的ItemRepository就自带了基本的增删改查功能
// 自定义查询
// 除了SpringData提供的基本方法,当我们需要按照个性化的逻辑来查询时
// 就需要使用自定义查询
// SpringData框架支持使用方法名称直接表示查询逻辑
// SpringData框架会自动按方法名称生成查询语句进行查询返回结果
// 所以方法名称有既定的语法格式,必须严格按照语法格式编写
// query:表示查询(类似select关键字)
// Items\Item:表示返回值(Item表示返回的类,带s是集合,不带s是对象)
// by:根据什么条件查询(类似where)
// Title:指定查询的属性名
// Matches:相当于sql中的like,即模糊查询
Iterable<Item> queryItemsByTitleMatches(String title);
}
测试类调用
// 单条件查询
@Test
void queryOne(){
// 查询title中包含"游戏"分词的Item对象
Iterable<Item> items=itemRepository
.queryItemsByTitleMatches("游戏");
items.forEach(item -> System.out.println(item));
}
上面查询时SpringData底层向Es发送的请求如下
### 单条件搜索
POST http://localhost:9200/items/_search
Content-Type: application/json
{
"query": {"match": { "title": "游戏" }}
}
多条件查询
查询条件可能不只一个时
多个查询条件之间就有"and","or"的查询逻辑
在上面章节查询条件的基础上添加"brand"属性条件
ItemRepository接口添加方法
// 多条件查询
// 使用And或Or表示查询逻辑
Iterable<Item> queryItemsByTitleMatchesAndBrandMatches(
String title,String brand);
测试代码如下
// 多条件查询
@Test
void queryTwo(){
Iterable<Item> items=itemRepository
.queryItemsByTitleMatchesAndBrandMatches(
"游戏","雷蛇");
items.forEach(item -> System.out.println(item));
}
底层运行的请求
### 多字段搜索
POST http://localhost:9200/items/_search
Content-Type: application/json
{
"query": {
"bool": {
"must": [
{ "match": { "title": "游戏"}},
{ "match": { "brand": "雷蛇"}}
]
}
}
}
排序查询
SpringData查询方法名支持添加排序条件
例如按照价格降序排序的查询
可以在ItemRepository接口中添加下面的方法
// 排序查询
Iterable<Item>
queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
String title,String brand);
测试代码
// 排序查询
@Test
void queryOrder(){
Iterable<Item> items=itemRepository
.queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
"游戏","罗技");
items.forEach(item -> System.out.println(item));
}
底层执行代码
### 多字段搜索
POST http://localhost:9200/items/_search
Content-Type: application/json
{
"query": {
"bool": {
"should": [
{ "match": { "title": "游戏"}},
{ "match": { "brand": "罗技"}}
]
}
},"sort":[{"price":"desc"}]
}
分页查询
SpringData框架支持查询返回分页结果
SpringData中实现分页不需要添加其它依赖
直接使用SpringData自带的分页功能即可
ItemRepository接口添加支持分页查询的方法
// 分页查询
Page<Item>
queryItemsByTitleMatchesOrBrandMatchesOrderByTitleDesc(
String title, String brand, Pageable pageable);
测试代码
//分页查询测试
@Test
void queryPage(){
int pageNum=1;
int pageSize=2;
Page<Item> page=itemRepository.
queryItemsByTitleMatchesOrBrandMatchesOrderByPriceDesc(
"游戏","罗技",
PageRequest.of(pageNum-1,pageSize));
page.forEach(item -> System.out.println(item));
System.out.println("总页数:"+page.getTotalPages());
System.out.println("当前页:"+page.getNumber());
System.out.println("每页条数:"+page.getSize());
System.out.println("是不是首页:"+page.isFirst());
System.out.println("是不是末页:"+page.isLast());
}
实现达内知道的搜索功能
搜索功能的业务流程
完成达内知道项目搜索功能要分为4个阶段
1.同步数据
也就是需要将mysql中question表中的所有数据复制到ES
2.接收用户在页面上输入的关键字,发送到ES进行查询
获得分页的查询结果
3.前端axios调用编写好的方法,将查询结果显示在页面上
4.在学生发布问题功能中,添加将问题保存到Es的操作
同步数据
我们先来实现mysql的question表中的全部数据添加到ES的代码
配置knows-search模块
要想实现数据的同步
首先要让knows-search项目添加到当前微服务结构中,称为一个微服务模块
添加Nacos\网关\Ribbon等支持的配置
还是从pom.xml文件开始
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-search</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 通用模块 -->
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>knows-commons</artifactId>
</dependency>
<!-- 安全框架(用户获得登录用户信息) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 用于将Page类转换为PageInfo -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
</project>
application.properties
额外添加nacos的配置
spring.cloud.nacos.discovery.server-addr=localhost:8848
SpringBoot启动类
@SpringBootApplication
@EnableDiscoveryClient
public class KnowsSearchApplication {
public static void main(String[] args) {
SpringApplication.run(KnowsSearchApplication.class, args);
}
// search模块会利用Ribbon调用faq获得问题表中的信息
// 以便于将这些信息新增到ES中
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
faq模块提供查询所有问题的Rest接口
我们已经明确search模块要通过Ribbon从faq模块获得所有问题
但是faq模块现在并没有查询所有问题的功能
我们要先在faq模块中编写查询所有问题的功能,以便search模块调用
而且这次全查不能一次性全部查询出来,要分页查询
所以下面我们要在faq模块中实现一个分页查询所有问题的方法
转到faq模块的业务逻辑层
IQuestionService接口添加一个能够分页查询所有问题的方法
// 分页查询所有question数据的方法
PageInfo<Question> getQuestions(Integer pageNum,
Integer pageSize);
业务逻辑层实现
QuestionServiceImpl
@Override
public PageInfo<Question> getQuestions(Integer pageNum, Integer pageSize) {
// 设置分页条件(页码和每页条数)
PageHelper.startPage(pageNum,pageSize);
List<Question> list=
questionMapper.selectList(null);
// 返回PageInfo类型对象
return new PageInfo<>(list);
}
控制层代码
// 分页查询全部question数据的方法
@GetMapping("/page")
public List<Question> questions(Integer pageNum,
Integer pageSize){
PageInfo<Question> pageInfo=
questionService.getQuestions(pageNum,pageSize);
// 返回分页结果pageInfo对象中的List
return pageInfo.getList();
}
// 根据每页条数计算总页数的Rest接口
@GetMapping("/page/count")
public int pageCount(Integer pageSize){
// 目标查询总页数
// 先要查询总条数,再根据pageSize值进行计算
// MybatisPlus提供了查询总条数的业务逻辑层方法
// questionService.count()就能实现
int count=questionService.count();
//return count%pageSize==0 ? count/pageSize
// : count/pageSize+1;
return (count+pageSize-1)/pageSize;
}
search模块Ribbon调用
faq模块准备好了Rest接口共数据同步
下面就是转到knows-search模块完成同步调用
首先完成对应Question的Vo类
创建search模块的QuestionVO类
对应ES数据库中的索引,用于后面的搜索
这里我们创建的QuestionVO类的属性也要和Question实体类一致
所以我们可以直接复制commons中model包里的Question类到knows-search模块命名为QuestionVO
修改后最终代码如下
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Document(indexName = "knows")
public class QuestionVO implements Serializable {
private static final long serialVersionUID = 1L;
// 3个状态常量
public static final Integer POSTED=0; // 已提交\未回复
public static final Integer SOLVING=1; // 正在采纳\已回复
public static final Integer SOLVED=2; // 已采纳\已解决
@Id
private Integer id;
/**
* 问题的标题
*/
@Field(type = FieldType.Text,
analyzer = "ik_max_word",
searchAnalyzer = "ik_max_word")
private String title;
/**
* 提问内容
*/
@Field(type = FieldType.Text,
analyzer = "ik_max_word",
searchAnalyzer = "ik_max_word")
private String content;
/**
* 提问者用户名
*/
@Field(type = FieldType.Keyword)
private String userNickName;
/**
* 提问者id
*/
@Field(type = FieldType.Integer)
private Integer userId;
/**
* 创建时间
*/
@Field(type = FieldType.Date,
format = DateFormat.basic_date_time)
private LocalDateTime createtime;
/**
* 状态,0-》未回答,1-》待解决,2-》已解决
*/
@Field(type = FieldType.Integer)
private Integer status;
/**
* 浏览量
*/
@Field(type = FieldType.Integer)
private Integer pageViews;
/**
* 该问题是否公开,所有学生都可见,0-》否,1-》是
*/
@Field(type = FieldType.Integer)
private Integer publicStatus;
@Field(type = FieldType.Date,
format = DateFormat.basic_date)
private LocalDate modifytime;
@Field(type = FieldType.Integer)
private Integer deleteStatus;
@Field(type = FieldType.Keyword)
private String tagNames;
/**
* 当前问题包含的所有标签的集合
*/
// 声明当前属性不对应ES中任何内容
@Transient // 临时的
private List<Tag> tags;
}
QuestionVO对应的数据访问层接口要定义出来
在repository包中创建QuestionRepository接口
代码如下
// 数据访问层注解@Repository必须要添加
@Repository
public interface QuestionRepository
extends ElasticsearchRepository<QuestionVO,Integer> {
}
下面编写业务逻辑层接口和实现类
在search模块中创建service包
包中创建IQuestionService接口,添加方法如下
public interface IQuestionService {
// 声明同步数据库question表到ES中的业务逻辑层方法
void syncData();
}
在service包下再创建一个impl包
包中创建业务逻辑层实现类QuestionServiceImpl
// 别忘了添加响应注解
@Service
@Slf4j
public class QuestionServiceImpl implements IQuestionService {
@Resource
private RestTemplate restTemplate;
@Resource
private QuestionRepository questionRepository;
@Override
public void syncData() {
// 通过Ribbon调用获得总页数
String url=
"http://faq-service/v2/questions/page/count?pageSize={1}";
int pageSize=8;
Integer total=restTemplate.getForObject(
url, Integer.class , pageSize);
// 根据总页数循环调用查询每页的信息
for (int i=1 ; i<=total ; i++){
// 循环中i就是当前循环要查询的页数
url="http://faq-service/v2/questions" +
"/page?pageNum={1}&pageSize={2}";
QuestionVO[] questions=restTemplate.getForObject(
url,QuestionVO[].class, i,pageSize);
// 将每页信息新增到ES
questionRepository.saveAll(Arrays.asList(questions));
log.debug("完成了第{}页的新增",i);
}
}
}
业务逻辑层代码编写完成
下面要执行这个业务逻辑层代码
因为这个同步数据的操作只需要运行一次
所以没有必要给它编写控制器方法
直接在测试类中调用这个方法即可
测试类代码编写如下
@Resource
IQuestionService questionService;
@Test
void run(){
// 执行同步操作
questionService.syncData();
}
// 测试查询ES中是否包含所有问题信息
@Resource
QuestionRepository questionRepository;
@Test
void getAll(){
Iterable<QuestionVO> qs=questionRepository.findAll();
qs.forEach(q-> System.out.println(q));
}
测试代码运行前启动
Nacos\faq\Elasticsearch
然后在运行测试方法!
随笔
66 9?
66/8=8+1
64/8=8+1 9
int count=66;
66 -> 73 =9
64 ->71 =8
page=(count+pageSize-1)/pageSize;
page= count%8==0 ? count/8 : count/8+1
if(count%8==0){
page=count/8;
}else{
page=count/8+1;
}