通过Spring Data Elasticsearch操作ES

1字数 2210阅读 244

简介

SpringBoot 集成ElasticSearch 方式有很多,网上教程看的是眼花缭乱,如果先前没接触过ES的同学,看着很懵逼,不知道用什么样的方式集成好,其实那种方式也能集成,但是相对来说那种方式更稳定,兼容性好咱们就用那种,这次咱们主要讲下通过Spring Data Elasticsearch 套件完成对ES的操作,安装使用场景可直接点击查看。

说明

spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高,下面为版本对照表。

spring data elasticsearch elasticsearch
3.2.x 6.5.0
3.1.x 6.2.2
3.0.x 5.5.0
2.1.x 2.4.0
2.0.x 2.2.0
1.3.x 1.5.2

这里选择的版本搭配为ES 6.24, Spring-data-es 版本为3.1.5.RELEASE

特性

  1. 基于Java的@Configuration类的Spring配置支持或ES客户端实例的XML命名空间
  2. 提供了用于操作ES的便捷工具类ElasticsearchTemplate。实现了文档到POJO之间的自动智能映射
  3. 利用Spring的数据转换服务实现功能丰富的对象映射
  4. 基于注解的元数据映射方式,且可扩展以支持更多不同的数据格式
  5. 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现),也支持人工定制查询

Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与关系型数据库是一致的,如下对照

索引库 关系型数据库
类型(type) Table 数据表
文档(Document) Row 行
字段(Field) Columns 列

详细说明

索引库(indices):indices代表许多的索引

类型(type): 模拟mysql中的table概念,一个索引库下可以有不同类型的索引,比如商品索引,订单索引,其数据格式不同

文档(document): 存入索引库原始的数据。比如每一条商品信息,就是一个文档

字段(field): 文档中的属性

映射配置(mappings): 字段的数据类型、属性、是否索引、是否存储等特性

索引集(Indices,index的复数):逻辑上的完整索引

分片(shard):数据拆分后的各个部分

副本(replica):每个分片的复制

注意:
Elasticsearch本身就是分布式的,即便只有一个节点,Elasticsearch默认也会对的数据进行分片和副本操作,向集群添加新数据时,数据也会在新加入的节点中进行平衡

实战

Pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.glj</groupId>
    <artifactId>elasticserarch-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>elasticserarch-demo</name>
    <description>Demo project for Spring Boot</description>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- elasticsearch启动器 (必须)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>

        <!--Mybatis Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 与swagger一起使用,需要注意-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.0-jre</version>
        </dependency>
        
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

server:
  port: 8188
spring:
  application:
    name: elasticserarch-demo
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/security_oauth?serverTimezone=GMT%2B8&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: root
    username: root
  data:
    elasticsearch:
      cluster-name: my-application
      cluster-nodes: 127.0.0.1:9300

实体对象

package com.glj.elasticserarch.demo.biz.dto;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
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;
import java.time.LocalDateTime;
import java.util.List;

/**
 * <p>
 *
 * </p>
 *
 * @author gaoleijie
 * @since 2019-04-09
 */
@Data
@Document(indexName = "user_index",type = "user")
public class SysUser implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    private Integer id;

    @ApiModelProperty(value = "账号")
    @Field(type = FieldType.Keyword)
    private String username;

    @ApiModelProperty(value = "密码")
    @Field(type = FieldType.Keyword)
    private String password;


    @ApiModelProperty(value = "昵称")
    @Field(type = FieldType.Text,analyzer = "ik_max_word")
    private String nickname;

    @ApiModelProperty(value = "邮箱")
    @Field(type = FieldType.Keyword)
    private String email;

    @ApiModelProperty(value = "状态(0:锁定,1:解锁)")
    @Field(type = FieldType.Integer)
    private Integer status;

    @ApiModelProperty(value = "创建人")
    @Field(type = FieldType.Keyword)
    private String createUser;

    @ApiModelProperty(value = "更新人")
    @Field(type = FieldType.Keyword)
    private String updateUser;

    @ApiModelProperty(value = "年龄")
    @Field(type = FieldType.Double)
    private Double age;


}

注意
SpringDataElasticSearch中,只需要操作对象,就可以操作elasticsearch中的数据

注解说明

@Document 作用在类,标记实体类为文档对象
包含属性
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1

@Id 作用在成员变量,标记一个字段作为id主键

