简介
SpringBoot 集成ElasticSearch 方式有很多,网上教程看的是眼花缭乱,如果先前没接触过ES的同学,看着很懵逼,不知道用什么样的方式集成好,其实那种方式也能集成,但是相对来说那种方式更稳定,兼容性好咱们就用那种,这次咱们主要讲下通过Spring Data Elasticsearch 套件完成对ES的操作,安装和使用场景可直接点击查看。
说明
spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高,下面为版本对照表。
spring data elasticsearch | elasticsearch |
---|---|
3.2.x | 6.5.0 |
3.1.x | 6.2.2 |
3.0.x | 5.5.0 |
2.1.x | 2.4.0 |
2.0.x | 2.2.0 |
1.3.x | 1.5.2 |
这里选择的版本搭配为ES 6.24, Spring-data-es 版本为3.1.5.RELEASE
特性
- 基于Java的@Configuration类的Spring配置支持或ES客户端实例的XML命名空间
- 提供了用于操作ES的便捷工具类ElasticsearchTemplate。实现了文档到POJO之间的自动智能映射
- 利用Spring的数据转换服务实现功能丰富的对象映射
- 基于注解的元数据映射方式,且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现),也支持人工定制查询
Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与关系型数据库是一致的,如下对照
索引库 | 关系型数据库 |
---|---|
类型(type) | Table 数据表 |
文档(Document) | Row 行 |
字段(Field) | Columns 列 |
详细说明
索引库(indices):indices代表许多的索引
类型(type): 模拟mysql中的table概念,一个索引库下可以有不同类型的索引,比如商品索引,订单索引,其数据格式不同
文档(document): 存入索引库原始的数据。比如每一条商品信息,就是一个文档
字段(field): 文档中的属性
映射配置(mappings): 字段的数据类型、属性、是否索引、是否存储等特性
索引集(Indices,index的复数):逻辑上的完整索引
分片(shard):数据拆分后的各个部分
副本(replica):每个分片的复制
注意:
Elasticsearch本身就是分布式的,即便只有一个节点,Elasticsearch默认也会对的数据进行分片和副本操作,向集群添加新数据时,数据也会在新加入的节点中进行平衡
实战
Pom文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
com.glj
elasticserarch-demo
0.0.1-SNAPSHOT
elasticserarch-demo
Demo project for Spring Boot
jar
1.8
UTF-8
UTF-8
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-elasticsearch
com.baomidou
mybatis-plus-boot-starter
3.0.6
org.apache.tomcat
tomcat-jdbc
io.springfox
springfox-swagger2
2.8.0
io.springfox
springfox-swagger-ui
2.8.0
org.springframework.boot
spring-boot-starter-freemarker
com.google.guava
guava
27.0-jre
org.springframework.cloud
spring-cloud-dependencies
Greenwich.RELEASE
pom
import
org.springframework.boot
spring-boot-maven-plugin
application.yml
server:
port: 8188
spring:
application:
name: elasticserarch-demo
datasource:
url: jdbc:mysql://127.0.0.1:3306/security_oauth?serverTimezone=GMT%2B8&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
password: root
username: root
data:
elasticsearch:
cluster-name: my-application
cluster-nodes: 127.0.0.1:9300
实体对象
package com.glj.elasticserarch.demo.biz.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
*
*
*
*
* @author gaoleijie
* @since 2019-04-09
*/
@Data
@Document(indexName = "user_index",type = "user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
@ApiModelProperty(value = "账号")
@Field(type = FieldType.Keyword)
private String username;
@ApiModelProperty(value = "密码")
@Field(type = FieldType.Keyword)
private String password;
@ApiModelProperty(value = "昵称")
@Field(type = FieldType.Text,analyzer = "ik_max_word")
private String nickname;
@ApiModelProperty(value = "邮箱")
@Field(type = FieldType.Keyword)
private String email;
@ApiModelProperty(value = "状态(0:锁定,1:解锁)")
@Field(type = FieldType.Integer)
private Integer status;
@ApiModelProperty(value = "创建人")
@Field(type = FieldType.Keyword)
private String createUser;
@ApiModelProperty(value = "更新人")
@Field(type = FieldType.Keyword)
private String updateUser;
@ApiModelProperty(value = "年龄")
@Field(type = FieldType.Double)
private Double age;
}
注意
SpringDataElasticSearch中,只需要操作对象,就可以操作elasticsearch中的数据
注解说明
@Document 作用在类,标记实体类为文档对象
包含属性
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1
@Id 作用在成员变量,标记一个字段作为id主键
@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性
包含属性
type:字段类型,是枚举:FieldType,可以是text、long、short、date、integer、object等
type属性名称 | 含义 |
---|---|
text | 存储数据时候,会自动分词,并生成索引 |
keyword | 存储数据时候,不会分词建立索引 |
Numerical | 数值类型,一类为基本数据类型:long、interger、short、byte、double、float、half_float 。一类为浮点数的高精度类型:scaled_float 需要指定一个精度因子,比如10或50,elasticsearch会把真实值乘以这个因子后存储,取出时再还原 |
Date日期类型 | elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间 |
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称,这里的ik_max_word即使用ik分词器
创建索引
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@PostMapping("/createIndex")
@ApiOperation("创建索引")
public Boolean createIndex(@RequestParam String indexName){
return elasticsearchTemplate.createIndex(indexName);
}
@PostMapping("/createIndex")
@ApiOperation("创建索引")
public Boolean createIndex(){
return elasticsearchTemplate.createIndex(SysUser.class);
}
可以根据类的信息自动生成,也可以手动指定indexName和Settings
删除索引
@PostMapping("/deleteIndex")
@ApiOperation("删除索引")
public Boolean deleteIndex(@RequestParam String indexName){
// return elasticsearchTemplate.deleteIndex(SysUser.class);
return elasticsearchTemplate.deleteIndex(indexName);
}
可以根据类名或索引名删除
新增文档之Repository
Repository接口
Spring Data 的强大之处,在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
来看下Repository的继承关系,自己新建一个接口,然后继承ElasticsearchRepository 就好了
package com.glj.elasticserarch.demo.biz.repository;
import com.glj.elasticserarch.demo.biz.dto.SysUser;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**
* @ClassName ElasticSerarchRepository
* @Description TODO
* @Author gaoleijie
* @Date 2019/7/18 10:15
**/
public interface UserRepository extends ElasticsearchRepository {
/**
* 根据昵称查找用户
* @param nickName
* @return
*/
List findByNickname(String nickName);
/**
* 根据昵称或者用户名进行查找
* @param nickName
* @param Password
* @return
*/
List findByNicknameOrPassword(String nickName,String Password);
}
再来看下ElasticsearchRepository
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.data.elasticsearch.repository;
import java.io.Serializable;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.data.repository.NoRepositoryBean;
@NoRepositoryBean
public interface ElasticsearchRepository extends ElasticsearchCrudRepository {
S index(S var1);
Iterable search(QueryBuilder var1);
Page search(QueryBuilder var1, Pageable var2);
Page search(SearchQuery var1);
Page searchSimilar(T var1, String[] var2, Pageable var3);
void refresh();
Class getEntityClass();
}
新增文档
@PostMapping("/save")
@ApiOperation("新增")
public SysUser save(@RequestBody SysUser user){
return repository.save(user);
}
@PostMapping("/saveAll")
@ApiOperation("批量新增")
public Iterable saveAll(@RequestBody List users){
return repository.saveAll(users);
}
运行完毕后,可以进入 http://localhost:9100/ 查看效果
注意
elasticsearch中本没有修改,它的修改原理是该是先删除再新增修改和新增是同一个接口,区分的依据就是id。
查询
@PostMapping("/findAllAndSort")
@ApiOperation("查询全部并根据密码排序")
public Iterable findAllAndSort(){
return repository.findAll(Sort.by("password").ascending());
}
@PostMapping("/findAll")
@ApiOperation("查询全部")
public Iterable findAll(){
return repository.findAll();
}
自定义方法
不知道大家有没有看到在我的UserRepository 接口中有自定义的查询方法,这些方法就是接下来我们要讲的 Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByName,那么它就知道你是根据name查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample |
---|---|
findByNameAndPrice | |
findByNameOrPrice | |
findByName | |
findByNameNot | |
findByPriceBetween | |
findByPriceLessThan | |
findByPriceGreaterThan | |
findByPriceBefore | |
findByPriceAfter | |
findByNameLike | |
findByNameStartingWith | |
findByNameEndingWith | |
findByNameContaining | |
findByNameIn(Collection |
|
findByNameNotIn(Collection |
|
findByStoreNear | |
findByAvailableTrue | |
findByAvailableFalse | |
findByAvailableTrueOrderByNameDesc |
例如我们下面两个例子,按照”昵称“去查找用户和”按照昵称或者密码“去查找,
不需要写实现类,然后我们直接去运行
@PostMapping("/findByNickname")
@ApiOperation("根据昵称查询用户")
public List findByNickname(@RequestParam("nickname")String nickName){
List list = repository.findByNickname(nickName);
return list;
}
@PostMapping("/findByNicknameOrPassword")
@ApiOperation("根据昵称或者密码查询用户")
public List findByNicknameOrPassword(@RequestParam("nickname")String nickName,@RequestParam("password")String Password){
List list = repository.findByNicknameOrPassword(nickName,Password);
return list;
}
package com.glj.elasticserarch.demo.biz.repository;
import com.glj.elasticserarch.demo.biz.dto.SysUser;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
/**
* @ClassName ElasticSerarchRepository
* @Description TODO
* @Author gaoleijie
* @Date 2019/7/18 10:15
**/
public interface UserRepository extends ElasticsearchRepository {
/**
* 根据昵称查找用户
* @param nickName
* @return
*/
List findByNickname(String nickName);
/**
* 根据昵称或者用户名进行查找
* @param nickName
* @param Password
* @return
*/
List findByNicknameOrPassword(String nickName,String Password);
}
查询结果
自定义查询
@PostMapping("/query")
@ApiOperation("自定义查询")
public Page query(@RequestParam("username")String userName){
NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.matchQuery("username",userName));
//如果实体和数据的名称对应就会自动封装,pageable分页参数
Page items = this.repository.search(builder.build());
long total = items.getTotalElements();
System.out.println("查询数量为:"+total);
return items;
}
NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
QueryBuilders.matchQuery(“username”, userName):利用QueryBuilders来生成一个查询。QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询:
Page
totalElements:总条数
totalPages:总页数
Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据
其它属性
模糊查询
/**
* 模糊查找
* @param userName
* @return
*/
@PostMapping("/fuzzyQuery")
@ApiOperation("模糊查找根据分词去模糊,如果默认为5,输入4是没有办法模糊的")
public Page fuzzyQuery(@RequestParam("username") String userName){
NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
builder.withQuery(QueryBuilders.fuzzyQuery("username",userName));
// 查找
Page page = this.repository.search(builder.build());
return page;
}
注意
如果在文档对象里面没有指定分片数量,默认是5,查询值必须大于5的时候才能进行分片查询,否则是查询不出来的,所以如果对于特定的文档,分片数量需要提前指定
聚合
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
什么牌子的手机最受欢迎?
这些手机的平均价格、最高价格、最低价格?
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果
lasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶,一个叫度量
桶
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个桶,比如如果按照男女进行划分 就会出现”男桶“”女桶“ 类似的桶
Elasticsearch中提供的划分桶的方式有很多:
Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
Histogram Aggregation:根据数值阶梯分组,与日期类似
Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
度量
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量
比较常用的一些度量聚合方式:
Avg Aggregation:求平均值
Max Aggregation:求最大值
Min Aggregation:求最小值
Percentiles Aggregation:求百分比
Stats Aggregation:同时返回avg、max、min、sum、count等
Sum Aggregation:求和
Top hits Aggregation:求前几
Value Count Aggregation:求总数
注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词。这里我们将updateUser和username这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合
聚合为桶
/**
* 根据列名进行聚合查询
*/
@PostMapping("/aggregateQuery")
@ApiOperation("根据列进行聚合查询")
public void aggregateQuery(@RequestParam("clumname") String clumname){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String []{""},null));
// 添加一个新的聚合,聚合类型为terms,聚合名称为列明,列名称为
queryBuilder.addAggregation(
AggregationBuilders.terms(clumname).field(clumname));
// 将查询结果转换为聚合分页查询
AggregatedPage aggPage = (AggregatedPage) this.repository.search(queryBuilder.build());
StringTerms agg = (StringTerms) aggPage.getAggregation(clumname);
List buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即列名称
System.out.println(bucket.getKeyAsString());
// 3.5、获取桶中的某列的数量
System.out.println(bucket.getDocCount());
}
}
结果显示
关键API
AggregationBuilders:聚合的构建工厂类。所有聚合都由这个类来构建
(1)统计某个字段的数量
ValueCountBuilder vcb= AggregationBuilders.count("count_nickname").field("nickname");
(2)去重统计某个字段的数量(可能有部分误差)
CardinalityBuilder cb= AggregationBuilders.cardinality("distinct_count_name").field("nickname");
(3)聚合过滤
FilterAggregationBuilder fab= AggregationBuilders.filter("name_filter").filter(QueryBuilders.queryStringQuery("nickname:刘德华"));
(4)按某个字段分组
TermsBuilder tb= AggregationBuilders.terms("group_name").field("name");
(5)求和
SumBuilder sumBuilder= AggregationBuilders.sum("sum_price").field("price");
(6)求平均
AvgBuilder ab= AggregationBuilders.avg("avg_price").field("price");
(7)求最大值
MaxBuilder mb= AggregationBuilders.max("max_price").field("price");
(8)求最小值
MinBuilder min= AggregationBuilders.min("min_price").field("price");
(9)按日期间隔分组
DateHistogramBuilder dhb= AggregationBuilders.dateHistogram("dh").field("date");
(10)获取聚合里面的结果
TopHitsBuilder thb= AggregationBuilders.topHits("top_result");
(11)嵌套的聚合
NestedBuilder nb= AggregationBuilders.nested("negsted_path").path("quests");
(12)反转嵌套
AggregationBuilders.reverseNested("res_negsted").path("kps ");
AggregatedPage:聚合查询的结果类
嵌套聚合,求平均值
@PostMapping("/arrregateAvg")
@ApiOperation("根据列进行聚合查询求平均值")
public void arrregateAvg(@RequestParam("clumname") String clumname){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withSourceFilter(new FetchSourceFilter(new String []{""},null));
queryBuilder.addAggregation(AggregationBuilders.terms(clumname).field(clumname).subAggregation(AggregationBuilders.avg("ageAvg").field("age")));
AggregatedPage aggPage =(AggregatedPage) repository.search(queryBuilder.build());
StringTerms agg = (StringTerms) aggPage.getAggregation(clumname);
// 3.2、获取桶
List buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
System.out.println(bucket.getKeyAsString()+",共"+bucket.getDocCount()+"编");
// 3.6.获取子聚合结果:
InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("ageAvg");
System.out.println("平均售价:" + avg.getValue());
}
}
查询结果
结束语
本文章大部分为自己实战出来的,实战思路是参考文章:SpringBoot整合Elasticsearch
如有什么不对,请及时指正。