万字长文带你弄清楚SpringData中的Elasticsearch操作以及在脚手架里接口的结构关系!经过前面鉴证授权的整合,荔枝开始熟悉项目的学习的方法了,虽然脚手架中的内容比较简单,但是把边角的知识点全部扫到还是比较花时间的尤其是对于基础不是特别牢固的小伙伴来说~荔枝也希望这篇文章能对正在学习的小伙伴有帮助~~~
前言
一、整合ES实现搜索
1.1 SpringData框架
1.2 ElasticsearchRepository
1.3 分页工具:Pageable、Page
1.3.1 Page接口
1.3.2 Pageable接口
1.4 函数式接口
1.5 常用注解
1.6 Elasticsearch实现搜索的流程
总结
Elasticsearch实现搜索的功能比较简单,我们需要自定义一个Dao层的接口来自定义Mybatis操作数据库并将需要搜索的数据导入到ES中,同时对于相关的操作ES的操作我们通过一个继承ElasticsearchRepository接口的功能接口比如EsProductRepository来实现,其中有关ElasticsearchRepository的知识需要我们着重去理解。同时我们需要清楚的是在SpringBoot中我们操作Elasticsearch是通过Spring Data框架来实现的。
Spring Data是Spring中的一个子项目,通过官网的介绍我们可以了解到Spring Data是为各种数据访问技术提供一种一致的基于Spring编程的模型,同时也保证数据存储的特殊特性。其中包含了比如JDBC、JPA、MongoDB、Redis、 Elasticsearch等技术在Spring项目中的数据操作。在SpringBoot项目中来操作ES使用SpringData无疑是最好的哈哈哈。
Repository是Spring Data框架中定义的一个泛型接口(标记接口),该接口并不会定义任何方法,我们可以通过定义功能接口继承Repository,则该接口会被IOC容器识别为一个Repository Bean纳入到IOC容器中,进而可以在该接口中定义满足一定规范的方法。
package org.springframework.data.repository;
import org.springframework.stereotype.Indexed;
@Indexed
public interface Repository {
}
需要注意的是,Spring Data中给出了该接口的基本的继承接口CrudRepository,该接口定义了基本的操作数据库的CRUD方法,比如save开头的保存数据方法、find用来查询数据的方法等
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository extends Repository {
S save(S entity); //保存单个实体
Iterable saveAll(Iterable entities); //保存集合
Optional findById(ID id);
boolean existsById(ID id);
Iterable findAll();
Iterable findAllById(Iterable ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable ids);
void deleteAll(Iterable entities);
void deleteAll();
}
继承的子接口:
ElasticsearchRepository是Repository的一个曾孙子辈的接口,继承关系自PagingAndSortingRepository接口。在前面我们知道该接口在CrudRepository接口基础上添加了一些分页的操作。回到前面Spring Data定义的要整合数据访问技术的初衷,有关ES的操作也就必须会有一个相应的特殊的接口实现了:ElasticsearchRepository。
下面先看看这张图理清楚不同了接口之间的继承关系:
再来看看源码:
@NoRepositoryBean
public interface ElasticsearchRepository extends PagingAndSortingRepository {
Page searchSimilar(T entity, @Nullable String[] fields, Pageable pageable);
}
说一下@NoRepositoryBean,该注解是为了防止Spring Data为其创建实例,官网规定我们在定义Repository的子接口的时候加入该注解,具体深入的原因这里就不赘述,可以看看这篇文章:
https://www.cnblogs.com/logoman/p/11707659.html
searchSimilar是一个一个只能使用ID字段进行模糊查询的方法,具体的细节大家可以看看这篇博文
Elasticsearch中ElasticsearchRepository的searchSimilar使用的坑-CSDN博客
看了这么多的补充知识,相信大家对大体的SpringData提供的ElasticsearchRepository接口也有了相应的了解,回到脚手架的实现上,这里定义了一个根据名称、标题和关键词来模糊搜索的方法,需要注意的是这里的方法命名语法是有特定的要求的,因为我们需要通过衍生查询来实现ES中的数据操作。
package com.crj.crj_mall_learning.elasticsearch.repository;
import com.crj.crj_mall_learning.elasticsearch.document.EsProduct;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* @auther lzddl
* @description 商品ES操作类
*/
public interface EsProductRepository extends ElasticsearchRepository {
/**
* 搜索查询
*
* @param name 商品名称
* @param subTitle 商品标题
* @param keywords 商品关键字
* @param page 分页信息
* @return
*/
//该接口交给RS自动生成相应的实现类
Page findByNameOrSubTitleOrKeywords(String name, String subTitle, String keywords, Pageable page);
//再随便写一个根据名字和标题搜索的方法试试
Page findByNameOrSubTitle(String name,String subTitle,Pageable page);
}
IDEA中默认会提示相应的衍生查询的关键字!
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(Collectionnames) |
{ "query" : { "bool" : { "must" : [ {"bool" : {"must" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} |
In |
findByNameIn(Collectionnames) |
{ "query": {"bool": {"must": [{"query_string":{"query": ""?" "?"", "fields": ["name"]}}]}}} |
NotIn (when annotated as FieldType.Keyword) |
findByNameNotIn(Collectionnames) |
{ "query" : { "bool" : { "must" : [ {"bool" : {"must_not" : [ {"terms" : {"name" : ["?","?"]}} ] } } ] } }} |
NotIn |
findByNameNotIn(Collectionnames) |
{"query": {"bool": {"must": [{"query_string": {"query": "NOT("?" "?")", "fields": ["name"]}}]}}} |
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"}}] } |
Exists |
findByNameExists |
{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}} |
IsNull |
findByNameIsNull |
{"query":{"bool":{"must_not":[{"exists":{"field":"name"}}]}}} |
IsNotNull |
findByNameIsNotNull |
{"query":{"bool":{"must":[{"exists":{"field":"name"}}]}}} |
IsEmpty |
findByNameIsEmpty |
{"query":{"bool":{"must":[{"bool":{"must":[{"exists":{"field":"name"}}],"must_not":[{"wildcard":{"name":{"wildcard":"*"}}}]}}]}}} |
IsNotEmpty |
findByNameIsNotEmpty |
{"query":{"bool":{"must":[{"wildcard":{"name":{"wildcard":"*"}}}]}}} |
除了这种方式,我们还可以通过@Query注解的方式来使用Elasticsearch原生的查询DSL语句:
public interface EsProductRepository extends ElasticsearchRepository {
@Query("{"bool" : {"must" : {"field" : {"name" : " ? 0"}}}}")
Page findByName(String name, Pageable pageable);
}
Page接口表示一个分页查询的结果集,它包含了查询结果的分页信息和数据。Page接口继承自Slice接口,因此也有相应的的分页信息方法。下面我们来看看相应的类图:
Pageable接口用于表示分页查询的请求信息,它包含了分页查询的相关参数,例如页数、每页记录数、排序方式等。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.data.domain;
import java.util.Optional;
import org.springframework.util.Assert;
public interface Pageable {
static Pageable unpaged() {
return Unpaged.INSTANCE;
}
static Pageable ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
default boolean isPaged() {
return true;
}
default boolean isUnpaged() {
return !this.isPaged();
}
//返回请求的页数
int getPageNumber();
//放回每页包含的记录数
int getPageSize();
//根据底层页面和页面大小返回偏移量
long getOffset();
//返回排序信息
Sort getSort();
default Sort getSortOr(Sort sort) {
Assert.notNull(sort, "Fallback Sort must not be null");
return this.getSort().isSorted() ? this.getSort() : sort;
}
Pageable next();
//返回前一页的 Pageable 对象,如果当前页已经是第一页则返回第一页。
Pageable previousOrFirst();
//返回第一页的 Pageable 对象
Pageable first();
//返回一个新的 Pageable 对象,指定请求的页数。
Pageable withPage(int pageNumber);
//判断是否有前一页。
boolean hasPrevious();
default Optional toOptional() {
return this.isUnpaged() ? Optional.empty() : Optional.of(this);
}
}
这里我们再回顾一下函数式接口的知识哈~
在Java8之后为了兼容加进来的lambda表达式,引入了一个函数式接口的概念,函数式接口指的是在接口中只有一个抽象方法。同时需要注意的是,从Java8之后定义接口中是可以存在方法体的,但必须是default默认方法和static静态方法。
二者区别
我们可以通过@FunctionalInterface注解来告诉编译器函数式接口的声明,一旦加上该注解,接口中就最多只会有一个抽象方法!
Spring Data中常用的操作Elasticsearch的注解主要有四个:@Document、@Setting、@Id和@Field。
注解名称 | 作用 | 参数说明 |
---|---|---|
@Document |
用于标识映射到Elasticsearch文档上的领域对象 |
indexName:索引库的名字,MySQL中数据库的概念 |
@Setting | ES的配置注解 |
shards:默认分片数 |
@Id |
用于标识文档的ID,文档可以认为是MySQL中表行的概念 |
无参数 |
@Field |
用于标识文档中的字段,可以认为是MySQL中列的概念 |
type:文档中字段的类型 |
Field中常用的Type类型
public enum FieldType {
Auto("auto"), //自动判断字段类型
Text("text"), //会进行分词并建了索引的字符类型
Keyword("keyword"), //不会进行分词建立索引的类型
Long("long"), //
Integer("integer"), //
Short("short"), //
Byte("byte"), //
Double("double"), //
Float("float"), //
Date("date"), //
Boolean("boolean"), //
Object("object"), //
Nested("nested"), //嵌套对象类型
Ip("ip"), //
}
在最开始我们其实比较清晰的弄清楚了ES的整合流程,这里荔枝梳理一下一些重要的部分和自己踩的坑。
dao层
dao层其实是比较重要的一环,我们需要将数据库中的数据查询出来,由于商品的信息比较负载,还涉及到嵌套的对象,所以这部分的需求需要自己写一个mapper文件来实现mybatis操作。
/**
* @auther lzddl
* @description 搜索系统中的商品管理自定义Dao,根据id来将数据库中的对应商品数据加载到EsProduct对象中
*/
public interface EsProductDao {
List getAllEsProductList(@Param("id") Long id);
}
server层的实现类
这个部分其实是比较重要的,我们可以来看看关系的类图,有关ES的操作其实就在EsProductRepository接口中,有关数据库数据导入的操作就在EsProductDao中,而EsProductService其实就作为control下面直接操作的ES搜索方法。
EsProductServiceImpl
/**
* @auther lzddl
* @description 搜索商品管理Service实现类
*/
@Service
public class EsProductServiceImpl implements EsProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(EsProductServiceImpl.class);
@Autowired
private EsProductDao productDao;
@Autowired
private EsProductRepository productRepository;
@Override
public int importAll() {
List esProductList = productDao.getAllEsProductList(null);
Iterable esProductIterable = productRepository.saveAll(esProductList);
Iterator iterator = esProductIterable.iterator();
int result = 0;
while (iterator.hasNext()) {
result++;
iterator.next();
}
return result;
}
@Override
public void delete(Long id) {
productRepository.deleteById(id);
}
@Override
public EsProduct create(Long id) {
EsProduct result = null;
List esProductList = productDao.getAllEsProductList(id);
if (esProductList.size() > 0) {
EsProduct esProduct = esProductList.get(0);
result = productRepository.save(esProduct);
}
return result;
}
@Override
public void delete(List ids) {
if (!CollectionUtils.isEmpty(ids)) {
List esProductList = new ArrayList<>();
for (Long id : ids) {
EsProduct esProduct = new EsProduct();
esProduct.setId(id);
esProductList.add(esProduct);
}
productRepository.deleteAll(esProductList);
}
}
@Override
public Page search(String keyword, Integer pageNum, Integer pageSize) {
Pageable pageable = PageRequest.of(pageNum, pageSize);
return productRepository.findByNameOrSubTitleOrKeywords(keyword, keyword, keyword, pageable);
}
}
安装ES踩的坑1:路径不要有空格!
不然你就会看到这样的报错呜呜呜~~~
安装ES踩的坑2:中文分词器的解压路径一定要在plugin/analysis-ik下!
看懂脚手架的源码+弄清楚边角知识+梳理文章真的很耗时,学的好慢不过确实有效果哈哈哈,最后也要感谢宏哥和各位大佬博客的帮助!希望荔枝能继续加快脚步给大家输出更有质量的博文,最后一起加油吧~~~
今朝已然成为过去,明日依然向往未来!我是荔枝,在技术成长之路上与您相伴~~~
如果博文对您有帮助的话,可以给荔枝一键三连嘿,您的支持和鼓励是荔枝最大的动力!
如果博文内容有误,也欢迎各位大佬在下方评论区批评指正!!!