SpringBoot 整合 Elasticsearch
准备工作
创建 maven 项目,引入下列依赖(ES 版本为 7.17.7)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
</dependencies>
创建配置文件 application.yml,在其中对 ES 地址进行配置
spring:
elasticsearch:
uris: <ES-server-ip>:9200
本文中所使用的实体类 User 代码如下
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "user")
public class User {
@Id
private Long userId;
@Field(type = FieldType.Text)
private String username;
@Field(type = FieldType.Text)
private String password;
@Field(type = FieldType.Text)
private String country;
@Field(type = FieldType.Integer)
private int age;
@Field(type = FieldType.Text)
private String remark;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis)
private Date createTime;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second_millis)
private Date updateTime;
@Field(type = FieldType.Integer)
private int status;
}
创建启动类
@SpringBootApplication
public class DemoStartApplication {
public static void main(String[] args) {
SpringApplication.run(DemoStartApplication.class);
}
}
SpringBoot 整合 ES 使用
基本 CRUD 操作
spring-boot-starter-data-elasticsearch 包已经为我们封装好了相应的 CRUD 的操作,我们只需要创建接口并继承ElasticsearchRepository
接口即可实现基本的 CRUD 操作
ElasticsearchRepository
需要指定实体类以及 Id 的类型,如代码所示
public interface UserRepository extends ElasticsearchRepository<User, Long> {
}
创建业务层代码,注入UserRepository
并调用相应的方法即可实现简单的 CRUD,所有方法的作用范围只针对 user 索引
注意:更新对象也使用save
方法实现,ES 中的更新是对原数据的覆盖,如果传入的某项属性为 null,则会清除原数据的相应字段
方法 | 概述 |
---|---|
<S extends T> S save(S entity) | 新增对象 |
<S extends T> Iterable<S> saveAll(Iterable<S> entities) | 批量新增对象 |
Optional<T> findById(ID id) | 通过 id 查找对象 |
Iterable<T> findAll() | 查找全部对象 |
Iterable<T> findAllById(Iterable<ID> ids) | 通过 id 批量查找对象 |
void deleteById(ID id) | 通过 id 删除对象 |
void deleteAll(Iterable<? extends T> entities) | 删除全部对象 |
void deleteAllById(Iterable<? extends ID> ids) | 通过 id 批量删除对象 |
用于测试的业务层代码如下
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 新增 User
* @return User
*/
public User addUser(User user) {
return userRepository.save(user);
}
/**
* 批量新增 User
* @return Object
*/
public Object addUsers(List<User> users) {
return userRepository.saveAll(users);
}
/**
* 修改 User
* @return User
*/
public User updateUser(User user) {
return userRepository.save(user);
}
/**
* 通过 id 查找 User
* @return User
*/
public User findById(Long id) {
Optional<User> userOptional = userRepository.findById(id);
return userOptional.orElse(null);
}
/**
* 通过 id 批量查找 User
* @return Iterable<User>
*/
public Iterable<User> findAllById(List<Long> ids) {
return userRepository.findAllById(ids);
}
/**
* 查找全部 User
* @return Iterable<User>
*/
public Iterable<User> findAll() {
return userRepository.findAll();
}
/**
* 通过 id 删除 User
* @return String
*/
public String deleteById(Long id) {
userRepository.deleteById(id);
return "success";
}
/**
* 通过 id 批量删除 User
* @return String
*/
public String deleteAllById(List<Long> ids) {
userRepository.deleteAllById(ids);
return "success";
}
/**
* 删除全部 User
* @return String
*/
public String deleteAll() {
userRepository.deleteAll();
return "success";
}
}
测试代码如下
@SpringBootTest
class UserTest {
@Autowired
private UserService userService;
/**
* 新增 User
*/
@Test
public void testAddUser() {
User user = User.builder()
.userId(1L).username("曹操").password("123456").country("魏").remark("宁教我负天下人,休教天下人负我")
.age(18).createTime(new Date()).updateTime(new Date()).status(0)
.build();
System.out.println(userService.addUser(user));
}
/**
* 批量新增 User
*/
@Test
public void testAddUsers() {
ArrayList<User> users = new ArrayList<>();
User user = User.builder()
.userId(1L).username("曹操").password("123456").country("魏").remark("宁教我负天下人,休教天下人负我")
.age(18).createTime(new Date()).updateTime(new Date()).status(0)
.build();
users.add(user);
User user1 = User.builder()
.userId(2L).username("刘备").password("123456").country("蜀").remark("唯贤唯德,能服于人")
.age(28).createTime(new Date()).updateTime(new Date()).status(0)
.build();
users.add(user1);
User user2 = User.builder()
.userId(3L).username("孙权").password("123456").country("吴").remark("合肥战神孙十万")
.age(38).createTime(new Date()).updateTime(new Date()).status(0)
.build();
users.add(user2);
User user3 = User.builder()
.userId(4L).username("诸葛亮").password("123456").country("蜀").remark("我从未见过如此厚颜无耻之人")
.age(16).createTime(new Date()).updateTime(new Date()).status(0)
.build();
users.add(user3);
System.out.println(userService.addUsers(users));
}
/**
* 修改 User
*/
@Test
public void testUpdateUser() {
User user = User.builder()
.userId(1L).username("曹操").password("654321").remark("宁教我负天下人,休教天下人负我")
.age(20).createTime(new Date()).updateTime(new Date()).status(0)
.build();
System.out.println(userService.updateUser(user));
}
/**
* 通过 id 查找 User
*/
@Test
public void testFindById() {
System.out.println(userService.findById(1L));
}
/**
* 通过 id 批量查找 User
*/
@Test
public void testFindAllById() {
ArrayList<Long> ids = new ArrayList<>();
ids.add(3L);
ids.add(4L);
Iterable<User> users = userService.findAllById(ids);
for (User user: users) {
System.out.println(user);
}
}
/**
* 查找全部 User
*/
@Test
public void testFindAll() {
Iterable<User> users = userService.findAll();
for (User user: users) {
System.out.println(user);
}
}
/**
* 通过 id 删除 User
*/
@Test
public void testDeleteById() {
System.out.println(userService.deleteById(1L));
}
/**
* 通过 id 批量删除 User
*/
@Test
public void testDeleteAllById() {
ArrayList<Long> ids = new ArrayList<>();
ids.add(3L);
ids.add(4L);
System.out.println(userService.deleteAllById(ids));
}
/**
* 删除全部 User
*/
@Test
public void testDeleteAll() {
System.out.println(userService.deleteAll());
}
}
自定义查询操作
我们可以在UserRepository
接口中按照一定的命名规则自定义方法,spring-boot-starter-data-elasticsearch 将根据方法名自动生成实现类
自定义方法的命名规则如下所示,表格摘录自官方文档
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} |
Or | findByNameOrPrice | { "query" : { "bool" : { "should" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } }, { "query_string" : { "query" : "?", "fields" : [ "price" ] } } ] } }} |
Is | findByName | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} |
Not | findByNameNot | { "query" : { "bool" : { "must_not" : [ { "query_string" : { "query" : "?", "fields" : [ "name" ] } } ] } }} |
Between | findByPriceBetween | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
LessThan | findByPriceLessThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } } ] } }} |
LessThanEqual | findByPriceLessThanEqual | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
GreaterThan | findByPriceGreaterThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } } ] } }} |
GreaterThanEqual | findByPriceGreaterThan | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} |
Before | findByPriceBefore | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } } ] } }} |
After | findByPriceAfter | { "query" : { "bool" : { "must" : [ {"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } } ] } }} |
Like | findByNameLike | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
StartingWith | findByNameStartingWith | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
EndingWith | findByNameEndingWith | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
Contains/Containing | findByNameContaining | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "*?*", "fields" : [ "name" ] }, "analyze_wildcard": true } ] } }} |
In (when annotated as FieldType.Keyword) | findByNameIn(Collection<String>names) | { "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} |
In | findByNameIn(Collection<String>names) | { "query": {"bool": {"must": [{"query_string":{"query": "\"?\" \"?\"", "fields": ["name"]}}]}}} |
NotIn (when annotated as FieldType.Keyword) | findByNameNotIn(Collection<String>names) | { "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} |
NotIn | findByNameNotIn(Collection<String>names) | {"query": {"bool": {"must": [{"query_string": {"query": "NOT(\"?\" \"?\")", "fields": ["name"]}}]}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }} |
False | findByAvailableFalse | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "false", "fields" : [ "available" ] } } ] } }} |
OrderBy | findByAvailableTrueOrderByNameDesc | { "query" : { "bool" : { "must" : [ { "query_string" : { "query" : "true", "fields" : [ "available" ] } } ] } }, "sort":[{"name":{"order":"desc"}}] } |
接口编写的自定义查询方法如下
public interface UserRepository extends ElasticsearchRepository<User, Long> {
/**
* 通过 name 查找 User
* @param name 姓名
* @return List<User>
*/
List<User> findByUsername(String name);
/**
* 通过 name 和 country 查找 User
* @param name 姓名
* @param country 国家
* @return List<User>
*/
List<User> findByUsernameAndCountry(String name, String country);
/**
* 通过 name 或 country 查找 User
* @param name 姓名
* @param country 国家
* @return List<User>
*/
List<User> findByUsernameOrCountry(String name, String country);
/**
* 通过 name not XXX 查找 User
* @param name 姓名
* @return List<User>
*/
List<User> findByUsernameNot(String name);
/**
* 按照 age 范围查询 User
* findByAgeLessThan\findByAgeLessThanEqual\findByAgeGreaterThan\findByAgeGreaterThan
* findByAgeBefore\findByAgeAfter
* @param start 范围左边界
* @param end 范围右边界
* @return List<User>
*/
List<User> findByAgeBetween(int start, int end);
/**
* 通过 remark 分词查询 User
* @param remark 备注
* @return List<User>
*/
List<User> findByRemarkLike(String remark);
/**
* 通过 remark ???查询 User
* @param remark 备注
* @return List<User>
*/
List<User> findByRemarkContaining(String remark);
/**
* 通过 remark 以固定字符开始分词查询 User
* findByRemarkEndingWith 同样使用
* @param param 字符串
* @return List<User>
*/
List<User> findByRemarkStartingWith(String param);
/**
* 通过 name 批量分词查询 User
* findByUsernameNotIn 同样使用
* @param names 姓名集合
* @return List<User>
*/
List<User> findByUsernameIn(List<String> names);
/**
* 分页查询 -- 通过 name 批量分词查询 User
* @param names 姓名集合
* @param pageable 分页参数
* @return List<User>
*/
Page<User> findByUsernameIn(List<String> names, Pageable pageable);
}
用于测试的业务层代码如下
@Service
public class UserService01 {
@Autowired
UserRepository userRepository;
public List<User> findByUsername(String name) {
return userRepository.findByUsername(name);
}
public List<User> findByUsernameAndCountry(String name, String country) {
return userRepository.findByUsernameAndCountry(name, country);
}
public List<User> findByUsernameOrCountry(String name, String country) {
return userRepository.findByUsernameOrCountry(name, country);
}
public List<User> findByUsernameNot(String name) {
return userRepository.findByUsernameNot(name);
}
public List<User> findByAgeBetween(int start, int end) {
return userRepository.findByAgeBetween(start, end);
}
public List<User> findByRemarkLike(String remark) {
return userRepository.findByRemarkLike(remark);
}
public List<User> findByRemarkContaining(String remark) {
return userRepository.findByRemarkContaining(remark);
}
public List<User> findByRemarkStartingWith(String param) {
return userRepository.findByRemarkStartingWith(param);
}
public List<User> findByUsernameIn(List<String> names) {
return userRepository.findByUsernameIn(names);
}
public Page<User> findByUsernameIn(List<String> names, Pageable pageable) {
return userRepository.findByUsernameIn(names, pageable);
}
}
测试代码如下
@SpringBootTest
public class UserTest01 {
@Autowired
UserService01 userService;
@Test
public void testFindByUsername() {
List<User> users = userService.findByUsername("诸葛");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByUsernameAndCountry() {
List<User> users = userService.findByUsernameAndCountry("曹操", "魏");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByUsernameOrCountry() {
List<User> users = userService.findByUsernameOrCountry("曹操", "蜀");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByUsernameNot() {
List<User> users = userService.findByUsernameNot("曹操");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByAgeBetween() {
List<User> users = userService.findByAgeBetween(10, 20);
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByRemarkLike() {
List<User> users = userService.findByRemarkLike("天下人");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByRemarkContaining() {
List<User> users = userService.findByRemarkContaining("天下人");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByRemarkStartingWith() {
List<User> users = userService.findByRemarkStartingWith("唯有爱和真诚");
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByUsernameIn() {
ArrayList<String> names = new ArrayList<>();
names.add("刘备");
names.add("诸葛");
List<User> users = userService.findByUsernameIn(names);
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByUsernameInPage() {
ArrayList<String> names = new ArrayList<>();
names.add("刘备");
names.add("诸葛");
names.add("曹");
PageRequest page = PageRequest.of(0, 2);
Page<User> users = userService.findByUsernameIn(names, page);
System.out.println(users.getContent());
}
}
自定义 DSL 操作
我们可以在接口中自定义 DSL 查询方法,并使用@Query
方法指定查询的语句,语句中可以使用问号+序号的形式指定查询参数
public interface UserRepository extends ElasticsearchRepository<User, Long> {
/**
* 自定义 DSL 查询 -- 通过 country 和 age 进行查询
* @param country 国家
* @param age 年龄
* @return List<User>
*/
@Query("{\"bool\":" +
"{\"must\":" +
"[{\"match\":{\"country\":\"?0\"}}," +
"{\"match\":{\"age\":\"?1\"}}]}}")
List<User> findByCountryAndAge(String country, int age);
/**
* 自定义 DSL 分页查询 -- 通过 country 和 age 进行查询
* @param country 国家
* @param age 年龄
* @param pageable 分页查询参数
* @return List<User>
*/
@Query("{\"bool\":" +
"{\"must\":" +
"[{\"match\":{\"country\":\"?0\"}}," +
"{\"match\":{\"age\":\"?1\"}}]}}")
Page<User> findByCountryAndAge(String country, int age, Pageable pageable);
}
用于测试的业务层代码如下
@Service
public class UserService02 {
@Autowired
UserRepository userRepository;
public List<User> findByCountryAndAge(String country, int age) {
return userRepository.findByCountryAndAge(country, age);
}
public Page<User> findByCountryAndAge(String country, int age, Pageable pageable) {
return userRepository.findByCountryAndAge(country, age, pageable);
}
}
测试代码如下
@SpringBootTest
public class UserTest02 {
@Autowired
UserService02 userService;
@Test
public void testFindByCountryAndAge() {
List<User> users = userService.findByCountryAndAge("蜀", 28);
for (User user: users) {
System.out.println(user);
}
}
@Test
public void testFindByCountryAndAgePage() {
PageRequest pageRequest = PageRequest.of(0, 2);
Page<User> users = userService.findByCountryAndAge("蜀", 28, pageRequest);
System.out.println(users.getContent());
}
}