请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用

2023-07-13

0. 前言

终于!终于!自个翻遍了网上的文章,加上对官网的文档和API的翻找,终于明白这玩意到底更新出了个啥出来!
本文章会带你了解,使用 SpringDataES5.1ES8.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)
        .

相关阅读

手机版|MSIPO技术圈 皖ICP备19022944号-2

Copyright © 2023, msipo.com

返回顶部