Spring Data Elasticsearch

1. Elasticsearch 客户端的缺点

Elasticsearch 提供的 Java 客户端有一些不太方便的地方:

  • 很多地方需要拼接 JSON 字符串,这将会非常麻烦
  • 需要把对象序列化为 JSON 存储
  • 查询到 JSON 结果也需要反序列化为对象

因此,接下来学习 Spring 提供的套件:Spring Data Elasticsearch,来解决这些问题。

2. Spring Data Elasticsearch 简介

Spring Data Elasticsearch 是 Spring Data 项目下的一个子模块。

Spring Data 的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如:MySQL),还是非关系数据库(如:Redis),或者是 Elasticsearch 这样的索引数据库。从而简化开发人员的代码,提高开发效率。

3. 搭建 Spring Data Elasticsearch

3.1 创建工程

  1. 打开 IDEA --> Create New Project

  2. Spring Initializr --> Next

  3. 填写项目信息 --> Next

    在这里插入图片描述

  4. 添加依赖 --> Next

    在这里插入图片描述

  5. 填写项目位置 --> Finish

    在这里插入图片描述

3.2 添加配置

编写配置文件 application.yaml

spring:
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 192.168.222.132:9300 # elasticsearch 的连接地址

3.3 添加实体类和注解

注解

Spring Data Elasticsearch 通过注解实现文档到实体类的映射,主要有的三个注解:

  • @Document:作用在类,标记实体类为文档对象,一般有四个属性
    • indexName:对应索引库名称
    • type:对应在索引库中的类型
    • shards:分片数量,默认 5
    • replicas:副本数量,默认 1
  • @Id:作用在成员变量,标记一个字段作为 id 主键
  • @Field:作用在成员变量,标记为文档的字段,并指定字段映射属性:
    • type:字段类型,取值是 FieldType 枚举类型
    • index:是否索引,布尔类型,默认是 true
    • store:是否存储,布尔类型,默认是 false
    • analyzer:分词器名称

测试

  1. 创建 pojo 包,用来编写实体类

    在这里插入图片描述

  2. 编写一个实体类 Item

    public class Item {
        Long id;
        String title; //标题
        String category;// 分类
        String brand; // 品牌
        Double price; // 价格
        String images; // 图片地址
        
        // getter、setter、toString 方法省略
    }
    
  3. 给实体类添加注解

    @Document(indexName = "item", type = "docs", shards = 1, replicas = 0)
    public class Item {
        @Id
        Long id;
        @Field(type = FieldType.Text, analyzer = "ik_max_word")
        String title; //标题
        @Field(type = FieldType.Keyword)
        String category;// 分类
        @Field(type = FieldType.Keyword)
        String brand; // 品牌
        @Field(type = FieldType.Double)
        Double price; // 价格
        @Field(type = FieldType.Keyword, index = false)
        String images; // 图片地址
    
        // getter、setter、toString 方法省略
    }
    

4. 索引操作

Spring Data Elasticsearch 提供了模板工具类 ElasticsearchTemplate,它提供了很多 Elasticsearch 操作相关的 API。

4.1 创建索引和映射

  1. 创建测试类

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = ZtElasticsearchApplication.class)
    public class IndexTest {
        @Autowired
        ElasticsearchTemplate elasticsearchTemplate;
    
        @Test
        public void testCreateIndex() {
            // 创建索引
            elasticsearchTemplate.createIndex(Item.class);
            // 创建映射
            elasticsearchTemplate.putMapping(Item.class);
        }
    }
    
  2. 运行测试类,然后打开 kibana 查看索引,创建成功

    在这里插入图片描述

4.2 删除索引

  1. 在测试类中添加方法

    @Test
    public void testDeleteIndex() {
        // 删除索引
        elasticsearchTemplate.deleteIndex(Item.class);
    }
    
  2. 运行测试类,然后打开 kibana 查看索引,删除成功

    在这里插入图片描述

5. 文档操作