@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性
包含属性
type:字段类型,是枚举:FieldType,可以是text、long、short、date、integer、object等

type属性名称 含义
text 存储数据时候,会自动分词,并生成索引
keyword 存储数据时候,不会分词建立索引
Numerical 数值类型,一类为基本数据类型:long、interger、short、byte、double、float、half_float 。一类为浮点数的高精度类型:scaled_float 需要指定一个精度因子,比如10或50,elasticsearch会把真实值乘以这个因子后存储,取出时再还原
Date日期类型 elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间

index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称,这里的ik_max_word即使用ik分词器

创建索引

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @PostMapping("/createIndex")
    @ApiOperation("创建索引")
    public Boolean createIndex(@RequestParam String indexName){
        return  elasticsearchTemplate.createIndex(indexName);
    }

    @PostMapping("/createIndex")
    @ApiOperation("创建索引")
    public Boolean createIndex(){
        return  elasticsearchTemplate.createIndex(SysUser.class);
    }

可以根据类的信息自动生成,也可以手动指定indexName和Settings

删除索引

   @PostMapping("/deleteIndex")
    @ApiOperation("删除索引")
    public Boolean deleteIndex(@RequestParam String indexName){
        // return elasticsearchTemplate.deleteIndex(SysUser.class);
        return  elasticsearchTemplate.deleteIndex(indexName);
    }

可以根据类名或索引名删除

新增文档之Repository

Repository接口
Spring Data 的强大之处,在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。

来看下Repository的继承关系,自己新建一个接口,然后继承ElasticsearchRepository 就好了

package com.glj.elasticserarch.demo.biz.repository;

import com.glj.elasticserarch.demo.biz.dto.SysUser;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

/**
 * @ClassName ElasticSerarchRepository
 * @Description TODO
 * @Author gaoleijie
 * @Date 2019/7/18 10:15
 **/
public interface UserRepository extends ElasticsearchRepository<SysUser,Long> {
    /**
     * 根据昵称查找用户
     * @param nickName
     * @return
     */
    List<SysUser> findByNickname(String nickName);

    /**
     * 根据昵称或者用户名进行查找
     * @param nickName
     * @param Password
     * @return
     */
    List<SysUser> findByNicknameOrPassword(String nickName,String Password);
}

再来看下ElasticsearchRepository

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.data.elasticsearch.repository;

import java.io.Serializable;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.data.repository.NoRepositoryBean;

@NoRepositoryBean
public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> {
    <S extends T> S index(S var1);

    Iterable<T> search(QueryBuilder var1);

    Page<T> search(QueryBuilder var1, Pageable var2);

    Page<T> search(SearchQuery var1);

    Page<T> searchSimilar(T var1, String[] var2, Pageable var3);

    void refresh();

    Class<T> getEntityClass();
}

新增文档

 @PostMapping("/save")
    @ApiOperation("新增")
    public SysUser save(@RequestBody SysUser user){
        return repository.save(user);
    }

    @PostMapping("/saveAll")
    @ApiOperation("批量新增")
    public Iterable<SysUser> saveAll(@RequestBody List<SysUser> users){
      return  repository.saveAll(users);
    }

运行完毕后,可以进入 http://localhost:9100/ 查看效果

image.png

注意
elasticsearch中本没有修改,它的修改原理是该是先删除再新增修改和新增是同一个接口,区分的依据就是id。

查询

    @PostMapping("/findAllAndSort")
    @ApiOperation("查询全部并根据密码排序")
    public Iterable<SysUser> findAllAndSort(){
       return repository.findAll(Sort.by("password").ascending());
    }

    @PostMapping("/findAll")
    @ApiOperation("查询全部")
    public Iterable<SysUser> findAll(){
        return repository.findAll();
    }

自定义方法

不知道大家有没有看到在我的UserRepository 接口中有自定义的查询方法,这些方法就是接下来我们要讲的 Spring Data 的另一个强大功能,是根据方法名称自动实现功能。

比如:你的方法名叫做:findByName,那么它就知道你是根据name查询,然后自动帮你完成,无需写实现类。

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

