0. 前言
终于!终于!自个翻遍了网上的文章,加上对官网的文档和API的翻找,终于明白这玩意到底更新出了个啥出来!
本文章会带你了解,使用 SpringDataES5.1 对 ES8.7 的【新增、修改、删除、多条件查询、聚合】等操作
以下SpringDataElasticSearch的时候我简称ES了。ES8.7就是客户端ES8.7
测试时需要引入对应的template哦!
@Resource
private ElasticsearchTemplate elasticsearchTemplate;
1. 实体类
在我们使用ES的时候,统统需要一个实体类进行接收充当媒介。所以我们可以在实体类添加上对应的属性值,添加上对应的构造、set、get方法。
@Data
@NoArgsConstructor
@AllArgsConstructor
// @Document 说明该实体类是属于哪一个索引
@Document(indexName = "mall_product")
public class SkuEsModel
我们需要注意一点,实体类规定的所有属性,都是跟 ES8.7 中添加的值是一致的!而且,每一个实体类对应的就是一个索引,当一个索引中存在多个实体类的属性,在查询中会出现问题!
2. 新增
2.1 单条新增
先说明单条新增
User user = new User();
user.setId(Long.valueOf(i)); // 设置ID不仅是设置内容的ID,也同时设置了索引的ID
user.setUserId(Long.valueOf(i));
user.setName("demo__"+i);
/**
* save方法:
* 参数一:实体类
* 参数二:本次保存的索引坐标
*/
elasticsearchTemplate.save(user,IndexCoordinates.of("test"));
单条新增最简单,世界调用elasticsearchTemplate的save方法,该方法需要放入添加的实体类,第二个参数是该次保存的索引,需要使用 IndexCoordinates.of 的方式来确认
- 当不使用 IndexCoordinates.of ,那么ES会使用该实体类中的 @Document 规定的索引
2.2 批量新增
/**
* save方法:
* 参数一:List<IndexQuery>
* 参数二:本次保存的索引坐标
*/
List<IndexQuery> indexQueryList = new ArrayList<>();
indexQueryList.add(new IndexQueryBuilder().withId("11").withObject(new User(11L, 11L,"demo__11")).build());
indexQueryList.add(new IndexQueryBuilder().withId("12").withObject(new User(12L, 12L,"demo__12")).build());
indexQueryList.add(new IndexQueryBuilder().withId("13").withObject(new User(13L, 13L,"demo__13")).build());
elasticsearchTemplate.bulkIndex(indexQueryList,IndexCoordinates.of("test"));
这里需要使用 IndexQueryBuilder(),设定对应添加的实体类。最后将结果build存放进List中。
elasticsearchTemplate.bulkIndex 根据 ES8.7 的一贯的操作逻辑,当存放数据时,若数据不存在那么就是新增,如果存在就是修改,所以这里批量存放是不存在数据所以就是新增。
同时这里必须指定该次保存的索引。
3. 修改
/**
* 更新操作,会按照实体类中的ID进行查询,如果没有ID,那么将会报错
*/
@Test
void update() throws JsonProcessingException {
User user = new User();
user.setId(4L);
user.setUserId(10L);
user.setName("demo1——test");
ObjectMapper objectMapper = new ObjectMapper();
List<UpdateQuery> indexQueryList = new ArrayList<>();
UpdateQuery builder = UpdateQuery
.builder(String.valueOf(user.getId()))
.withDocument(Document.parse(objectMapper.writeValueAsString(user)))
.build();
indexQueryList.add(builder);
elasticsearchTemplate.update(indexQueryList,IndexCoordinates.of("test"));
}
我们在修改中可以看到,我们使用了 Json 转换工具,在更改中指定了 Document 这个需要时JSON 格式
实际上操作逻辑跟新增是没有多少区别的
- 小贴士:既然ES规定保存存在数据就是修改,那么是否可以指定ID然后以新增的方式修改数据呢?可以试试…
4. 删除
/**
* 删除:delete方法
* 参数一:构造器 or 文档ID
* 参数二:索引名称
*/
@Test
void delete(){
List<String> strings = new ArrayList<>();
strings.add("1");
strings.add("2");
strings.add("3");
NativeQuery build = new NativeQueryBuilder().withQuery(Query.multiGetQuery(strings)).build();
// 批量删除
elasticsearchTemplate.delete(build, User.class,IndexCoordinates.of("test"));
// 单个删除
elasticsearchTemplate.delete("0",IndexCoordinates.of("test"));
}
删除的逻辑就更加的简单了,添加 String 类型的List 代指的就是需要删除的 ID 组。
当使用 elasticsearchTemplate.delete 方法时,它提供了两种方式,
- 一种是直接给ID,是单个删除
- 另一种是给Builder,这个意思是什么呢?
- ES 的意思就是说我们可以将查询到的数据给放到这里从而将查询出来的数据都进行删除操作。
当然,delete方法还是需要指定索引。
5. 查询
终于到了重头戏查询了,在开始前,我们需要深度记得,查询都是以 lambda 表达式的方式来使用的。
但这里最新版,我目前所了解到的是,SpringData这边给到了三种方式。我来简单概括一下
- 第一种,也是官网上放出来的,它将所有的API操作都归向了注解的操作逻辑。
- 这样的好处是提高了解耦合度,方便查看
- 但坏处是,新的操作逻辑出来,参考文档极少,学习成本大。
- 第二种,也就是现在使用的,它将几乎所有的 ES 使用场景都选择的 lambda 表达式的方式来使用,过于完善的 lambda 的表达式,使得不利于查看,但是开发力度减小,代码块集中。
- 第三种,传统的方式,基本不使用,主要是因为代码量冗余,虽然拥有足够清晰的结构,但是开发到现在的阶段,人们更加向着高效进发,渐渐的取消了原来的 new 对象 的方式,所以到了这个版本,基本上ES将传统的 new 对象 方式改变为了 lambda 表达式
我这里使用第二种方式,几乎都是以 lambda 表达式来演示。
5.1 单个查询
/**
*根据ID查询
*/
@Test
void listOne(){
User user = elasticsearchTemplate.get("1", User.class, IndexCoordinates.of("test"));
System.out.println(user);
}
这个没什么好说的了,调用get方法就好了
- 参数一:文档数据的 ID
- 参数二:查询出来接收的实体类类型
- 参数三:指定文档的索引
5.2 查询所有
/**
* 查询所有
*/
@Test
void listTwo(){
List<User> products = new ArrayList<>();
SearchHits<User> search = elasticsearchTemplate.search(Query.findAll(), User.class,IndexCoordinates.of("test"));
List<SearchHit<User>> searchHits = search.getSearchHits();
// 获得searchHits,进行遍历得到content
searchHits.forEach(hit -> {
products.add(hit.getContent());
});
System.out.println(products);
}
调用search方法,这里要强调说明的是,在接下来的查询中都是使用search方法,这很重要
- 参数一:Query
- 参数二:查询出来接收的实体类类型
- 参数三:指定文档的索引
这里我们重点看search方法的第一个参数,Query。
Query 有 _dsl包下的 Query 类,也有 core 包下的 Query 接口。
多数情况下我们会使用 core 包下的Query接口,请不要引入成 _dsl 包的 Query 类。
Query接口,包含了很多的构造类,新的操作构造方法否是按照构造器来进行build从而得到Query,进而传入到search方法里
- 这里使用 Query.findAll() 是直接查询索引中的全部数据,属于是给定好的查询。
5.3 NativeQueryBuilder 构造器
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder()
.withQuery(a->a.bool(boolQuery.build()))
.withPageable(Pageable.ofSize(10).withPage(0))
.withAggregation("brand_agg",brandAgg);
我们可以看到,该构造器给予了很多的方法,包括了许多的搜索条件,而后面我们也会使用这些条件来完成条件搜索。
GET /mall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "8+128"
}
}
],
"filter": [
{
"term": {
"catelogId": "225"
}
},
{
"terms": {
"brandId": [
"26",
"27"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "9"
}
}
},
{
"terms": {
"attrs.attrValue": [
"麒麟980;"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": "false"
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 20
}
我们可以仔细查看上面的查询语句,发现一个点,query是query的查询区域,sort是sort,很明确的结构。
所以我们在使用构造器的时候就应该规划好清晰明了的组织结构,例如:
我们使用构造器就应该去按照这些结构进行构造
5.4 bool 查询
// 这些都是构造器
BoolQuery.Builder boolQuery = QueryBuilders.bool();
// 这是总构造器装载,相当于最开始的 {}
new NativeQueryBuilder()
.withQuery(a->a.bool(boolQuery.build()))
5.5 must 查询(match)
boolQuery.must(a->a.match(b->b.field("skuTitle").query(parm.getKeyword())));
这里根据 【5.4】的步骤接着往下走,直接使用 bool 调用出 must 方法。
到了这里,统统都是使用 lambda 表达式了可以看到使用 链式点 出来的方法有哪些
就是这样一层层往下调用,从而达到 最终目标的查询结构
5.6 filter查询
5.6.1 term
boolQuery.filter(a->a.term(b -> b.field("catelogId").value(parm.getCatalog3Id())));
使用基本一致,都是链式使用 lambda 表达式
5.6.2 terms
// 遍历品牌
List<FieldValue> brandIdValues = new ArrayList<>();
for (Long id : parm.getBrandId()) {
// 注意:这里 Long类型需要转化为 FieldValue 类型
brandIdValues.add(FieldValue.of(id));
}
boolQuery.filter(
q->q.terms(a->a.field("brandId")
.terms(b->b.value(brandIdValues))
)
);
这里与以往的调用方式不同,需要的是FieldValue类型的List,所以需要进行转换
这里需要注意,我们是先调用了terms,写入了过滤的列名,然后通过这个列名再调用出了terms来选择该列对应的值
5.7 关于 nested 类型的查询
// attr=1_五寸:8
String[] split = attr.split("_");
String attrId = split[0];
List<FieldValue> attrValues = new ArrayList<>();
String[] attrValue = split[1].split(":");
// 填写属性值
for (String value : attrValue) {
attrValues.add(FieldValue.of(value));
}
boolQuery.filter(q->q.nested(
a -> a.path("attrs")
.query(b -> b.term(
c -> c.field("attrs.attrId").value(attrId)
))
.query(b->b.terms(
c->c.field("attrs.attrValue").terms(d->d.value(attrValues)))
)
)
);
这里以filter演示,在调用出 nested 方法后,关键一步就是 设置path,根据这个path方法进而查询出在属性组下的内容
5.8 range 查询
RangeQuery.Builder range = QueryBuilders.range();
// 非常重要,需要确认该区间判断是针对于哪一列(属性)
range.field("skuPrice");
String[] split = parm.getSkuPrice().split("_");
if (split.length>2){
// 代表当前价格是一个价格区间
range.lte(JsonData.of(new BigDecimal(split[1])))
.gte(JsonData.of(new BigDecimal(split[0])));
}else if (split.length== 1){
// 代表当前价格是一个确定的数值
if (parm.getSkuPrice().startsWith("_")){
//小于等于
range.lte(JsonData.of(new BigDecimal(split[0])));
}
// 代表当前价格是一个确定的数值
if (parm.getSkuPrice().endsWith("_")){
// 大于等于
range.gte(JsonData.of(new BigDecimal(split[0])));
}
}
boolQuery.filter(a->a.range(range.build()));
我这里使用对象的方式声明出来了一个RangeQuery出来,这样是为了让我更好的做业务逻辑处理,当然你要是全部 lambda 表达式也是完全没有问题。
这块在使用【lte、gte】的时候,它们都需要 JsonData.of 进行转换数值
6. 聚合 !!敲重点
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs":{
"brand_name_agg":{
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_image_agg":{
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catelog_agg":{
"terms": {
"field": "catelogId",
"size": 10
},
"aggs": {
"catelog_name_agg": {
"terms": {
"field": "catelogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs":{
"attr_id_agg":{
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
先看这段聚合查询,我们的聚合查询首先就是 aggs{} 来概括其内容全部都是聚合操作,其后就是对聚合片段进行命名 “brand_agg”,再者就是以列的类型来得到聚合数据,包括针对于哪个列,需要多少数据,换到我们的构造器中,来看结构。
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder()
.withQuery(a->a.bool(boolQuery.build()))
.withAggregation("brand_agg",brandAgg)
.withAggregation("catelog_agg",catelogAgg)
.withAggregation("attr_agg",attrAgg);
我们的总构造器就相当于最外层的 “{}”,但是ES这里,它没有规定 “aggs{}” 这个层级,所以,在构造器中,我们只需要规定好聚合组的名称和对应的聚合组构造器就可以了。
6.1 普通类型的聚合
Aggregation brandAgg = Aggregation.of(a -> a.terms(b -> b.field("brandId").size(10))
.aggregations("brand_name_agg",
Aggregation.of(c -> c.terms(d -> d.field("brandName").size(10))
)
)
.aggregations("brand_image_agg",
Aggregation.of(e -> e.terms(f -> f.field("brandImg").size(10))
)
)
);
在所有的聚合中,都需要 Aggregation.of 来获得对应装配的Aggregation类型,理所应当的,根据它来进行链式调用terms查询到对应的列,你所查询的列的类型是什么,那么调用的方法就应该与对应的类型相同。
重点:聚合中是可以允许重复嵌套的,也就是子聚合,所以这里在调用出terms后,是可以再次调用 aggregations 方法来完成子嵌套的聚合,其使用的方法跟外层的聚合操作是一致的。
6.2 nested 类型的聚合
Aggregation attrAgg = Aggregation.of(a -> a.nested(b -> b.path("attrs"))
.aggregations("attr_id_agg",
Aggregation.of(c -> c.terms(d -> d.field("attrs.attrId").size(10))
.aggregations("attr_name_agg",
Aggregation.of(e -> e.terms(f -> f.field("attrs.attrName").size(10)))
)
.aggregations("attr_value_agg",
Aggregation.of(e -> e.terms(f -> f.field("attrs.attrValue").size(10)))
)
)
)
);
nested比普通的聚合要复杂点,它在聚合的最外层只需要设置好path,然后根据对应的nested结构来规划好的聚合。其聚合的使用都是差不多的。
当聚合写好后,不要忘记装配到总构造器中。
7. 分页
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder()
.withPageable(Pageable.ofSize(10).withPage(0));
分页很简单,在每个构造器中应该都可以看到 分页.withPageable 这个方法,意思也很好理解,需要传入 Pageable 这个对象,.ofSize()就是当前所有查询中所展示的条数,.withPage()就是当前数据的第几页
8. 排序
// 正序
nativeQueryBuilder.withSort(Sort.by(split[0]).ascending());
// 倒序
nativeQueryBuilder.withSort(Sort.by(split[0]).descending());
在新版本的ES中,倒叙和正序都用了单独的方法,其排序也应该可以在基本的构造器中可以看到
.withSort() 方法。
该方法需要传入 Sort 对象,.by代表了根据一个字段进行排序,随后就是.ascending方法和.descending方法
- .ascending() 方法:默认的,正序排序
- .descending()方法:倒叙排序
同理,这些排序的设置都在总构造器中设置
9. 获得查询内容
SearchHits<SkuEsModel> result = elasticsearchTemplate.search(query, SkuEsModel.class, IndexCoordinates.of(EsConstant.PRODUCT_INDEX));
List<SearchHit<SkuEsModel>> searchHits = result.getSearchHits();
// 1. 返回所有查询到的商品
ArrayList<SkuEsModel> skuEsModelsList = new ArrayList<>();
if (!searchHits.isEmpty()){
for (SearchHit<SkuEsModel> row : searchHits) {
SkuEsModel content = row.getContent();
skuEsModelsList.add(content);
}
}
获得查询内容与以前版本的获取方式是一致的。将查询结果进行遍历放入新的List集合中就可以了
10. 获得聚合内容
/**
* 获得聚合查询内容
* @param result 查询结果
* @return
*/
private List<SearchResult.AttrVo> getAttrVo(SearchHits<SkuEsModel> result){
// 接收聚合参数对象
List<SearchResult.AttrVo> resultList = new ArrayList<SearchResult.AttrVo>();
// 获得聚合参数
ElasticsearchAggregations aggregations = (ElasticsearchAggregations) result.getAggregations();
// 指定聚合的名称
ElasticsearchAggregation elasticsearchAggregation = aggregations.get("attr_agg");
// 获得聚合
Aggregate aggregate = elasticsearchAggregation.aggregation().getAggregate();
// 得到聚合组是属于什么类型
String aClass = aggregate._get().getClass().toString();
// 如果不是为LongtermsAggreate类型,那么就代表当前聚合组中没有列表元素
if (!aClass.contains("LongTermsAggregate") && !aClass.contains("NestedAggregate")){
return resultList;
}
// 得到该聚合组中的子聚合
List<LongTermsBucket> array = aggregate.nested().aggregations().get("attr_id_agg").lterms().buckets().array();
for (LongTermsBucket longTermsBucket : array) {
// 当前的属性的ID
long key = longTermsBucket.key();
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
attrVo.setAttrId(key);
// 得到该聚合组中的子聚合
List<StringTermsBucket> brandNameAgg = longTermsBucket.aggregations().get("attr_name_agg").sterms().buckets().array();
String name = (String) brandNameAgg.get(0).key()._get();
attrVo.setAttrName(name);
// 得到该聚合组中的子聚合
List<StringTermsBucket> brandImageAgg = longTermsBucket.aggregations().get("attr_value_agg").sterms().buckets().array();
List<String> valueList = new ArrayList<>();
for (StringTermsBucket stringTermsBucket : brandImageAgg) {
String o = (String) stringTermsBucket.key()._get();
if (!o.equals("") && !o.equals(";")){
valueList.add(o);
}
}
attrVo.setAttrValue(valueList);
resultList.add(attrVo);
}
return resultList;
}
聚合查询获得难度很大,我这里可能不是用了官方的方法,应该是拿取的死数据(硬拿)。
在获得聚合内容时,在这个版本的 SearchHits对象 有前仅有 AggregationsContainer<?> getAggregations(); 这一个获得聚合函数的方法。
官网说需要进行强转却没有告诉我们强转的类型,而我初步推测是转为 Bucket,而结果嘛,还是获取不到,所以我只能够跟着聚合内容的一步步强制获取聚合的内容。
如果说有更完善的方法欢迎在评论区指出
11. 最后
当前使用的版本:
使用 | 版本 |
---|---|
ElasticSearch | 8.7 |
SpringBoot | 3.1.0 |
SpringBoot-starter-data-elasticsaerch | 3.0.8 |
12. 代码:
放评论区了