SpringBoot整合Elasticsearch(一)

SpringBoot整合Elasticsearch

基础环境

  • SpringBoot 版本 : 2.4.0
  • ES 版本: 7.9.3
  • Kibana版本: 7.9.3
  • SpringBoot内置Tomcat版本: 9.0.39
  • spring-boot-starter-data-elasticsearch 版本: 对应springboot版本即可
    首先说明下版本对应的重要性和关联,在进行SpringBoot和ES整合的时候,版本的对应关系尤其重要,一般情况下,建议在SpringBoot官网上找到springboot中对应的spring-boot-starter-data-elasticsearch 版本,根据版本号判断支持的ES的版本;其次根据spring-data-es 的版本找到对应的springboot版本,需要在marven仓库中确认该springboot版本是否支持对应的ES版本,很有可能出现marven仓库中的版本对应关系和文档中有一定的出入。在springboot官网对应的es文档中,对应的关系如下:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xA7M1hai-1607665361839)(en-resource://database/1029:1)]
    参考链接: SpringBoot-Data-ES文档
    需要的springboot版本是 2.3.x,但实际在marven仓库中,2.3.x支持的版本是 6.8.x版本的es,而目前es最高版本已经升级至7.10 ,实际情况就是可能出现一定的兼容性问题,查看spring-boot-starter 2.4.0 版本,可以看到支持的es的版本为7.9.3,为当前次新的版本。综合来说,选择springboot 2.4.0 整合es 7.9.3 版本。
    其次直接选择SpringBoot 2.4.0 对于其内置的tomcat版本也有一定的需求,在marven仓库中也有标注,需要将内置tomcat内置版本升级至9.0.39,在此基础上,进行配置文件的配置,可以启动项目

配置文件

相对而言,springboot整合es对于配置文件中需要配置的信息是比较少的,目前刚开始的情况下,我本地项目中仅适用了两个配置,具体如下:

spring.elasticsearch.rest.uris=http://127.0.0.1:9200
spring.data.elasticsearch.repositories.enabled = true
在Springboot-es 7.9.x 的版本整合中,已经放弃了原本5.6.1至今的节点名称配置,也逐步放弃适用TransportClient的API方式,改为Rest方式API进行调用,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L5LEZ9xq-1607665361845)(en-resource://database/1031:1)]

在配置文件中如果进行上面图片中配置,会有一定的报错或提示Deprecated,为了比较好的兼容,直接使用rest.uris 配置相对而言更合适,并且在开发中进行复杂查询时可以直接使用**ElasticsearchRestTemplate**。

实体类

    在ES 7.0之前的版本,ES中有 索引(index)、类型(type)、文档(doc)、字段(field) ,分别对应常规的关系型数据库中的 数据库、表、行、列,但如果整合的ES的版本为7.0及以上版本的时候,是移除了type的概念,从ES的官方解释来说,ES初期设计是参考关系型数据库的设计模式,所以存在了type(数据表)的概念,但ES本身是基于Luncene,根本快的原因是因为 倒序索引 的使用,而倒序索引 是基于index的,而非type,多个type反而会降低es搜索的速度,所以逐步过渡的情况下,es在7.x版本中去除了type。
    另外一种原因是因为type一定程度上限制了es的使用,比如两个实体类 User 和 WechatUser  其中都有 age 这个字段,但在User中字段类型是 int ,但在WechatUser中类型是 double ,在7.x之前的版本中,必须要求 User 和 WechatUser 的 type中 age这个字段的类型必须完全一致,但 7.x 的版本中,es所有不同类型的同字段内部使用的是同一个字段存储,区分type反而会降低es 压缩数据的能力。

在建立Bean的时候,需要在类头部添加@Document(index="xxx")的注解,并且在对应字段上添加@Field字段,可以根据实际使用情况添加是否使用分词器(分词器的作用后续再说),es 中比较特殊的是日期类型的格式,比较建议使用自定义注解的方式去定义,es默认会使用时间戳的方式进行保存。自定义时间格式的注解为:
@Field(type = FieldType.Date,format = DateFormat.custom,pattern = 
    "yyyy-MM-dd HH:mm:ss")private Date invoiceDate;

基础CRUD

    Java中对于ES数据的查询主要分为两种,一个是继承 Repository<T, ID> ,实现一部分简单的CRUD查询,比较类似于mybatis-plus中的查询。Repository 的结构如下: PagingAndSortingRepository 继承 CrudRepository,继承  Repository,一般情况我们实际使用过程中 继承 PagingAndSortingRepository,基本可以实现简单的单个保存、批量保存、根据id删除等。
    另外一种方式是复杂查询(这里仅仅指组合查询),通过ElasticsearchRestTemplate 进行自定义数据检索操作。
Repository使用

新建一个Service直接继承PagingAndSortingRepository进行查询。

    public interface UserRepository extends PagingAndSortingRepository<User,Long> {}

继承Repository后,可以直接在Service层进行注入使用,

@Autowiredprivate UserRepository userRepository;
@Testvoid saveUser(){    
    Iterable<User> all = userRepository.saveAll(null);
}

userRepository.xxx方法基本实现了能够对实体类进行基本CRUD操作,但还有一些简单查询,但并不是已经默认定义好的,也可以通过一些关键字来实现。简单展示如下:

public interface InvoiceRepository extends 
PagingAndSortingRepository<Invoice,Long> {   
List<Invoice> findByInvoiceNum(String invoiceNum);   
long deleteByInvoiceCodeAndInvoiceNumAndCompanyId(String invoiceCode,String invoiceNum,String companyId);}


两条自定义的处理语句作用为1:根据发票号码查询数据,2: 根据发票代码、号码、公司ID 删除发票数据,其中组建自定义语句的主要的关键字有 And、Or、Is、Not、Between、Like、After等,具体参考链接:自定义Repository关键字 ,参考文件篇幅为8.2.2 Query creation。
同样的定义语句也可以使用@Query(“es原生查询语句”)的方式进行编写,具体如下:

@Query("{ \"query\" : { \"bool\" : { \"must\" : [ { \"query_string\" : { 
\"query\" : \"?\", \"fields\" : [ \"invoiceCode\" ] } } ] } }}")long findByInvoiceCode(String invoiceCode);


原生语法DSL
索引结构
  • 创建索引
    索引的创建在默认情况下分片数量是5个,副本数量是1个,但可以在创建的时候显式指定分片数量以及副本数量,具体代码如下,创建3个分片,2个副本的的索引
PUT fatp_sale_invoice
{
  "settings": {"number_of_shards": 3,"number_of_replicas": 2}
}

# 返回
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "fatp_test1"
}


  • 获取索引