Spring Data Elasticsearch 的强大之处在于,你不用写任何 CRUD 代码,只要你定义一个接口,然后继承 ElasticsearchRepository 接口,就能具备各种基本的 CRUD 功能。

5.1 新增文档

  1. 创建 ItemRepository 接口,继承 ElasticsearchRepository 接口

    public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
    }
    

    ElasticsearchRepository<T, ID> 有两个泛型参数:

    • T:索引库的类型
    • ID:id 的类型
  2. 创建测试类

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = ZtElasticsearchApplication.class)
    public class DocumentTest {
        @Autowired
        private ItemRepository itemRepository;
    
        @Test
        public void testSave() {
            Item item = new Item(1L, "小米10", "手机",
                    "小米", 3999.00, "http://image.leyou.com/123.jpg");
            // 新增文档
            itemRepository.save(item);
        }
    }
    
    
  3. 运行测试类,然后打开 kibana 查看文档,新增成功

    在这里插入图片描述

5.2 批量新增文档

  1. 在测试类中添加方法

    @Test
    public void testSaveAll() {
        List<Item> list = new ArrayList<>();
        list.add(new Item(2L, "iphoneX", "手机", "苹果", 6000.00, "http://image.leyou.com/124.jpg"));
        list.add(new Item(3L, "华为mate 30", " 手机", "华为", 5000.00, "http://image.leyou.com/125.jpg"));
        // 批量新增文档
        itemRepository.saveAll(list);
    }
    
  2. 运行测试类,然后打开 kibana 查看文档,批量新增成功

    在这里插入图片描述

5.3 修改文档

修改和新增是同一个接口,这一点跟我们在页面发起 PUT 请求是类似的。

5.4 删除文档

  1. 在测试类中添加方法

    @Test
    public void testDelete() {
        Item item = new Item();
        item.setId(3L);
        // 删除文档
        itemRepository.delete(item);
    }
    
  2. 运行测试类,然后打开 kibana 查看文档,删除成功

    在这里插入图片描述

5.5 查询文档

根据 ID 查询文档

  1. 在测试类中添加方法

    @Test
    public void testFindById() {
        // 根据 ID 查询文档
        Optional<Item> item = itemRepository.findById(2L);
        Item item1 = item.get();
        System.out.println(item);
    }
    
  2. 运行测试类,结果如下

    Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
    

查询所有文档

  1. 在测试类中添加方法

    @Test
    public void testFindAll() {
        // 查询所有文档
        Iterable<Item> items = itemRepository.findAll();
        for (Item item : items) {
            System.out.println(item);
        }
    }
    
  2. 运行测试类,结果如下

    Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'}
    Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
    

5.6 自定义方法

现在我想要实现根据 title 查询文档,没有这个 API 怎么办呢?

Spring Data Elasticsearch 提供了一个强大的功能,就是根据方法名称自动实现功能,并且不需要写实现类。

当然方法名称要符合一定的约定:

KeywordSample
AndfindByNameAndPrice
OrfindByNameOrPrice
IsfindByName
NotfindByNameNot
BetweenfindByPriceBetween
LessThanEqualfindByPriceLessThan
GreaterThanEqualfindByPriceGreaterThan
BeforefindByPriceBefore
AfterfindByPriceAfter
LikefindByNameLike
StartingWithfindByNameStartingWith
EndingWithfindByNameEndingWith
Contains/ContainingfindByNameContaining
InfindByNameIn(Collection<String>names)
NotInfindByNameNotIn(Collection<String>names)
NearfindByStoreNear
TruefindByAvailableTrue
FalsefindByAvailableFalse
OrderByfindByAvailableTrueOrderByNameDesc

下面实现根据 title 查询文档:

  1. 在 ItemRepository 接口中增加方法 findByTitle

    public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
        /**
         * 根据 title 查询文档
         * @param title
         * @return
         */
        public List<Item> findByTitle(String title);
    }
    
  2. 在测试类中添加方法

    @Test
    public void testFindByTitle() {
        // 根据 title 查询文档
        List<Item> items = itemRepository.findByTitle("小米");
        for (Item item : items) {
            System.out.println(item);
        }
    }
    
  3. 运行测试类,结果如下

    Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'}
    

