Spring Data Elasticsearch
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#reference
Spring Data Elasticsearch 是 Elasticsearch 搜索引擎开发的解决方案。它提供:
模板对象,用于存储、搜索、排序文档和构建聚合的高级API。
案例说明
在 Elasticsearch 中存储学生数据,并对学生数据进行搜索测试。
数据结构:
学号 姓名 性别 出生日期
27 张三 男 2020-12-4
案例测试以下数据操作:
学号 | 姓名 | 性别 | 出生日期 |
---|---|---|---|
27 | 张三 | 男 | 2020-12-4 |
创建 students 索引和映射
C - 创建学生数据
R - 访问学生数据
U - 修改学生数据
D - 删除学生数据
使用 Repository 和 Criteria 搜索学生数据
一 新建spring工程,添加elasticsearch依赖,修改pom版本,以及编辑配置文件
spring:
elasticsearch:
rest:
uris:
- http://192.168.64.181:9200
- http://192.168.64.181:9201
- http://192.168.64.181:9202
logging:
level:
tracer: TRACE
二 在 Elasticsearch 中创建 students 索引
在开始运行测试之前,在 Elasticsearch 中先创建 students 索引:
PUT /students
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2,
"index.max_ngram_diff":30,
"analysis": {
"analyzer": {
"ngram_analyzer": {
"tokenizer": "ngram_tokenizer"
}
},
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 1,
"max_gram": 30,
"token_chars": [
"letter",
"digit"
]
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name": {
"type": "text",
"analyzer": "ngram_analyzer"
},
"gender": {
"type": "keyword"
},
"birthDate": {
"type": "date",
"format": "yyyy-MM-dd"
}
}
}
}
三 添加Lombok依赖,封装学生数据对象
- 封装的数据对象
/**
* @Document 用于指定索引,也可以用来自动创建索引,
* 一般索引是自己手动创建*/
@Document(indexName = "students",shards = 3,replicas = 2)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
@Id //使用学生学号作为索引id()
private Long id;
private String name;
private Character gender;
//设置索引中的字段名,对应实体类中的属性
//若字段名和属性名相同,可以省略此注解
@Field("birthDate")
private String birthDate;
}
- 新建接口
package cn.tedu.es;
import cn.tedu.es.entity.Student;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**Spring Data Repository 规范
* 只需要定义抽象接口,抽象方法,不需要自己实现
* Repository 底层代码已经实现了所有的增删改成的数据操作代码
* */
public interface StudentRepository
extends ElasticsearchRepository<Student,Long> {//数据类型Student,索引类型Long
}
四 通过 ElasticsearchRepository 实现 CRUD 操作
Spring Data 的 Repository 接口提供了一种声明式的数据操作规范,无序编写任何代码,只需遵循 Spring Data 的方法定义规范即可完成数据的 CRUD 操作。
ElasticsearchRepository 继承自 Repository,其中已经预定义了基本的 CURD 方法,我们可以通过继承 ElasticsearchRepository,添加自定义的数据操作方法。
Repository 方法命名规范
自定义数据操作方法需要遵循 Repository 规范,示例如下:
Elasticsearch(五)Spring Data Elasticsearch - 增删改查API_wanght笔记-CSDN博客
五 创建测试类,添加数据并保存,增删改查
package cn.tedu.es;
import cn.tedu.es.entity.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
@SpringBootTest
public class Test1 {
@Autowired
private StudentRepository r;
@Test //import org.junit.jupiter.api.Test;
public void test1() {
Student s1 = new Student(9527L, "唐伯虎", '男', "2021-09-29");
Student s2 = new Student(9528L, "华夫人", '女', "2021-08-29");
Student s3 = new Student(9529L, "祝枝山", '男', "2021-07-29");
Student s4 = new Student(9530L, "华安", '男', "2021-06-29");
Student s5 = new Student(9531L, "旺财", '男', "2021-05-29");
Student s6 = new Student(9532L, "小强", '男', "2021-04-29");
Student s7 = new Student(9533L, "秋香", '女', "2021-03-29");
Student s8 = new Student(9534L, "石榴姐", '男', "2021-02-29");
r.save(s1);
r.save(s2);
r.save(s3);
r.save(s4);
r.save(s5);
r.save(s6);
r.save(s7);
r.save(s8);
}
//修改
@Test
public void test2(){
Student student = new Student(9533L, "零零", '女', "2021-03-09");
r.save(student);
}
//查询
@Test
public void test3(){
Optional<Student> o1 = r.findById(9533L);
if (o1.isPresent()) { //是否存在/出现
Student student = o1.get();
System.out.println(student);
}
System.out.println("--------------------");
Iterable<Student> all = r.findAll();
for (Student s: all){
System.out.println(s);
}
}
//删除
@Test
public void test4(){
r.deleteById(9533L);
}
}
六 搜索操作
- 接口添加搜索的抽象方法
package cn.tedu.es;
import cn.tedu.es.entity.Student;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**Spring Data Repository 规范
* 只需要定义抽象接口,抽象方法,不需要自己实现
* Repository 底层代码已经实现了所有的增删改成的数据操作代码
* */
public interface StudentRepository
extends ElasticsearchRepository<Student,Long> {//数据类型Student,索引类型Long
// 在 name 中搜索关键词
List<Student> findByName(String nameKey);
// 在 name 和 birthDate 中搜索
List<Student> findByNameOrBirthDate(String name,String birthDate);
}
- 测试类编辑搜索方法
//根据名字搜索
@Test
public void test5(){
List<Student> l = r.findByName("华");
for (Student s:l){
System.out.println(s);
}
}
//根据名字或者生日搜索
@Test
public void test6(){
List<Student> list = r.findByNameOrBirthDate("华", "2021-08-29");
for(Student s:list){
System.out.println(s);
}
}
六 使用 Criteria 构建查询
Spring Data Elasticsearch 中,可以使用 SearchOperations 工具执行一些更复杂的查询,这些查询操作接收一个 Query 对象封装的查询操作。
Spring Data Elasticsearch 中的 Query 有三种:
- CriteriaQuery
- StringQuery
- NativeSearchQuery
- 自定义搜索方法的类
package cn.tedu.es;
import cn.tedu.es.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class StudentSearcher {
//执行CriteriaQuery 查询的工具对象
@Autowired
private ElasticsearchOperations o;
// 任意定义的搜索方法
public List<Student> findByName(String nameKey){
Criteria c = new Criteria("name");
c.is(nameKey);
return exec(c);
}
public List<Student> findByBirthDate(String from,String to){
Criteria c = new Criteria("birthDate");
c.between(from,to);
return exec(c);
}
private List<Student> exec(Criteria c) {
// 把条件,封装到一个查询对象
CriteriaQuery q = new CriteriaQuery(c);
// 集合SearchHit[SearchHit<Student>, SearchHit<Student>, SearchHit<Student>...]
SearchHits<Student> shs = o.search(q, Student.class);
// List<Student> list =new ArrayList<>();
// for (SearchHit<Student> sh : shs) {
// Student s = sh.getContent();
// // 从SearchHit集合中取出的SearchHit<Student>放入list集合中
// list.add(s);
// }
// 可以使用集合的 Stream api 和 lambda 语法, 简化上面的代码
// 映射出SearchHit集合的数据,放到另一个集合中
List<Student> list = shs.stream().map(SearchHit::getContent).collect(Collectors.toList());
return list;
}
}
- 测试类实现自定义方法
package cn.tedu.es;
import cn.tedu.es.entity.Student;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class Test2 {
@Autowired
private StudentSearcher s;
@Test
public void test1() {
List<Student> list = s.findByName("华");
for (Student s : list) {
System.out.println(s);
}
}
@Test
public void test2() {
List<Student> list = s.findByBirthDate("2000-01-01", "2030-01-01");
for (Student s : list) {
System.out.println(s);
}
}
}
七 Spring Data ES 分页操作
根据关键词搜索出来很多结果,会对结果进行分页
- Pageable -- 封装向服务器提交的分页参数
- Page -- 封装从服务器返回的搜索结果的一页数据,和分页属性数据
- Repository api
List<Student> findByName(String nameKey, Pageable pageable)
...
Pageable p =PageRequest.of(0,20);
r.finfByName("华,)
- 接口创建查询并分页方法
- Test1测试类实现这个方法
//根据名字搜索并分页
@Test
public void test5(){
Pageable p = PageRequest.of(0, 20);//第一页开始,每页20条数据
Page<Student> list = r.findByName("华", p);
for (Student s:list){
System.out.println(s);
}
System.out.println(list.getNumber());
System.out.println(list.hasPrevious());
System.out.println(list.hasNext());
System.out.println(list.getTotalPages());
System.out.println(list.getSize());
}
八 pd-web项目
- Open Recent 打开位置:Rabbitmq中
- 启动他的启动类并配置,然后重启,访问localhost:80 进入拼多商城页面
拼多商城搜索条
http://localhost/search/toSearch.html?key=%E6%89%8B%E6%9C%BA
1. pom.xml 添加 es 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.application.yml 添加连接地址
spring
....
elasticsearch:
rest:
uris:
- http://192.168.64.181:9200
- http://192.168.64.181:9201
- http://192.168.64.181:9202
3.添加实体类Item.封装从es 服务器搜索的结果
package com.pd.pojo;
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;
@Document(indexName = "pditems") // 指定创建的索引
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
@Id
private Long id;
private String brand;
private String title;
@Field("sell_point")//因为这个字段有下划线,需要进行指定
private String sellPoint;
private String price;
private String image;
}
4.创建es包添加 ItemRepository 接口和搜索以及分页的方法: findByTitleOrSellPoint()
package com.pd.es;
import com.pd.pojo.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ItemRepository
extends ElasticsearchRepository<Item,Long> {//对象类型,数据类型
//方法名必须要按格式书写
Page<Item> findByTitleOrSellPoint(String key1, String key2, Pageable pageable);
}
5.SearchService接口 以及 实现类
package com.pd.service;
import com.pd.pojo.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface SearchService {
Page<Item> search(String key, Pageable pageable);
}
package com.pd.service.impl;
import com.pd.es.ItemRepository;
import com.pd.pojo.Item;
import com.pd.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ItemRepository r;
@Override
public Page<Item> search(String key, Pageable pageable) {
return r.findByTitleOrSellPoint(key,key,pageable); //pageable分页参数
}
}
6.SearchController
package com.pd.controller;
import com.pd.pojo.Item;
import com.pd.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SearchController {
@Autowired
private SearchService searchService;
//.../search/toSearch.html?key=手机&page=1&size=20
@GetMapping("/search/toSearch.html")
public String search(Model model, String key, Pageable pageable){
// Model 是用来向界面传递数据工具
Page<Item> page =
searchService.search(key, pageable);//搜索结果
model.addAttribute("page",page);//结果放到model对象中
return "/search.jsp"; //在这里面显示
}
}
7.webapp包下Search jsp
8.重启启动类,在搜索栏搜索"手机",即可搜到关于此产品的信息
9.search.jsp添加上一页下一页功能
- 重启测试:
10. 搜索框回显搜索的关键词
webapp-->commons-->headers.jsp
- 重启测试:
11 搜索后关键词的高亮显示
- 商品存储ItemRepository接口中添加高亮显示的注解
注意:
- SearchHit对象会报错,根据提示的错误找到对象或接口,挨个全部改为SearchHit即可
- Re'pository api 高亮显示结果 SearchHit , 只能用List 集合存放, 不能用Page,否则会缺少分页属性.
第一步:ItemRepository
package com.pd.es;
import com.pd.pojo.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
@Highlight(
parameters = @HighlightParameters(
preTags = "<em>",
postTags = "</em>"
),
fields = @HighlightField(name = "title")
)
List<SearchHit<Item>> findByTitleOrSellPoint(String k1, String k2, Pageable pageable);
}
第二步:SearchService接口以及实现类
package com.pd.service;
import com.pd.pojo.Item;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchHit;
import java.util.List;
public interface SearchService {
List<SearchHit<Item>> search(String key, Pageable pageable);
}
package com.pd.service.impl;
import com.pd.es.ItemRepository;
import com.pd.pojo.Item;
import com.pd.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private ItemRepository r;
@Override
public List<SearchHit<Item>> search(String key, Pageable pageable) {
return r.findByTitleOrSellPoint(key,key,pageable);
}
}
第三步:SearchController
- 商品数据的title 设置成高亮结果
- 所有商品放入List<Item>集合
package com.pd.controller;
import com.pd.pojo.Item;
import com.pd.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
public class SearchController {
@Autowired
private SearchService searchService;
@GetMapping("/search/toSearch.html")
public String search(Model model, String key, Pageable pageable){
List<SearchHit<Item>> list = searchService.search(key, pageable);
List<Item>items = new ArrayList<>();
for (SearchHit<Item> sh : list) {
Item it = sh.getContent();
String title=hlTitle(sh.getHighlightField("title"));
it.setTitle(title);
items.add(it);
}
model.addAttribute("items",items);
model.addAttribute("page",pageable);
// pageable.getPageSize();
// pageable.getPageNumber();
return "/search.jsp";
}
private String hlTitle(List<String> title) {
StringBuilder sb = new StringBuilder();
for (String s : title) {
sb.append(s);
}
return sb.toString();
}
}
第四步:search.jsp
- 设置高亮效果:
- 重启启动类,搜索"电脑",所有结果中电脑这个词显示红色,并且可以使用上一页下一页功能