GET fatp_test
# 返回
{
  "fatp_test" : {
    "aliases" : { },
    "mappings" : { },
    "settings" : {
      "index" : {
        "creation_date" : "1607563246081",
        "number_of_shards" : "3",
        "number_of_replicas" : "2",
        "uuid" : "44UK8efkTRWFqzakTYTSxQ",
        "version" : {
          "created" : "7090399"
        },
        "provided_name" : "fatp_test"
      }
    }
  }
}

  • 删除索引
    删除索引这件事最好还是谨慎点,这操作基本等同于删库了,一般情况下是不建议开发人员有操作删除索引这个权限的。
DELETE fatp_test

# 返回
{
  "acknowledged" : true
}

  • 重建索引
    重建索引最基本的功能是拷贝文件从一个索引到另一个索引,语法如下:
POST _reindex
{
  "source": {"index": "fatp_sale_invoice"},
  "dest": {"index":"fatp_sale_invoice_bk"}
}

# 返回
{
  "took" : 19009,
  "timed_out" : false,
  "total" : 23425,
  "updated" : 0,
  "created" : 23425,
  "deleted" : 0,
  "batches" : 24,
  "version_conflicts" : 0,
  "noops" : 0,
  "retries" : {
    "bulk" : 0,
    "search" : 0
  },
  "throttled_millis" : 0,
  "requests_per_second" : -1.0,
  "throttled_until_millis" : 0,
  "failures" : [ ]
}

参数说明:
took : 耗时毫秒数,
timed_out : 是否超时,
total : 总数据量,
updated : 更新数据量,
created : 创建数据量,
batches : 重重建索引拉回的滚动响应的数量

在此基础上,重建索引不仅仅可以全量复制,也可以添加一定的查询条件,限定值复制符合条件的数据,如下:

POST _reindex
{
  "source": {"index": "fatp_sale_invoice",
  "query": {"match_phrase": {
    "invoiceNum": "112233"
  }}},
  "dest": {"index":"fatp_sale_invoice_bk"}
}
  • 字段映射类型
    es 中对于每个字段都有自己单独的类型,并且同一个字段同时拥有不同的类型,这个实现主要基于7.x 更新后更加明显的体现出,具体不多赘述,在之前的 实体类 介绍中已进行过一部分说明,字段的类型主要有 text、integer、date、double等,基本能够满足所有的字段类型的要求,一般有两种方式进行创建索引,一个是通过es原生语法创建时指定类型,另外是通过java等语言进行整合实体类的创建,保存数据的时候会自动创建,但如果使用代码进行创建索引,未指定具体字段类型的话,直接保存数据,es会默认读取某字段第一次保存时的值的类似进行判断获取字段值类型,后续如果值和当前值类型不匹配的时候,就会无法保存,所以建议通过代码创建索引的时候,一开始明确指定字段的具体类型。需要注意的是,在es中如果需要对某个字段进行排序或分组,需要设置字段的fielddata为true
  • 创建、更新映射
    创建和更新索引都是使用PUT方法,第一个参数为索引名称,第二个参数为更新索引映射,也就是字段设置,简单创建一个索引以及一个字段的语法如下,其中 “fielddata”:true ,名称为:字段数据,在使用 排序、聚合以及脚本访问中需要使用到的字段必须设置fielddata:true,缺点在于使用fielddata的字段会占据比较大的的堆内存空间,需要谨慎设置
PUT fatp_sale_invoice/_mapping
{
  "properties":{
    "invoiceLine":{
      "type":"text",
      "fielddata":true
    }
  }
}
  • 查询索引字段映射
# 查询某索引下所有字段映射
GET fatp_sale_invoice/_mapping/
# 返回结果
{
  "fatp_sale_invoice" : {
    "mappings" : {
      "properties" : {
        "_class" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "invoiceDate" : {
          "type" : "date",
          "format" : "date_hour_minute_second"
        },
        "billingMethodName" : {
          "type" : "text"
        },
        "totalAmount" : {
          "type" : "double"
        },
        "teamId" : {
          "type" : "integer"
        },
        "buyerName" : {
          "type" : "text"
        }
       }
    }
  }
} 


# 查询某索引下具体字段类型映射
GET fatp_sale_invoice/_mapping/field/invoiceNum
# 返回结果
{
  "fatp_sale_invoice" : {
    "mappings" : {
      "invoiceNum" : {
        "full_name" : "invoiceNum",
        "mapping" : {
          "invoiceNum" : {
            "type" : "text",
            "fielddata" : true
          }
        }
      }
    }
  }
}

查询过滤 query/filter
  • 在ES中查询和过滤有两种方式,一种是过滤、一种是查询,过滤主要使用关键字 filter,查询主要根据query中三个关键字 must/must not/should,两者的区别主要在于 filter 不参与评分,缓存数据,query 计算评分,不缓存数据,相对而言,很多时候filter会比queyr快。当然实际使用过程中,must/must not /should 等一般都是和 filter结合使用,简单举个栗子:
GET fatp_sale_invoice/_search
{
  "query": {
    
    "bool": {
      "must": [
        {
          "match_phrase": {
            "companyId": "91310230674560485R"
          }
        },
        {
          "match_phrase": {
            "invoiceStatus": "1"
          }
        }
      ],
      "must_not": [
        {"match_phrase": {
          "isCancel": "1"
        }}
      ], 
      
      
      "filter": [
        {"range": {
          "invoiceDate": {
            "gte": "2020-01-01T00:00:00",
            "lte": "2020-01-31T23:59:52"
          }
        }}
        
      ]
    }
    
  },
  "sort": [
    {
      "invoiceNum": {
        "order": "desc"
      }
    }
  ],
  "size": 10, 
  "track_total_hits":true
}
  • 上述这段代码的主要实现的功能就是,通过几个must 条件,判断某些字段必须满足部分条件,某些字段必须不等于某些条件,最后通过时间范围进行数据过滤,对上述代码中的结构和作用进行简单解释下:
    1、curl 中的 GET 方法代表查询, fatp_sale_invoice 代表索引名称, _search 代表该语句时为了查询
    2、外层结构 query ,代表整个查询体,其中内部可以定义很多查询语句,这里定义了bool类型,因为需要对多个字段进行条件限定和条件否定,当然 query 结构体中也可以直接使用 match_phrase 等查询语法进行操作,但同样的查询语法关键字只能出现一次。
    3、match_phrase 短语匹配,这个内容会在后续进行详细解释,match_phrase 意义为查询的关键字中的某个字 一段 内容能够和查询的值匹配就会查询出,有点类似于 sql 中 like ‘%xx%’ 的意。
    4、filter 过滤,对查询出结果,根据时间范围进行过滤
    5、range 范围过滤关键字 ,gte 大于等于,lte 小于等于
    6、sort 排序 ,可以对查询结果根据某字段进行升序、降序排序
    7、size 查询展示数量,不写该属性的情况下,kibana 默认展示10条数据
    8、track_total_hits 返回命中结果是否全部显示 ,默认false 。在kibana中如果不设定该值,当返回结果总数大于10000的时候也会仅显示10000

    上述查询结果返回值如下:

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 6,
    "successful" : 6,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 170,
      "relation" : "eq"
    },
    "max_score" : 5.207824E-4,
    "hits" : [
      {
        "_index" : "fatp_sale_invoice",
        "_type" : "_doc",
        "_id" : "10444391",
        "_score" : 5.207824E-4,
        "_source" : {
          "_class" : "com.aisino.fatp.entity.Invoice",
          "id" : 10444391,
          "orderId" : "7e4725b3d9499897b",
          "companyId" : "91310230674560485R",
          "userId" : "618",
          "invType" : "s",
          "invoiceCode" : "310423422130",
          "invoiceNum" : "51923230",
          "invoiceDate" : "2020-01-14T06:56:28",
          "invoiceStatus" : "1",
          "buyerName" : "xxx媒有限公司",
          "buyerTaxNo" : "9xxxxx7507",
          "buyerAddrTel" : "上海市xxx15号16幢021-888888880",
          "buyerBankAccount" : "xxxx路支行975323232323740000451",
          "saleserName" : "上海xxxx有限公司",
          "saleserTaxNo" : "91310sfasassa0485R",
          "saleserAddrTel" : "xxxxxxx楼5211111111666",
          "saleserBankAccount" : "上海xxx支行31222222200759391",
          "totalAmount" : 1980.0,
          "noTaxAmount" : 1867.92,
          "taxAmount" : 112.08,
          "isCancel" : "0",
          "clerk" : "1111xxx",
          "invoiceSource" : "1",
          "invoiceLine" : "1",
          "machineNo" : "5",
          "listType" : "0",
          "taxType" : "1",
          "departmentId" : 28,
          "employeeId" : 14,
          "employeeName" : "张三",
          "teamId" : 33,
          "teamName" : "软件市场部-渠道销售一组",
          "goldTaxNo" : "661232323814491",
          "invoiceImgStatus" : 0,
          "invoiceDetails" : [
            {
              "goodsName" : "服务费",
              "goodsId" : 111,
              "detailTaxFee" : 6,
              "detailTotalAmount" : 106,
              "detailNoTaxAmount" : 100,
              "taxRate" : 0.06,
              "goodsQuantity" : 1.0,
              "goodsPrice" : 100,
              "taxClassCode" : "3040201030000000000",
              "taxShortName" : "信息技术服务",
              "goodsLineType" : "0",
              "taxPreFlag" : "0",
              "lineNo" : 1
            }
          ]
        }
      }
    ]
  }
}

  • 字段说明:
    took : 耗时毫秒数,
    hits : total :总命中数量,
    hits : _index 索引,_type 文档,_source 显示字段列表,_class 对应实体类路径
Java Template使用(基础使用)
查询
在编写Java中查询语句时,可以使用SearchQueryBuilder 进行封装实际需要的查询语句,一般情况下我使用NativeSearchQueryBuilder 较多,可以自定义的构建QueryBuilders中的各个查询,比如 boolQuery 、matchQuery、 matchPhraseQuery、rangeQuery等,一 一介绍如下,建议es原生的写法和Java中写法结合起来对照使用。
  • boolQuery
    booQuery 在ES原生语法中是bool ,作为一种是否判断,主要有三种判断 must 、must_not 、should ,等同理解为 and 、not 、or,在实际的查询中我们需要动态组合使用,首先了解下同一个查询在原生语法中应该怎么实现,代码如下:
# 查询税号为 91310230674560485R ,分机号为 1 ,并且发票作废状态不等于 1 的发票数据
GET fatp_sale_invoice/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {
          "companyId": "91310230674560485R"
          }
        },
        {
          "match_phrase": {
            "machineNo": "1"
          }
        }
      ],
      "must_not": [
        {
          "match_phrase": {
            "isCancel": "1"
          }
        }
      ]
    }
    
  },
  "track_total_hits":true
}
  • 在Java中实现,代码如下:
NativeSearchQueryBuilder searchQueryBuilder = new 
NativeSearchQueryBuilder();BoolQueryBuilder 
boolQueryBuilder =        QueryBuilders.boolQuery().must(QueryBuilders.matchPhraseQuery("companyId","91310230674560485R"))                
.must(QueryBuilders.matchPhraseQuery("machineNo","1"))                .mustNot(QueryBuilders.matchPhraseQuery("isCancel","1"));
searchQueryBuilder.withQuery(boolQueryBuilder);
SearchHits<Invoice> search = 
elasticsearchRestTemplate.search(searchQueryBuilder.build(), Invoice.class);
// 总数long totalHits = search.getTotalHits();
System.out.println(totalHits);
// 发票数据List<Invoice> invoiceList = search.get().map(SearchHit::getContent).collect(Collectors.toList());

  • rangeQuery
    范围查询对于一般的字段类型来说是没有难度的,原生代码如下:
GET fatp_sale_invoice/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {
          "companyId": "91310230674560485R"
          }
        },
        {
          "match_phrase": {
            "machineNo": "1"
          }
        },
        {
          "range": {
            "totalAmount": {
              "gte": 300,
              "lte": 10000
            }
          }
        },
        {
          "range": {
            "invoiceDate": {
              "gte": "2020-01-01T00:00:00",
              "lte":"2020-01-31T23:59:59"
            }
          }
        }
      ],
      "must_not": [
        {
          "match_phrase": {
            "isCancel": "1"
          }
        }
      ]
    }
    
  },
  "track_total_hits":true
}
  • 区别在于日期范围的查询,es中默认日期存储的格式为 timestamp 类型,在显示和查询上不太方便,一般在存储日期类型的字段会指定格式,使用 fortmat 参数指定日期格式为: yyyy-mm-dd HH:mm:ss ,如下代码是上述返回中部分字段,
            "invoiceDate" : "2020-06-19T02:27:00",
          "invoiceStatus" : "1",
          "invoiceRemark" : "",
          "buyerName" : "上海爱信诺航天信息有限公司",
          "buyerTaxNo" : "913101047989613362",
  • 注意到,日期格式会在中间有个“T”字母,并且这个字母是无法使用格式化语句进行格式化的,所以目前来说,我的处理方案是自己拼接查询日期格式,上述原生查询在代码中实现如下:
NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();BoolQueryBuilder 
boolQueryBuilder =        QueryBuilders.boolQuery()
.must(QueryBuilders.matchPhraseQuery("companyId", "91310230674560485R"))                .must(QueryBuilders.matchPhraseQuery("machineNo","1"))                
.must(QueryBuilders.rangeQuery("totalAmount").gte(300).lte(1000))                
.must(QueryBuilders.rangeQuery("invoiceDate").gte("2020-01-01T00:00:00").lte("2020-01-31T23:59:59"))                .mustNot(QueryBuilders.matchPhraseQuery("isCancel","1"));
searchQueryBuilder.withQuery(boolQueryBuilder);
// 查询
SearchHits<Invoice> search = 
elasticsearchRestTemplate.search(searchQueryBuilder.build(), Invoice.class);

  • matchQuery、matchPhraseQuery、matchPhrasePrefixQuery
    在上面的查询中,使用到了matchPhraseQuery ,matchQuery/matchPhraseQuery/matchPhrasePrefixQuery 这三个关键字查询的区别主要在于 matchQuery 是直接进行查询,在文本字段上进行全文检索,一般更更多是结合分词器进行使用。matchPhraseQuery 短语查询,查询字段中某一部分完全匹配查询条件。matchPhrasePrefixQuery 短语查询的基础下,对文本最后一个字段进行前缀匹配。
  • multiMatchQuery
    多字段匹配同一个值,比如以下dsl:
GET fatp_sale_invoice/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_phrase": {
          "invoiceNum": "112233"
          }
        },
        {
          "match_phrase": {
            "invoiceCode": "122"
          }
        }
      ]
    }
    
  },
  "track_total_hits":true
}

  • 查询发票代码、发票号码等于 112233 的结果,以上查询可以优化为:
GET fatp_sale_invoice/_search
{
  "query": {
    "multi_match": {
      "query": "112233",
      "fields": ["invoiceCode","invoiceNum"]
    }
  }
}
  • 并且可以匹配正则模糊匹配,比如查询所有字段前缀为 invoice* 的字段,都等于 112233 的值,如下:

GET fatp_sale_invoice/_search
{
  "query": {
    "multi_match": {
      "query": "112233",
      "fields": ["invoice*"]
    }
  }
}
  • java中实现代码如下:
NativeSearchQueryBuilder searchQueryBuilder = new 
NativeSearchQueryBuilder();MultiMatchQueryBuilder multiMatchQueryBuilder = 
QueryBuilders.multiMatchQuery("11223", new String[]{"invoiceCode", "invoiceNum"});
searchQueryBuilder.withQuery(multiMatchQueryBuilder);SearchHits<Invoice> search = 
elasticsearchRestTemplate.search(searchQueryBuilder.build(), Invoice.class);

  • termsQuery
    单个字段匹配多个值,类似于 in,可以通过一个字段,判断是否同时匹配多个值列表,原生语法如下:
GET fatp_sale_invoice/_search
{
  "query": {
    "terms": {
      "invoiceCode": [
        "11",
        "22"
      ]
    }
  }
}
  • 代码查询为,查询发票代码在[“11”,“22”]的数据,在java中实现如下:
NativeSearchQueryBuilder searchQueryBuilder = new 
NativeSearchQueryBuilder();TermsQueryBuilder termsQuery = QueryBuilders.termsQuery("invoiceCode", 
new String[]{"11", "22"});
searchQueryBuilder.withQuery(termsQuery);
SearchHits<Invoice> search = 
elasticsearchRestTemplate.search(searchQueryBuilder.build(), Invoice.class);
  • filter
    上文提到过,es 中查询过滤是可以实现类似的效果的,两者区别就在于filter是不计算匹配得分的,只是简单决定文档是否匹配,主要用于过滤结构化的数据,并且可以缓存数据提高性能效率。举个使用栗子:
GET fatp_sale_invoice/_search
{
  "query": {
    
    "bool": {
      "must": [
        {
          "match_phrase": {
            "companyId": "91310230674560485R"
          }
        },
        {
          "match_phrase": {
            "invoiceStatus": "1"
          }
        }
      ],
      "must_not": [
        {"match_phrase": {
          "isCancel": "1"
        }}
      ], 
      
      
      "filter": [
        {"range": {
          "invoiceDate": {
            "gte": "2020-01-01T00:00:00",
            "lte": "2020-01-31T23:59:52"
          }
        }}
        
      ]
    }
    
  } ,
  "sort": [
    {
      "invoiceNum": {
        "order": "desc"
      }
    }
  ], 
  "size": 1, 
  "track_total_hits":true
}
  • filter 可以用在查询中直接使用,也可以作为聚合分组的条件,具体后续介绍,上述代码在java 中实现如下:
NativeSearchQueryBuilder searchQueryBuilder = new 
NativeSearchQueryBuilder();BoolQueryBuilder 
boolQueryBuilder =        QueryBuilders.boolQuery()                
.must(QueryBuilders.matchPhraseQuery("companyId", "91310230674560485R"))                .must(QueryBuilders.matchPhraseQuery("invoiceStatus","1"))                .mustNot(QueryBuilders.matchPhraseQuery("isCancel","1"))                
.filter(QueryBuilders.rangeQuery("invoiceDate").gte("2019-12-01T00:00:00").lte("2019-12-31T23:59:59"))                ;
FieldSortBuilder invoiceNumSort = 
SortBuilders.fieldSort("invoiceNum").order(SortOrder.DESC);
searchQueryBuilder.withQuery(boolQueryBuilder).withSort(invoiceNumSort);
  • from size
    分页,es中的分页其实原则上来说就等价于sql 中的limit,区别在于,limit可以接受一个参数,但在es中,如果仅仅需要查询数量,可以直接使用size后面接数量,如果是一个范围的,就需要 from 和 size 一起用,具体代码如下:
GET fatp_sale_invoice/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, 
  "size": 20,
  "track_total_hits": true
}
  • 这段代码时最简单的分页查询,java 中实现也是比较简单,具体关键代码如下:
searchQueryBuilder.withPageable(PageRequest.of(0,20));
  • sort
    排序,这里说的排序仅仅是查询的排序,并不包含聚合后排序以及过滤后的排序,基础的排序语法,原生代码如下:
GET fatp_sale_invoice/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, 
  "size": 20,
  "sort": [
    {
      "invoiceNum": {
        "order": "desc"
      }
    }
  ], 
  "track_total_hits": true
}

这段代码时分页后,根据发票号码升序排序,Java中代码实现如下:

FieldSortBuilder invoiceNumSort = 
SortBuilders.fieldSort("invoiceNum").order(SortOrder.DESC);searchQueryBuilder.withSort(invoiceNumSort);
  • aggregation
    聚合应该算是es中最核心也最重要的部分,这里仅仅会简单介绍下聚合的使用,详细点的介绍会在后续中进行介绍,参考的文章如下 Elasticsearch 聚合的重要概念,下面介绍一个简单使用,根据票种进行聚合,展示的字段为发票代码、号码,然后计算每个票种类型下发票金额合计,原生代码如下:
GET fatp_sale_invoice/_search
{
  "query": {
    
    "bool": {
      "must": [
        {
          "match_phrase": {
            "companyId": "91310230674560485R"
          }
        },
        {
          "match_phrase": {
            "invoiceStatus": "1"
          }
        }
      ],
      "must_not": [
        {"match_phrase": {
          "isCancel": "1"
        }}
      ], 
      
      
      "filter": [
        {"range": {
          "invoiceDate": {
            "gte": "2020-01-01T00:00:00",
            "lte": "2020-01-31T23:59:52"
          }
        }}
        
      ]
    }
    
  } ,
  "aggs": {
    "invTypeAggs": {
      "terms": {
        "field": "invType"
      },
      
      
     "aggs": {
       "mytopHits":{
         "top_hits": {
           "size": 1,
           "_source": ["invoiceCode","invoiceNum"]
         }
       },
       "sumTotalMoney": {
         "sum": {
           "field": "totalAmount"
         }
       }
     } 
    }
  }, 
  "size": 0, 
  "track_total_hits":true
}
  • 返回结果:
{
  "took" : 10,
  "timed_out" : false,
  "_shards" : {
    "total" : 6,
    "successful" : 6,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 170,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "invTypeAggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "s",
          "doc_count" : 97,
          "mytopHits" : {
            "hits" : {
              "total" : {
                "value" : 97,
                "relation" : "eq"
              },
              "max_score" : 5.207824E-4,
              "hits" : [
                {
                  "_index" : "fatp_sale_invoice",
                  "_type" : "_doc",
                  "_id" : "10444391",
                  "_score" : 5.207824E-4,
                  "_source" : {
                    "invoiceCode" : "3100192130",
                    "invoiceNum" : "51934230"
                  }
                }
              ]
            }
          },
          "sumTotalMoney" : {
            "value" : 1120339.42
          }
        },
        {
          "key" : "p",
          "doc_count" : 56,
          "mytopHits" : {
            "hits" : {
              "total" : {
                "value" : 56,
                "relation" : "eq"
              },
              "max_score" : 5.207824E-4,
              "hits" : [
                {
                  "_index" : "fatp_sale_invoice",
                  "_type" : "_doc",
                  "_id" : "10444738",
                  "_score" : 5.207824E-4,
                  "_source" : {
                    "invoiceCode" : "031001900111",
                    "invoiceNum" : "89872110"
                  }
                }
              ]
            }
          },
          "sumTotalMoney" : {
            "value" : 7200.0
          }
        },
        {
          "key" : "c",
          "doc_count" : 17,
          "mytopHits" : {
            "hits" : {
              "total" : {
                "value" : 17,
                "relation" : "eq"
              },
              "max_score" : 5.207824E-4,
              "hits" : [
                {
                  "_index" : "fatp_sale_invoice",
                  "_type" : "_doc",
                  "_id" : "10444693",
                  "_score" : 5.207824E-4,
                  "_source" : {
                    "invoiceCode" : "031001800304",
                    "invoiceNum" : "44351980"
                  }
                }
              ]
            }
          },
          "sumTotalMoney" : {
            "value" : 30180.0
          }
        }
      ]
    }
  }
}

  • 这种聚合的查询主要结果看 aggregations节点下的数据,简单解释下字段含义:
    buckets : 桶,将发票票种作为桶,可以分为 p,c,s三个桶,
    buckets - mytopHits - hits: 具体展示的数据
    buckets - sumTotalMoney : 指标,某一个桶下面的合计含税金额

  • 在java 中实现如下:

NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder =       QueryBuilders.boolQuery()                .must(QueryBuilders.matchPhraseQuery("companyId", "91310230674560485R"))                .must(QueryBuilders.matchPhraseQuery("invoiceStatus","1"))                .mustNot(QueryBuilders.matchPhraseQuery("isCancel","1"))                
.filter(QueryBuilders.rangeQuery("invoiceDate").gte("2019-12-01T00:00:00").lte("2019-12-31T23:59:59")) ;
TermsAggregationBuilder termsAggregationBuilder = 
AggregationBuilders.terms("invTypeAggs").field("invType")        
.subAggregation(AggregationBuilders.topHits("mytopHits").sort("invoiceNum", SortOrder.ASC)                
.fetchSource(new String[]{"invoiceCode","invoiceNum"},null).size(1))        
.subAggregation(AggregationBuilders.sum("totalAmount").field("invoiceDetails.detailTotalAmount"));
searchQueryBuilder.withQuery(boolQueryBuilder).addAggregation(termsAggregationBuilder);


  • 对代码层次进行简单介绍下,首先 booleanQueryBuilder 是一个查询以及过滤的构建条件,
    termsAggregationBuilder 是为了根据 invType 分组,起名:invTypeAggs,在此基础上进行过滤除需要展示的发票代码、号码,显示数量为1,同层级下对每一个分组进行金额汇总统计,这种写法不难,比较关键点在于如果获取其中的值,首先获取我们需要展示的invoiceCode,invoiceNum ,代码如下:
SearchHits<InvoiceOrg> search = 
elasticsearchRestTemplate.search(searchQueryBuilder.build(), 
InvoiceOrg.class);
Map<String, Aggregation> stringAggregationMap = search.getAggregations().asMap();
// 获取需要展示的数据
Terms term = (Terms) stringAggregationMap.get("invTypeAggs");
List<? extends Terms.Bucket> buckets = term.getBuckets();
for(Terms.Bucket bucket : buckets){    
    String keyAsString = bucket.getKeyAsString();    
    System.out.println("keyAsString:"+keyAsString);    
    // 获取每个桶下的含税金额
    Sum totalAmountSum = (Sum) bucket.getAggregations().asMap().get("totalAmount");
    TopHits topHits = bucket.getAggregations().get("mytopHits");    
    for(SearchHit hit : topHits.getHits()){        
        System.out.println("id ->"+hit.getId()+hit.getSourceAsMap().toString());    
     }
}

  • 以上是SpringBoot整合ES的简单操作,比较粗糙,希望不要介意,后续会增加更多细节以及用法,会针对比如 boolQuery 里面的must 、must_not、should 顺序以及嵌套层级分别会怎么影响结果以及评分进行单独介绍,聚合分组更是重中之重,也会在后续文章中一一介绍。
结束;
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是SpringBoot整合Elasticsearch的步骤: 1. 添加Elasticsearch的依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> ``` 2. 配置Elasticsearch连接 在application.yml或application.properties中添加以下配置: ``` spring: data: elasticsearch: cluster-name: elasticsearch # Elasticsearch集群名称 cluster-nodes: 127.0.0.1:9300 # Elasticsearch连接地址 ``` 3. 创建Elasticsearch实体类 创建一个Java类,使用@Document注解标记为Elasticsearch文档类型,并使用其他注解指定属性的映射关系,如下所示: ``` @Document(indexName = "my_index", type = "my_type") public class MyDocument { @Id private String id; private String title; private String content; // ... 省略getter和setter方法 } ``` 4. 创建Elasticsearch操作接口 创建一个接口,继承ElasticsearchRepository接口,并指定泛型为步骤3中创建的实体类,如下所示: ``` public interface MyDocumentRepository extends ElasticsearchRepository<MyDocument, String> { } ``` 5. 使用Elasticsearch操作数据 在需要使用Elasticsearch的地方注入MyDocumentRepository,即可使用其提供的方法进行数据的CRUD操作,如下所示: ``` @Autowired private MyDocumentRepository repository; public void save(MyDocument document) { repository.save(document); } public MyDocument findById(String id) { return repository.findById(id).orElse(null); } public void deleteById(String id) { repository.deleteById(id); } ``` 以上就是SpringBoot整合Elasticsearch的基本步骤,希望对你有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值