elasticsearch学习第三天-springboot整合es

522 阅读5分钟

前言

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服务失败,最后没有采用。

image.png

2、使用ElasticsearchRestTemplate,非常遗憾,在es7.6.1和2.2.5的springboot中,这个bean的注册失败,所以最终也是放弃了。

3、没关系,下面咱们的整合都使用elasticsearch包的Repository就可以了。细心的同学估计发现了,这里的jar版本是6.8.6,我心里也在想是不是应该引入更高版本的它,之前的问题也不排除是因为jar的版本存在问题导致的。但是时间有限,这个放到之后研究,这里就不纠结了。

image.png

整合

创建索引库、介绍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,直接存储一个集合,效果如下。

image.png

大家发现没有,这个保存对象集合的saveAll()方法我们根本就没手动写就直接可以用了,最终还执行成功了。一直被spring家族产品围绕的咱们应该也见怪不怪了,忙猜继承的上层接口实现了此方法。点进去saveAll()方法发现它出现在CrudRepository中。

image.png 看下ItemRepository的继承类图。

image.png

ok,一切解释的通了。 spring data jpa自带了很多的操作,甚至允许自定义操作,允许的自定义操作,你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类,只要你的方法名符合一定的规定。(如何生成的代理实现类倒是值得关注的一个地方)。

KeywordSampleElasticsearch Query String
AndfindByNameAndPrice{"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
OrfindByNameOrPrice{"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}}
IsfindByName{"bool" : {"must" : {"field" : {"name" : "?"}}}}
NotfindByNameNot{"bool" : {"must_not" : {"field" : {"name" : "?"}}}}
BetweenfindByPriceBetween{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
LessThanEqualfindByPriceLessThan{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
GreaterThanEqualfindByPriceGreaterThan{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
BeforefindByPriceBefore{"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}}
AfterfindByPriceAfter{"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}}
LikefindByNameLike{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
StartingWithfindByNameStartingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}}
EndingWithfindByNameEndingWith{"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}}
Contains/ContainingfindByNameContaining{"bool" : {"must" : {"field" : {"name" : {"query" : "**?**","analyze_wildcard" : true}}}}}
InfindByNameIn(Collection<String>names){"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}}
NotInfindByNameNotIn(Collection<String>names){"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}}
NearfindByStoreNearNot Supported Yet !
TruefindByAvailableTrue{"bool" : {"must" : {"field" : {"available" : true}}}}
FalsefindByAvailableFalse{"bool" : {"must" : {"field" : {"available" : false}}}}
OrderByfindByAvailableTrueOrderByNameDesc{"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);
    }
}

结果:

image.png

没有任何问题。

再来试试 findByPriceBetween:

/**
 * 这是我们自行定义的方法
 */
@Test
public void queryByPriceBetween() {
    List<Item> list = this.itemRepository.findByPriceBetween(2000.00, 3500.00);
    for (Item item : list) {
        System.out.println("item = " + item);
    }
}

image.png

依旧是可以的。对于这些不需要太关注,因为涉及到一些复杂的查询,(模糊、通配符、词条查询甚至聚合等)就显得力不从心了。所以,仅仅感叹下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);
}

image.png term精确查查询

/**
 * 下面咱们再来玩下term查询
 */
@Test
public void termQueryTest() {
    TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "华为");
    Iterable<Item> search = itemRepository.search(termQueryBuilder);
    search.forEach(System.out::println);
}

image.png 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);
}

image.png

对查询的结果做过滤、排序等操作

// 可以明显感觉出来,咱们想要对结果做一些指定显示,过滤、排序都不是特别方便 想想办法解决一手
@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);
}

image.png

基本聚合-聚合为桶

@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());
        }

    }

}

额,说实话,后面这些操作很难解释,解释起来也很无聊。需要一定的想象力和经验~加油