6. 高级查询

前面介绍的查询文档和自定义方法已经很强大了,但要实现一些复杂的查询(比如:词条查询、模糊查询等)时,还是不够的。接下来就介绍一下更高级的查询方式。

6.1 基本查询

Spring Data Elasticsearch 为我们提供了一个对象 QueryBuilders,它提供了大量的静态方法,用于生成各种不同类型的查询对象,例如:词条、模糊、通配符等。

在这里插入图片描述

再将 QueryBuilder 作为参数传入 Repository 的 search 方法就可以查询了。

下面实现根据词条查询文档:

  1. 在测试类中添加方法

    @Test
    public void testQueryBuilder() {
        // 词条查询
        TermQueryBuilder queryBuilder = QueryBuilders.termQuery("category", "手机");
        // 执行查询
        Iterable<Item> items = itemRepository.search(queryBuilder);
        for (Item item : items) {
            System.out.println(item);
        }
    }
    
  2. 运行测试类,结果如下

    Item{id=1, title='小米10', category='手机', brand='小米', price=3999.0, images='http://image.leyou.com/123.jpg'}
    Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
    Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}
    Item{id=5, title='荣耀V10', category='手机', brand='华为', price=2799.0, images='http://image.leyou.com/13123.jpg'}
    

6.2 自定义查询

但 QueryBuilders 还是不够灵活,比如我希望将词条查询的结果,然后按价格升序排序,再分页,这就没办法了。Spring Data Elasticsearch 提供的一个查询条件构建器,帮我们实现复杂的自定义查询。

下面实现根据词条查询文档,然后按价格升序排序,再分页:

  1. 在测试类中添加方法

    @Test
    public void testNativeSearchQueryBuilder() {
        // 查询条件构建器
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 添加词条查询
        nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
        // 按价格升序排序
        nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
        // 添加分页,page = 1,注意 page 是从 0 开始的,size = 2
        nativeSearchQueryBuilder.withPageable(PageRequest.of(1,2));
        // 执行搜索,得到分页结果对象
        Page<Item> itemPage = itemRepository.search(nativeSearchQueryBuilder.build());
        // 遍历得到结果
        List<Item> items = itemPage.getContent();
        for (Item item : items) {
            System.out.println(item);
        }
        System.out.println("总页数:"+itemPage.getTotalPages());
        System.out.println("总结果数:"+itemPage.getTotalElements());
    }
    
  2. 运行测试类,结果如下

    Item{id=4, title='小米Mix2S', category='手机', brand='小米', price=4299.0, images='http://image.leyou.com/13123.jpg'}
    Item{id=2, title='iphoneX', category='手机', brand='苹果', price=6000.0, images='http://image.leyou.com/124.jpg'}
    总页数:2
    总结果数:4
    

7. 聚合

接下看看在 Spring Data Elasticsearch 中是如何实现聚合的。

7.1 聚合为桶

比如这里我们以 brand 分桶,先来看看原生的 Elasticsearch 的实现:

  1. 在 kibana 中查询

    GET item/_search
    {
      "size": 0,
      "aggs": {
        "brands": {
          "terms": {
            "field": "brand"
          }
        }
      }
    }
    
  2. 得到响应结果

    {
      "took": 15,
      "timed_out": false,
      "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": 5,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "brands": {
          "doc_count_error_upper_bound": 0,
          "sum_other_doc_count": 0,
          "buckets": [
            {
              "key": "华为",
              "doc_count": 2
            },
            {
              "key": "小米",
              "doc_count": 2
            },
            {
              "key": "苹果",
              "doc_count": 1
            }
          ]
        }
      }
    }
    