Keyword Sample
\color{red}{And} findByNameAndPrice
\color{red}{Or} findByNameOrPrice
\color{red}{Is} findByName
\color{red}{Not} findByNameNot
\color{red}{Between} findByPriceBetween
\color{red}{LessThanEqual} findByPriceLessThan
\color{red}{GreaterThanEqual} findByPriceGreaterThan
\color{red}{Before} findByPriceBefore
\color{red}{After} findByPriceAfter
\color{red}{Like} findByNameLike
\color{red}{StartingWith} findByNameStartingWith
\color{red}{EndingWith} findByNameEndingWith
\color{red}{Contains/Containing} findByNameContaining
\color{red}{In} findByNameIn(Collection<String>names)
\color{red}{NotIn} findByNameNotIn(Collection<String>names)
\color{red}{Near } findByStoreNear
\color{red}{True } findByAvailableTrue
\color{red}{False} findByAvailableFalse
\color{red}{OrderBy} findByAvailableTrueOrderByNameDesc

例如我们下面两个例子,按照”昵称“去查找用户和”按照昵称或者密码“去查找,
不需要写实现类,然后我们直接去运行

    @PostMapping("/findByNickname")
    @ApiOperation("根据昵称查询用户")
    public List<SysUser> findByNickname(@RequestParam("nickname")String nickName){
        List<SysUser> list =  repository.findByNickname(nickName);
        return list;
    }

    @PostMapping("/findByNicknameOrPassword")
    @ApiOperation("根据昵称或者密码查询用户")
    public List<SysUser> findByNicknameOrPassword(@RequestParam("nickname")String nickName,@RequestParam("password")String Password){
        List<SysUser> list =  repository.findByNicknameOrPassword(nickName,Password);
        return list;
    }
package com.glj.elasticserarch.demo.biz.repository;

import com.glj.elasticserarch.demo.biz.dto.SysUser;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import java.util.List;

/**
 * @ClassName ElasticSerarchRepository
 * @Description TODO
 * @Author gaoleijie
 * @Date 2019/7/18 10:15
 **/
public interface UserRepository extends ElasticsearchRepository<SysUser,Long> {
    /**
     * 根据昵称查找用户
     * @param nickName
     * @return
     */
    List<SysUser> findByNickname(String nickName);

    /**
     * 根据昵称或者用户名进行查找
     * @param nickName
     * @param Password
     * @return
     */
    List<SysUser> findByNicknameOrPassword(String nickName,String Password);
}

查询结果


image.png

自定义查询

@PostMapping("/query")
    @ApiOperation("自定义查询")
    public Page<SysUser> query(@RequestParam("username")String userName){
        NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
        builder.withQuery(QueryBuilders.matchQuery("username",userName));
        //如果实体和数据的名称对应就会自动封装,pageable分页参数
        Page<SysUser> items = this.repository.search(builder.build());
        long total = items.getTotalElements();
        System.out.println("查询数量为:"+total);
        return items;
    }

NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体

QueryBuilders.matchQuery(“username”, userName):利用QueryBuilders来生成一个查询。QueryBuilders提供了大量的静态方法,用于生成各种不同类型的查询:

Page<SysUser>:默认是分页查询,因此返回的是一个分页的结果对象,包含属性:

totalElements:总条数

totalPages:总页数

Iterator:迭代器,本身实现了Iterator接口,因此可直接迭代得到当前页的数据

其它属性


image.png

模糊查询

 /**
     * 模糊查找
     * @param userName
     * @return
     */
    @PostMapping("/fuzzyQuery")
    @ApiOperation("模糊查找根据分词去模糊,如果默认为5,输入4是没有办法模糊的")
    public Page<SysUser> fuzzyQuery(@RequestParam("username") String userName){
        NativeSearchQueryBuilder builder=new NativeSearchQueryBuilder();
        builder.withQuery(QueryBuilders.fuzzyQuery("username",userName));
        // 查找
        Page<SysUser> page = this.repository.search(builder.build());
        return page;
    }

注意
如果在文档对象里面没有指定分片数量,默认是5,查询值必须大于5的时候才能进行分片查询,否则是查询不出来的,所以如果对于特定的文档,分片数量需要提前指定

聚合

聚合可以让我们极其方便的实现对数据的统计、分析。例如:
什么牌子的手机最受欢迎?
这些手机的平均价格、最高价格、最低价格?
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果

lasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶,一个叫度量


桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个桶,比如如果按照男女进行划分 就会出现”男桶“”女桶“ 类似的桶

Elasticsearch中提供的划分桶的方式有很多:
Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
Histogram Aggregation:根据数值阶梯分组,与日期类似
Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组

度量
综上所述,我们发现bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量

分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为度量

比较常用的一些度量聚合方式:

Avg Aggregation:求平均值
Max Aggregation:求最大值
Min Aggregation:求最小值
Percentiles Aggregation:求百分比
Stats Aggregation:同时返回avg、max、min、sum、count等
Sum Aggregation:求和
Top hits Aggregation:求前几
Value Count Aggregation:求总数

注意:在ES中,需要进行聚合、排序、过滤的字段其处理方式比较特殊,因此不能被分词。这里我们将updateUser和username这两个文字类型的字段设置为keyword类型,这个类型不会被分词,将来就可以参与聚合

聚合为桶

/**
     * 根据列名进行聚合查询
     */
    @PostMapping("/aggregateQuery")
    @ApiOperation("根据列进行聚合查询")
    public void aggregateQuery(@RequestParam("clumname") String clumname){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String []{""},null));
        // 添加一个新的聚合,聚合类型为terms,聚合名称为列明,列名称为
        queryBuilder.addAggregation(
                AggregationBuilders.terms(clumname).field(clumname));
        // 将查询结果转换为聚合分页查询
        AggregatedPage<SysUser> aggPage = (AggregatedPage<SysUser>) this.repository.search(queryBuilder.build());
        StringTerms agg = (StringTerms) aggPage.getAggregation(clumname);
        List<StringTerms.Bucket> buckets = agg.getBuckets();
        // 3.3、遍历
        for (StringTerms.Bucket bucket : buckets) {
            // 3.4、获取桶中的key,即列名称
            System.out.println(bucket.getKeyAsString());
            // 3.5、获取桶中的某列的数量
            System.out.println(bucket.getDocCount());
        }

    }

结果显示


image.png

关键API
AggregationBuilders:聚合的构建工厂类。所有聚合都由这个类来构建

(1)统计某个字段的数量
  ValueCountBuilder vcb=  AggregationBuilders.count("count_nickname").field("nickname");
(2)去重统计某个字段的数量(可能有部分误差)
 CardinalityBuilder cb= AggregationBuilders.cardinality("distinct_count_name").field("nickname");
(3)聚合过滤
FilterAggregationBuilder fab= AggregationBuilders.filter("name_filter").filter(QueryBuilders.queryStringQuery("nickname:刘德华"));
(4)按某个字段分组
TermsBuilder tb=  AggregationBuilders.terms("group_name").field("name");
(5)求和
SumBuilder  sumBuilder= AggregationBuilders.sum("sum_price").field("price");
(6)求平均
AvgBuilder ab= AggregationBuilders.avg("avg_price").field("price");
(7)求最大值
MaxBuilder mb= AggregationBuilders.max("max_price").field("price"); 
(8)求最小值
MinBuilder min= AggregationBuilders.min("min_price").field("price");
(9)按日期间隔分组
DateHistogramBuilder dhb= AggregationBuilders.dateHistogram("dh").field("date");
(10)获取聚合里面的结果
TopHitsBuilder thb=  AggregationBuilders.topHits("top_result");
(11)嵌套的聚合
NestedBuilder nb= AggregationBuilders.nested("negsted_path").path("quests");
(12)反转嵌套
AggregationBuilders.reverseNested("res_negsted").path("kps ");

AggregatedPage:聚合查询的结果类

嵌套聚合,求平均值

 @PostMapping("/arrregateAvg")
    @ApiOperation("根据列进行聚合查询求平均值")
    public void arrregateAvg(@RequestParam("clumname") String clumname){
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String []{""},null));

        queryBuilder.addAggregation(AggregationBuilders.terms(clumname).field(clumname).subAggregation(AggregationBuilders.avg("ageAvg").field("age")));

        AggregatedPage aggPage =(AggregatedPage<SysUser>) repository.search(queryBuilder.build());

        StringTerms agg = (StringTerms) aggPage.getAggregation(clumname);
        // 3.2、获取桶
        List<StringTerms.Bucket> buckets = agg.getBuckets();

        // 3.3、遍历
        for (StringTerms.Bucket bucket : buckets) {
            System.out.println(bucket.getKeyAsString()+",共"+bucket.getDocCount()+"编");

            // 3.6.获取子聚合结果:
            InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("ageAvg");
            System.out.println("平均售价:" + avg.getValue());
        }

    }

查询结果


image.png

结束语

本文章大部分为自己实战出来的,实战思路是参考文章:SpringBoot整合Elasticsearch
如有什么不对,请及时指正。

推荐阅读更多精彩内容