前言
2021/12/2,一天啥也没做。一直在鼓捣springboot整合es,我开始写文章的时候是14:28,I hope it can end in 13:00 , now start!
环境准备和技术选型
pom文件
首先,承接之前,我们的es版本玩的是es7.6.1,那么spring的版本自然也不会低,这里选择2.2.5父版本。整个的pom文件如下。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
yml配置:
server:
port: 10086
spring:
elasticsearch:
rest:
uris: http://localhost:9200
connection-timeout: 30s
read-timeout: 30s
技术选型
既然要整合es了,操作es的方式特别多,进行下选择。 1、使用RestHighLevelClient这种方式,这种方式一直报错连接es服务失败,最后没有采用。
2、使用ElasticsearchRestTemplate,非常遗憾,在es7.6.1和2.2.5的springboot中,这个bean的注册失败,所以最终也是放弃了。
3、没关系,下面咱们的整合都使用elasticsearch包的Repository就可以了。细心的同学估计发现了,这里的jar版本是6.8.6,我心里也在想是不是应该引入更高版本的它,之前的问题也不排除是因为jar的版本存在问题导致的。但是时间有限,这个放到之后研究,这里就不纠结了。
整合
创建索引库、介绍spring data jpa
往索引库里面存的是文档,再具体就是一行一行的有很多属性的文档,那么每一行对应java中的一个类。所以直接创建一个对应目标文档的java类,当你往es存储数据的时候索引库自动就创建好了。
package com.cmdc.bean;
import lombok.*;
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;
@Document(indexName = "item", type = "_doc", shards = 1, replicas = 0)
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Item implements Serializable{
@Id
private Long id;
/**
* 标题
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
/**
* 分类
*/
@Field(type = FieldType.Keyword)
private String category;
/**
* 品牌
*/
@Field(type = FieldType.Keyword)
private String brand;
/**
* 价格
*/
@Field(type = FieldType.Double)
private Double price;
/**
* 图片地址
*/
@Field(index = false, type = FieldType.Keyword)
private String images;
}
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
-
@Document
作用在类,标记实体类为文档对象,一般有两个属性- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认5
- replicas:副本数量,默认1
-
@Id
作用在成员变量,标记一个字段作为id主键 -
@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:- type:字段类型,取值是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称
自定义接口继承springdata的ElasticsearchRepository:
import com.cmdc.bean.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
/**
* 根据价格区间查询
*
* @param price1
* @param price2
* @return
*/
List<Item> findByPriceBetween(double price1, double price2);
/**
* 根据category
*
* @param category
* @return
*/
List<Item> findByCategory(String category);
}
下面插入一些数据
/**
* 新增多个记录
*/
@Test
public void indexList() {
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.com/13123.jpg"));
list.add(new Item(6L, "小米旗舰机", "手机", "小米", 1999.00, "http://image.com/fff.jpg"));
list.add(new Item(7L, "小米手表", "手表", "小米", 1999.00, "http://image.com/fff.jpg"));
list.add(new Item(8L, "小米旗舰机", "手表", "小米", 2999.00, "http://image.com/fff.jpg"));
list.add(new Item(9L, "小米旗舰机", "手表", "锤子", 999.00, "http://image.com/fff.jpg"));
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
}
ok,直接存储一个集合,效果如下。
大家发现没有,这个保存对象集合的saveAll()方法我们根本就没手动写就直接可以用了,最终还执行成功了。一直被spring家族产品围绕的咱们应该也见怪不怪了,忙猜继承的上层接口实现了此方法。点进去saveAll()方法发现它出现在CrudRepository中。
看下ItemRepository的继承类图。
ok,一切解释的通了。 spring data jpa自带了很多的操作,甚至允许自定义操作,允许的自定义操作,你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类,只要你的方法名符合一定的规定。(如何生成的代理实现类倒是值得关注的一个地方)。
Keyword | Sample | Elasticsearch Query String |
---|---|---|
And | findByNameAndPrice | {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Or | findByNameOrPrice | {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Is | findByName | {"bool" : {"must" : {"field" : {"name" : "?"}}}} |
Not | findByNameNot | {"bool" : {"must_not" : {"field" : {"name" : "?"}}}} |
Between | findByPriceBetween | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
LessThanEqual | findByPriceLessThan | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
GreaterThanEqual | findByPriceGreaterThan | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Before | findByPriceBefore | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
After | findByPriceAfter | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Like | findByNameLike | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
StartingWith | findByNameStartingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
EndingWith | findByNameEndingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}} |
Contains/Containing | findByNameContaining | {"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}} |
In | findByNameIn(Collection<String>names) | {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn | findByNameNotIn(Collection<String>names) | {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {"bool" : {"must" : {"field" : {"available" : true}}}} |
False | findByAvailableFalse | {"bool" : {"must" : {"field" : {"available" : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}} |
那我们也模仿着玩一下,调用下我已经定义好的 findByCategory:
/**
* 这是我们自行定义的方法
*/
@Test
public void findByCategory() {
List<Item> list = this.itemRepository.findByCategory("手表");
for (Item item : list) {
System.out.println("item = " + item);
}
}
结果:
没有任何问题。
再来试试 findByPriceBetween:
/**
* 这是我们自行定义的方法
*/
@Test
public void queryByPriceBetween() {
List<Item> list = this.itemRepository.findByPriceBetween(2000.00, 3500.00);
for (Item item : list) {
System.out.println("item = " + item);
}
}
依旧是可以的。对于这些不需要太关注,因为涉及到一些复杂的查询,(模糊、通配符、词条查询甚至聚合等)就显得力不从心了。所以,仅仅感叹下spring的强大,用处却比较鸡肋。
进阶查询
match匹配查询
/**
* 下面咱们把基本的手法来玩一下,首先来玩一下match
*/
@Test
public void matchQueryTest() {
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "小米手机")
.operator(Operator.AND);
Iterable<Item> search = itemRepository.search(matchQueryBuilder);
search.forEach(System.out::println);
}
term精确查查询
/**
* 下面咱们再来玩下term查询
*/
@Test
public void termQueryTest() {
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "华为");
Iterable<Item> search = itemRepository.search(termQueryBuilder);
search.forEach(System.out::println);
}
bool查询
/**
* 现在玩下bool查询,要求品牌是华为且价格在2000-3000
*/
@Test
public void boolQueryTest() {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
TermQueryBuilder termQueryBuilder = new TermQueryBuilder("brand", "华为");
boolQueryBuilder.must(termQueryBuilder);
RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder("price");
rangeQueryBuilder.gt(2000.00);
rangeQueryBuilder.lt(3000.00);
boolQueryBuilder.must(rangeQueryBuilder);
itemRepository.search(boolQueryBuilder).forEach(System.out::println);
}
对查询的结果做过滤、排序等操作
// 可以明显感觉出来,咱们想要对结果做一些指定显示,过滤、排序都不是特别方便 想想办法解决一手
@Test
public void testNativeQuery() {
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米"));
// 做一下排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
// 还希望做一下过滤
queryBuilder.withFilter(QueryBuilders.boolQuery().mustNot(new MatchQueryBuilder("title", "旗舰")));
// 再来个分页 下面的页数可以选到0的
queryBuilder.withPageable(PageRequest.of(0, 2));
// 执行搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
// 打印总条数
System.out.println(items.getTotalElements());
// 打印总页数
System.out.println(items.getTotalPages());
items.forEach(System.out::println);
}
基本聚合-聚合为桶
@Test
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand"));
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build());
// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为Terms类型
Terms agg = (Terms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<? extends Terms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (Terms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称
System.out.println(bucket.getKeyAsString());
// 3.5、获取桶中的文档数量
System.out.println(bucket.getDocCount());
}
}
桶内度量、桶内套桶等操作
@Test
public void testSubAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
AvgAggregationBuilder aggregationBuilder = AggregationBuilders.avg("priceAvg").field("price");
SumAggregationBuilder sumAggregationBuilder = AggregationBuilders.sum("priceSum").field("price");
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("brands").field("brand")
.subAggregation(aggregationBuilder)
.subAggregation(sumAggregationBuilder)
.subAggregation(AggregationBuilders.terms("categorys").field("category"));
// 在品牌聚合桶内进行嵌套聚合,求平均值
queryBuilder.addAggregation(
termsAggregationBuilder
);
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>) this.itemRepository.search(queryBuilder.build());
// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为Terms类型
Terms agg = (Terms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<? extends Terms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (Terms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");
// 3.6.获取子聚合结果:
Avg avg = (Avg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:" + avg.getValue());
Sum sum = (Sum) bucket.getAggregations().asMap().get("priceSum");
System.out.println("总和:" + sum.getValue());
Terms categorys = (Terms) bucket.getAggregations().asMap().get("categorys");
List<? extends Terms.Bucket> buckets1 = categorys.getBuckets();
for (Terms.Bucket bucket1 : buckets1) {
System.out.println(bucket1.getKeyAsString() + "一共" + bucket1.getDocCount());
}
}
}
额,说实话,后面这些操作很难解释,解释起来也很无聊。需要一定的想象力和经验~加油