再来看看在 Spring Data Elasticsearch 实现:

  1. 在测试类中添加方法

    @Test
    public void testAggs() {
        // 查询条件构建器
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 添加一个聚合,聚合类型为 terms,聚合名称为 brands,聚合字段为 brand
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand"));
        // 不查询任何结果,相当于 "size": 0
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
        // 执行查询,把查询结果强转为 AggregatedPage 类型
        AggregatedPage<Item> itemAggregatedPage = (AggregatedPage<Item>) itemRepository.search(nativeSearchQueryBuilder.build());
        // 从结果中取出名称为 brands 的那个聚合,并转换为 StringTerm 类型
        StringTerms brands = (StringTerms) itemAggregatedPage.getAggregation("brands");
        // 获取桶
        List<StringTerms.Bucket> buckets = brands.getBuckets();
        // 遍历桶
        for (StringTerms.Bucket bucket : buckets) {
            // 获取桶中的 key,即 brand
            System.out.println("key:" + bucket.getKeyAsString());
            // 获取桶中的文档数量
            System.out.println("doc_count:" + bucket.getDocCount());
        }
    }
    
  2. 运行测试类,结果如下

    key:华为
    doc_count:2
    key:小米
    doc_count:2
    key:苹果
    doc_count:1
    

7.2 桶内度量

比如这里我们以 brand 分桶,再以桶内的价格平均值为度量,先来看看原生的 Elasticsearch 的实现:

  1. 在 kibana 中查询

    GET item/_search
    {
      "size": 0,
      "aggs": {
        "brands": {
          "terms": {
            "field": "brand"
          },
          "aggs": {
            "avg_price": {
              "avg": {
                "field": "price"
              }
            }
          }
        }
      }
    }
    
  2. 得到响应结果

    {
      "took": 23,
      "timed_out": false,
      "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": 5,
        "max_score": 0,
        "hits": []
      },
      "aggregations": {
        "brands": {
          "doc_count_error_upper_bound": 0,
          "sum_other_doc_count": 0,
          "buckets": [
            {
              "key": "华为",
              "doc_count": 2,
              "avg_price": {
                "value": 3899.5
              }
            },
            {
              "key": "小米",
              "doc_count": 2,
              "avg_price": {
                "value": 4149
              }
            },
            {
              "key": "苹果",
              "doc_count": 1,
              "avg_price": {
                "value": 6000
              }
            }
          ]
        }
      }
    }
    

再来看看在 Spring Data Elasticsearch 实现:

  1. 在测试类中添加方法

    @Test
    public void testAggs() {
        // 查询条件构建器
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        // 添加一个聚合,聚合类型为 terms,聚合名称为 brands,聚合字段为 brand
        // 添加一个度量,度量类型为 avg,度量名称为 avg_price,度量字段 price
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brand").subAggregation(AggregationBuilders.avg("avg_price").field("price")));
        // 不查询任何结果,相当于 "size": 0
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
        // 执行查询,把查询结果强转为 AggregatedPage 类型
        AggregatedPage<Item> itemAggregatedPage = (AggregatedPage<Item>) itemRepository.search(nativeSearchQueryBuilder.build());
        // 从结果中取出名称为 brands 的那个聚合,并转换为 StringTerm 类型
        StringTerms brands = (StringTerms) itemAggregatedPage.getAggregation("brands");
        // 获取桶
        List<StringTerms.Bucket> buckets = brands.getBuckets();
        // 遍历桶
        for (StringTerms.Bucket bucket : buckets) {
            // 获取桶中的 key,即 brand
            System.out.println("key:" + bucket.getKeyAsString());
            // 获取桶中的文档数量
            System.out.println("doc_count:" + bucket.getDocCount());
            // 获取度量结果
            InternalAvg avg_price = (InternalAvg) bucket.getAggregations().asMap().get("avg_price");
            System.out.println("avg_value:"+avg_price.getValue());
        }
    }
    
  2. 运行测试类,结果如下

    key:华为
    doc_count:2
    avg_value:3899.5
    key:小米
    doc_count:2
    avg_value:4149.0
    key:苹果
    doc_count:1
    avg_value:6000.0
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值