今日学习:ES8语法 | Spring整合ES | ES场景八股

文章目录

ES8

1 ES架构

在这里插入图片描述

2 ES适用场景

搭建日志系统
ELK套件日志系统应该是ElasticSearch使用最广泛的场景之一了,ElasticSearch支持海量数据的存储和查询,特别适合日志搜索场景。广泛使用的ELK套件(ElasticSearch、Logstash、Kibana)是日志系统最经典的案例,使用Logstash和Beats组件进行日志收集,ElasticSearch存储和查询应用日志,Kibana提供日志的可视化搜索界面。

在这里插入图片描述

搭建数据分析系统
Elasitcsearch支持数据分析,例如强大的数据聚合功能,通过搭配Kibana,提供诸如直方图、统计分组、范围聚合等方便使用的功能,能够快速实现一些数据报表等功能。
在数字化转型的大行其道的当下,需要从海量数据中发现数据的规律,从而做出一定的决策,ElasticSearch一定是最适合的解决方案之一。
在这里插入图片描述
搭建搜索系统
ElasticSearch为搜索而生,用于搭建全文搜索系统是自然而然的事情,它能够提供快速的索引和搜索功能,还有相关的评分功能、分词插件等,支持丰富的搜索特性,可以用于搭建大型的搜索引擎,更加常用语实现站内搜索,例如银行App、购物App等站内商品、服务搜索。

构建海量数据业务系统即席查询服务
目前大量的需要支持事务的系统使用MySQL作为数据库,但随着业务的开展,数据量会越来越大,而MySQL的性能会越来越差,虽然可以通过分库分表的方案进行解决,但是操作比较复杂,而且往往每隔一段时间就需要进行扩展,且代码需要配合修改。
这种情况下可以将数据从MySQL同步到ElasticSearch,针对实时性要求不太高或者主要查询历史数据且数据量比较大的场景使用ElasticSearch提供查询,而对需要事务实时控制的即时数据还是通过MySQL存储和查询。

在这里插入图片描述
作为独立数据库系统
ElasticSearch本身提供了数据持久化存储的能力,并且提供了增删改查的功能,在某些应用场景下可以直接当做数据库系统来使用,既提供了存储能力,又能够同时具备搜索能力,整体技术架构会比较简单,例如博客系统、评论系统。需要注意的是,ElasticSearch不支持事务,且写入的性能相对关系型数据库稍弱,所有需要使用事务的场景都不能将ElasticSearch当做唯一的数据库系统,这使得这种使用场景很少见。

3 ES使用

3.1对比mysql

在这里插入图片描述

3.2 索引(Index)

一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。

能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。

ElasticSearch索引的精髓:一切设计都是为了提高搜索的性能。

3.3 类型(Type)

在一个索引中,你可以定义一种或多种类型。

一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化

版本Type
5.x支持多种type
6.x只能有一种type
7.x默认不再支持自定义索引类型(默认类型为:_doc)
8.x默认类型为:_doc

3.4 文档(Document)

一个文档是一个可被索引的基础信息单元,也就是一条数据

比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。

在一个index/type里面,你可以存储任意多的文档。

3.5 字段(Field)

相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。

3.6 映射(Mapping)

mapping是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,其它就是处理ES里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。

4 ElasticSearch 基础功能

参考文档:https://www.elastic.co/guide/en/ElasticSearch/reference/8.5/ElasticSearch-intro.html

我们在Kibana(前面已经安装过) 软件给大家演示基本操作

详见《软件环境安装》

4.1 分词器

官方提供的分词器有这么几种: Standard、Letter、Lowercase、Whitespace、UAX URL Email、Classic、Thai等,中文分词器可以使用第三方的比如IK分词器。前面我们已经安装过了。

IK分词器:

POST _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}
结果:
{
  "tokens": [
    {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
    },
    {
      "token": "是",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_CHAR",
      "position": 1
    },
    {
      "token": "中国人",
      "start_offset": 2,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 2
    }
  ]
}

POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}
结果:
{
  "tokens": [
    {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "CN_CHAR",
      "position": 0
    },
    {
      "token": "是",
      "start_offset": 1,
      "end_offset": 2,
      "type": "CN_CHAR",
      "position": 1
    },
    {
      "token": "中国人",
      "start_offset": 2,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 2
    },
    {
      "token": "中国",
      "start_offset": 2,
      "end_offset": 4,
      "type": "CN_WORD",
      "position": 3
    },
    {
      "token": "国人",
      "start_offset": 3,
      "end_offset": 5,
      "type": "CN_WORD",
      "position": 4
    }
  ]
}

Standard分词器:

POST _analyze
{
  "analyzer": "standard",
  "text": "我是中国人"
}

结果:
{
  "tokens": [
    {
      "token": "我",
      "start_offset": 0,
      "end_offset": 1,
      "type": "<IDEOGRAPHIC>",
      "position": 0
    },
    {
      "token": "是",
      "start_offset": 1,
      "end_offset": 2,
      "type": "<IDEOGRAPHIC>",
      "position": 1
    },
    {
      "token": "中",
      "start_offset": 2,
      "end_offset": 3,
      "type": "<IDEOGRAPHIC>",
      "position": 2
    },
    {
      "token": "国",
      "start_offset": 3,
      "end_offset": 4,
      "type": "<IDEOGRAPHIC>",
      "position": 3
    },
    {
      "token": "人",
      "start_offset": 4,
      "end_offset": 5,
      "type": "<IDEOGRAPHIC>",
      "position": 4
    }
  ]
}

4.2 索引操作

ES 软件的索引可以类比为 MySQL 中表的概念,创建一个索引,类似于创建一个表

所有RestFul风格API不用记忆,知道每个接口作用即可,会查询官方文档:https://www.elastic.co/guide/index.html

  • RestFul文档:https://www.elastic.co/guide/en/elastic-stack/8.5/index.html
4.2.1 创建索引

语法: PUT /{索引名称}

PUT /my_index

结果:
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "my_index"
}
4.2.2 查看所有索引
GET /_cat/indices?v
4.2.3 查看单个索引

语法: GET /{索引名称}

GET /my_index
结果:
{
  "my_index": {
    "aliases": {},
    "mappings": {},
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        },
        "number_of_shards": "1",
        "provided_name": "my_index",
        "creation_date": "1693294063006",
        "number_of_replicas": "1",
        "uuid": "kYMuXUZQRumMGqHoV0fDJw",
        "version": {
          "created": "8050099"
        }
      }
    }
  }
}
4.2.4 删除索引

语法: DELETE /{索引名称}

DELETE /my_index
结果:
{
  "acknowledged" : true
}

4.3 文档操作

文档是 ES 软件搜索数据的最小单位, 不依赖预先定义的模式,所以可以将文档类比为表的一行JSON类型的数据。我们知道关系型数据库中,要提前定义字段才能使用,在ElasticSearch中,对于字段是非常灵活的,有时候我们可以忽略该字段,或者动态的添加一个新的字段。

4.3.1 创建文档

语法:

PUT /{索引名称}/{类型}/{id}

{

jsonbody

}

在创建数据时,需要指定唯一性标识,那么请求范式 POST,PUT 都可以

PUT /my_index/_doc/1
{
  "title": "小米手机",
  "brand": "小米",
  "images": "http://www.gulixueyuan.com/xm.jpg",
  "price": 3999
}

返回结果:
{
  "_index": "my_index",
  "_id": "1",
  "_version": 3,
  "_seq_no": 2,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "title": "小米手机",
    "brand": "小米",
    "images": "http://www.gulixueyuan.com/xm.jpg",
    "price": 3999
  }
}
4.3.2 查看文档

语法:GET /{索引名称}/{类型}/{id}

GET /my_index/_doc/1
结果:
{
  "_index" : "my_index",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "title" : "小米手机",
    "brand" : "小米",
    "images" : "http://www.gulixueyuan.com/xm.jpg",
    "price" : 3999
  }
}
4.3.3 查询所有文档
语法: GET /{索引名称}/_search
GET /my_index/_search

结果:
{
  "took": 941,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "my_index",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "小米手机",
          "brand": "小米",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 3999
        }
      }
    ]
  }
}
4.3.4 修改文档

语法:

PUT /{索引名称}/{类型}/{id}

{

jsonbody

}

PUT /my_index/_doc/1
{
  "title": "小米手机",
  "brand": "小米",
  "images": "http://www.gulixueyuan.com/xm.jpg",
  "price": 4500
}
4.3.5 修改局部属性

语法:

POST /{索引名称}/_update/{docId}
{
“doc”: {
“属性”: “值”
}
}

注意:这种更新只能使用post方式。

POST /my_index/_update/1
{
  "doc": {
    "price": 4500
  }
}
4.3.6 删除文档

语法: DELETE /{索引名称}/{类型}/{id}

DELETE /my_index/_doc/1
结果:
{
  "_index": "my_index",
  "_id": "1",
  "_version": 5,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 6,
  "_primary_term": 1
}

4.4 映射mapping

创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)

4.4.1 查看映射

语法: GET /{索引名称}/_mapping

GET /my_index/_mapping
结果:
{
  "my_index": {
    "mappings": {
      "properties": {
        "brand": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "images": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "price": {
          "type": "long"
        },
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
  }
}
4.4.2 动态映射

在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建 表字段、类型、长度、主键等,最后才能基于表插入数据。而ElasticSearch中不 需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入 ElasticSearch时,会根据文档字段自动识别类型,这种机制称之为动态映射

映射规则对应:

数据对应的类型
null字段不添加
true|flaseboolean
字符串text/keyword
数值long
小数float
日期date

特殊类型:字符串

  • text:用于长文本,对本文内容进行分词(产生倒排索引文档列表),支持多关键字全文查询。例如:电商项目中商品名称,或者博客项目文章标题,正文设置为text 缺点:不能进行聚合(分组),不支持排序。
  • keyword:用于词条精确查询,支持等值查询,不需要进行分词字符串(分词后无意义)。例如:用户昵称、用户手机号、身份证号、图片地址。场景:根据品牌名称等值查询。支持聚合(分组)、排序 不支持全文查询查询
4.4.3 静态映射(推荐)

静态映射是在ElasticSearch中也可以事先定义好映射,即手动映射,包含文档的各字段类型、分词器等,这称为静态映射

字符串进行全文查询(多关键字模糊查询):需要对字段值进行分词(中文分词器),查询分词器,指定为text

字符串进行等值查询:指定为keyword

#删除原创建的索引
DELETE /my_index

#创建索引,并同时指定映射关系和分词器等。
PUT /my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "index": true,
        "store": true,
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "brand": { 
        "type": "keyword",
        "index": true,
        "store": true
      },
      "images": {
        "type": "keyword",
        "index": false,
        "store": true
      },
      "price": {
        "type": "integer",
        "index": true,
        "store": true
      }
    }
  }
}

结果:
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "my_index"
}

type分类如下:

  • 字符串:text(支持分词)和 keyword(不支持分词)。
  • text:该类型被用来索引长文本,在创建索引前会将这些文本进行分词,转化为词的组合,建立索引;允许es来检索这些词,text类型不能用来排序和聚合。
  • keyword:该类型不能分词,可以被用来检索过滤、排序和聚合,keyword类型不可用text进行分词模糊检索。
  • 数值型:long、integer、short、byte、double、float
  • 日期型:date
  • 布尔型:boolean
4.4.4 nested 介绍

nested:类型是一种特殊的对象object数据类型(specialised version of the object datatype ),允许对象数组彼此独立地进行索引和查询。

demo: 建立一个普通的index

如果linux 中有这个my_comment_index 先删除!DELETE /my_comment_index

步骤1:建立一个索引( 存储博客文章及其所有评论)

PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}

如上所示,所以我们有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。
但是ElasticSearch搜索中的内部对象并不像我们期望的那样工作。

步骤2 : 执行查询

GET /my_comment_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "comments.name": "李四"
          }
        },
        {
          "match": {
            "comments.age": 34
          }
        }
      ]
    }
  }
}

查询结果正常的响应

原因分析:comments字段默认的数据类型是Object,故我们的文档内部存储为:

{
“title”: [ 狂人日记],
“body”: [ 《狂人日记》是一篇象征性和寓意很强的小说,当时… ],
“comments.name”: [ 张三, 李四, 王五 ],
“comments.comment”: [ 非常棒的文章,文章非常好,王五,… ],
“comments.age”: [ 33, 34, 38 ],
“comments.rating”: [ 7, 8, 9 ]
}

我们可以清楚地看到,comments.name和comments.age之间的关系已丢失。这就是为什么我们的文档匹配李四和34的查询。

步骤3:删除当前索引

DELETE /my_comment_index

步骤4:建立一个nested 类型的(comments字段映射为nested类型,而不是默认的object类型)

PUT my_comment_index
{
  "mappings": {
      "properties": {
        "comments": {
          "type": "nested" 
        }
    }
  }
}


PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}

重新执行步骤1,使用nested 查询

GET /my_comment_index/_search
{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "comments.name": "李四"
              }
            },
            {
              "match": {
                "comments.age": 34
              }
            }
          ]
        }
      }
    }
  }
}

结果发现没有返回任何的文档,这是何故?

当将字段设置为nested 嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。文档的内部表示:

{
{
“comments.name”: [ 张三],
“comments.comment”: [ 非常棒的文章 ],
“comments.age”: [ 34 ],
“comments.rating”: [ 9 ]
},
{
“comments.name”: [ 李四],
“comments.comment”: [ 文章非常好 ],
“comments.age”: [ 38 ],
“comments.rating”: [ 8 ]
},
{
“comments.name”: [ 王五],
“comments.comment”: [手动点赞],
“comments.age”: [ 33 ],
“comments.rating”: [ 7 ]
},
{
“title”: [ 狂人日记 ],
“body”: [ 《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国… ]
}
}

每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。

5 DSL高级查询

5.1 DSL概述

本质调用ES提供检索数据restful接口

Query DSL概述: Domain Specific Language(领域专用语言),ElasticSearch提供了基于JSON的DSL来定义查询。

创建索引库设置好映射:

#创建索引,并同时指定映射关系和分词器等。
PUT /my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "index": true,
        "store": true,
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "brand": { 
        "type": "keyword",
        "index": true,
        "store": true
      },
      "images": {
        "type": "keyword",
        "index": false,
        "store": true
      },
      "price": {
        "type": "integer",
        "index": true,
        "store": true
      }
    }
  }
}

准备数据:

PUT /my_index/_doc/1
{"id":1,"title":"华为笔记本电脑","brand":"华为","images":"http://www.gulixueyuan.com/xm.jpg","price":5388}

PUT /my_index/_doc/2
{"id":2,"title":"华为手机","brand":"华为","images":"http://www.gulixueyuan.com/xm.jpg","price":5500}

PUT /my_index/_doc/3
{"id":3,"title":"VIVO手机","brand":"vivo","images":"http://www.gulixueyuan.com/xm.jpg","price":3600}

5.2 DSL查询

5.2.1 查询所有文档

match_all:

POST /my_index/_search
{
  "query": {
    "match_all": {}
  }
}

结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "id" : 3,
          "title" : "VIVO手机",
          "brand" : "vivo",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 3600
        }
      }
    ]
  }
}
5.2.2 匹配查询(match)

match:

POST /my_index/_search
{
  "query": {
    "match": {
      "title": "华为智能手机"
    }
  }
}

结果:
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.5619608,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.5619608,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.35411233,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      }
    ]
  }
}

5.2.3 多字段匹配
POST /my_index/_search
{
  "query": {
    "multi_match": {
      "query": "华为智能手机",
      "fields": ["title","brand"]
    }
  }
}

结果:
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.5619608,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.5619608,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.35411233,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      }
    ]
  }
}

5.2.4 关键字精确查询

term:关键字不会进行分词。

POST /my_index/_search
{
  "query": {
   "term": {
     "title": {
       "value": "华为手机"
     }
   }
  }
}

结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
5.2.6 多关键字精确查询
POST /my_index/_search
{
  "query": {
   "terms": {
     "title": [
       "华为手机",
       "华为"
     ]
   }
  }
}

结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      }
    ]
  }
}
5.2.7 范围查询

范围查询使用range。

  • gte: 大于等于
  • lte: 小于等于
  • gt: 大于
  • lt: 小于
POST /my_index/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 3000,
        "lte": 5000
      }
    }
  }
}
结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "title" : "VIVO手机",
          "brand" : "vivo"
        }
      }
    ]
  }
}

5.2.8 指定返回字段

query同级增加_source进行过滤。

POST /my_index/_search
{
  "query": {
   "terms": {
     "title": [
       "华为手机",
       "华为"
     ]
   }
  },
  "_source": ["title","brand"]
}
5.2.9 组合查询

bool 各条件之间有and,or或not的关系

  • must: 各个条件都必须满足,所有条件是and的关系
  • should: 各个条件有一个满足即可,即各条件是or的关系
  • must_not: 不满足所有条件,即各条件是not的关系
  • filter: 与must效果等同,但是它不计算得分,效率更高点
must
POST /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 3000,
              "lte": 5400
            }
          }
        }
      ]
    }
  }
}
结果:
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.2923405,
    "hits": [
      {
        "_index": "my_index",
        "_id": "1",
        "_score": 1.2923405,
        "_source": {
          "id": 1,
          "title": "华为笔记本电脑",
          "brand": "华为",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 5388
        }
      }
    ]
  }
}
should
POST /my_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 3000,
              "lte": 5000
            }
          }
        }
      ]
    }
  }
}

结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "id" : 3,
          "title" : "VIVO手机",
          "brand" : "vivo",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 3600
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.5619608,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.35411233,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      }
    ]
  }
}

如果should和must同时存在,他们之间是and关系:

POST /my_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 3000,
              "lte": 5000
            }
          }
        }
      ],
      "must": [
        {
          "match": {
            "title": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 3000,
              "lte": 5000
            }
          }
        }
      ]
    }
  }
}

结果:
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

must_not
POST /my_index/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "title": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 3000,
              "lte": 5000
            }
          }
        }
      ]
    }
  }
}
结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}
filter

_score的分值为0

POST /my_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "title": "华为"
          }
        }
      ]
    }
  }
}

结果:
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.0,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.0,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      }
    ]
  }
}
5.2.10 聚合查询

聚合允许使用者对es文档进行统计分析,类似与关系型数据库中的group by,当然还有很多其他的聚合,例如取最大值、平均值等等。

聚合三要素:聚合名称(给不同聚合业务其名称-用于解析结果)、聚合字段(对哪个字段进行分组)、聚合类型(如何聚合-常见:字段值相同放在一组)

max
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "max_price": {
      "max": {
        "field": "price"
      }
    }
  }
}

结果:
{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "max_price" : {
      "value" : 5500.0
    }
  }
}

min
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "min_price": {
      "min": {
        "field": "price"
      }
    }
  }
}

结果:
{
  "took" : 12,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "max_price" : {
      "value" : 3600.0
    }
  }
}
avg
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "avg_price": {
      "avg": {
        "field": "price"
      }
    }
  }
}
结果:
{
  "took" : 12,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "avg_price" : {
      "value" : 4829.333333333333
    }
  }
}
sum
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "sum_price": {
      "sum": {
        "field": "price"
      }
    }
  }
}
结果:
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "sum_price" : {
      "value" : 14488.0
    }
  }
}
stats
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "stats_price": {
      "stats": {
        "field": "price"
      }
    }
  }
}
结果:
{
  "took" : 20,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "stats_price" : {
      "count" : 3,
      "min" : 3600.0,
      "max" : 5500.0,
      "avg" : 4829.333333333333,
      "sum" : 14488.0
    }
  }
}
terms

桶聚合相当于sql中的group by语句

POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "groupby_brand": {
      "terms": {
        "field": "brand",
        "size": 10
      }
    }
  }
}
结果:
{
  "took" : 16,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "groupby_brand" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "华为",
          "doc_count" : 2
        },
        {
          "key" : "vivo",
          "doc_count" : 1
        }
      ]
    }
  }
}

还可以对桶继续下钻:

POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "size": 0, 
  "aggs": {
    "groupby_brand": {
      "terms": {
        "field": "brand",
        "size": 10
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}
结果:
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "groupby_brand" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "华为",
          "doc_count" : 2,
          "avg_price" : {
            "value" : 5444.0
          }
        },
        {
          "key" : "vivo",
          "doc_count" : 1,
          "avg_price" : {
            "value" : 3600.0
          }
        }
      ]
    }
  }
}
5.2.11 排序
POST /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "华为"
          }
        }
      ]
    }
  },
  "sort": [
    {
      "price": {
        "order": "asc"
      }
    },
    {
      "_score": {
        "order": "desc"
      }
    }
  ]
}
结果:
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.35411233,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        },
        "sort" : [
          5388,
          0.35411233
        ]
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.5619608,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        },
        "sort" : [
          5500,
          0.5619608
        ]
      }
    ]
  }
}

5.2.12 分页查询

分页的两个关键属性:from、size。

  • from: 当前页的起始索引,默认从0开始。 from = (pageNum - 1) * size
  • size: 每页显示多少条
POST /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 2
}
结果:
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "id" : 1,
          "title" : "华为笔记本电脑",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5388
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "id" : 2,
          "title" : "华为手机",
          "brand" : "华为",
          "images" : "http://www.gulixueyuan.com/xm.jpg",
          "price" : 5500
        }
      }
    ]
  }
}
5.2.13 高亮

高亮三要素:

  • 高亮字段
  • 高亮前置标签(HTML标签)
  • 高亮后置标签(HTML标签)

根据关键词查询商品

#高亮 必须要求用户录入关键字
GET my_index/_search
{
  "query": {
    "match": {
      "title": "华为 手机"
    }
  },
  "highlight": {
    "fields": {"title": {}},
    "pre_tags": "<font style='color:red'>",
    "post_tags": "</font>"
  }
}

结果:

{
  "took": 96,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": 1.2155836,
    "hits": [
      {
        "_index": "my_index",
        "_id": "2",
        "_score": 1.2155836,
        "_source": {
          "id": 2,
          "title": "华为手机",
          "brand": "华为",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 5500
        },
        "highlight": {
          "title": [
            "<font style='color:red'>华为</font><font style='color:red'>手机</font>"
          ]
        }
      },
      {
        "_index": "my_index",
        "_id": "1",
        "_score": 0.49191093,
        "_source": {
          "id": 1,
          "title": "华为笔记本电脑",
          "brand": "华为",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 5388
        },
        "highlight": {
          "title": [
            "<font style='color:red'>华为</font>笔记本电脑"
          ]
        }
      },
      {
        "_index": "my_index",
        "_id": "3",
        "_score": 0.41299206,
        "_source": {
          "id": 3,
          "title": "VIVO手机",
          "brand": "vivo",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 3600
        },
        "highlight": {
          "title": [
            "VIVO<font style='color:red'>手机</font>"
          ]
        }
      },
      {
        "_index": "my_index",
        "_id": "4",
        "_score": 0.41299206,
        "_source": {
          "id": 3,
          "title": "OPPO手机",
          "brand": "oppo",
          "images": "http://www.gulixueyuan.com/xm.jpg",
          "price": 5500
        },
        "highlight": {
          "title": [
            "OPPO<font style='color:red'>手机</font>"
          ]
        }
      }
    ]
  }
}

6 Java Api操作ES

6.1 ElasticSearch Java API Client

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/8.5/_getting_started.html

6.1.1 搭建项目

1、创建项目:elasticSearch_demo

2、导入pom.xml:

<?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>

    <groupId>com.atguigu</groupId>
    <artifactId>ElasticSearch_demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>co.elastic.clients</groupId>
            <artifactId>elasticsearch-java</artifactId>
            <version>8.5.3</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.12.3</version>
        </dependency>

        <dependency>
            <groupId>jakarta.json</groupId>
            <artifactId>jakarta.json-api</artifactId>
            <version>2.0.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
        </dependency>
    </dependencies>

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

</project>

6.1.2 配置连接

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.atguigu.demo.Goods;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;


public class DocumentCurdTest {


    ElasticsearchClient client = null;

    @Before
    public void initElasticsearchClient() {
        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
        credsProv.setCredentials(
                AuthScope.ANY, new UsernamePasswordCredentials("elastic", "111111")
        );
        RestClient restClient = RestClient
                .builder(HttpHost.create("http://192.168.200.6:9200"))
                .setHttpClientConfigCallback(hc -> hc
                        .setDefaultCredentialsProvider(credsProv)
                )
                .build();

        // Create the transport with a Jackson mapper
        RestClientTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        client = new ElasticsearchClient(transport);
    }
}

6.1.3 实体类

package com.atguigu.elasticSearch_demo.model;

import lombok.Data;


@Data
public class Goods {
    private String id;
    private String title;
    private String images;
    private String brand;
    private Integer price;
}

6.1.3 测试查询

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.atguigu.demo.Goods;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;


public class DocumentCurdTest {


    ElasticsearchClient client = null;

    @Before
    public void initElasticsearchClient() {
        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
        credsProv.setCredentials(
                AuthScope.ANY, new UsernamePasswordCredentials("elastic", "111111")
        );
        RestClient restClient = RestClient
                .builder(HttpHost.create("http://192.168.200.6:9200"))
                .setHttpClientConfigCallback(hc -> hc
                        .setDefaultCredentialsProvider(credsProv)
                )
                .build();

        // Create the transport with a Jackson mapper
        RestClientTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        client = new ElasticsearchClient(transport);
    }

    private static final String INDEX_NAME = "my_index";

    /**
     * 文档新增
     * Lambda表达式写法 : 通过XxxRqeustBuilder对象.build方法 产生XxxReqeust对象简化过程
     */
    @Test
    public void saveDoc() throws IOException {
        //1.准备保存文档对象Goods
        Goods goods = new Goods();
        goods.setTitle("华为meta50手机");
        goods.setBrand("华为");
        goods.setId(3);
        goods.setPrice(19999);
        //2.调用客户端对象新增文档 - 采用lambda表达式写法
        IndexResponse response = client.index(i -> i
                .index(INDEX_NAME) //指定索引库名称
                .id(goods.getId().toString()) //文档主键 _id
                .document(goods)  //文档对象
        );
        System.out.println(response);

        //2.调用客户端对象新增文档 - 传统写法
        IndexRequest.Builder<Goods> goodsBuilder = new IndexRequest.Builder<>();
        goodsBuilder.index(INDEX_NAME);
        goodsBuilder.id(goods.getId().toString());
        goodsBuilder.document(goods);
        //
        IndexResponse response1 = client.index(goodsBuilder.build());
        //System.out.println(response1);
    }


    @Test
    public void testGetDoc() throws IOException {
        //GetRequest.Builder builder = new GetRequest.Builder();
        //builder.index(INDEX_NAME);
        //builder.id("1");
        //GetResponse<Goods> response = client.get(builder.build(), Goods.class);
        //Goods source = response.source();
        //System.out.println(source);

        GetResponse<Goods> goodsGetResponse = client.get(g -> g.index(INDEX_NAME).id("1"), Goods.class);
        System.out.println(goodsGetResponse.source());
    }


    /**
     * 删除文档
     * @throws IOException
     */
    @Test
    public void testDeleteDoc() throws IOException {
        //DeleteRequest.Builder builder = new DeleteRequest.Builder();
        //builder.index(INDEX_NAME);
        //builder.id("1");
        //DeleteResponse response = client.delete(builder.build());
        //System.out.println(response);


        System.out.println(client.delete(d -> d.index(INDEX_NAME).id("2")));
    }
}

6.2 Spring Data ElasticSearch

官方文档:https://spring.io/projects/spring-data-elasticsearch

Spring Data是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷。 Spring Data可以极大的简化JPA(ElasticSearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些常用的功能。

Spring Data ElasticSearch 基于 spring data API 简化 ElasticSearch操作,将原始操作ElasticSearch的客户端API 进行封装 。Spring Data为ElasticSearch项目提供集成搜索引擎。Spring Data ElasticSearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储索引库数据访问层。

6.2.1 搭建项目

1、创建项目:elasticSearch_demo_springdata_es

2、导入pom.xml

<?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>

    <groupId>com.atguigu</groupId>
    <artifactId>elasticSearch_demo_springdata_es</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


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

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

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

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

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

3、启动类、配置文件

package com.atguigu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author: atguigu
 * @create: 2023-12-12 15:07
 */
@SpringBootApplication
public class SpringDataESDemoApp {

    public static void main(String[] args) {
        SpringApplication.run(SpringDataESDemoApp.class, args);
    }
}

application.yml

spring:
  elasticsearch:
    uris: http://192.168.200.6:9200
    username: elastic
    password: 111111

6.2.2 document映射

package com.atguigu.model;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Document;

/**
 * @author: atguigu
 * @create: 2024-08-07 14:21
 */
@Data
@Document(indexName = "my_index")
public class Goods {
    private Integer id;
    private String title;
    private String images;
    private String brand;
    private int price;
}

映射

Spring Data通过注解来声明字段的映射属性,有下面的三个注解:

@Document 作用在类,标记实体类为文档对象, indexName:对应索引库名称

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

@Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:

type:字段类型,取值是枚举:FieldType
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称:ik_max_word

package com.atguigu.repository;

import com.atguigu.model.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

/**
 * SpringData会自动扫描继承自*Repository接口产生代理对象
 */
public interface GoodsRepository extends ElasticsearchRepository<Goods, Integer> {
}

启动项目,自动新增索引库

6.2.3 测试查询

引入spring-boot-starter-data-ElasticSearch后,添加配置文件,springboot会自动配置es连接

package com.atguigu;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import com.atguigu.model.Goods;
import com.atguigu.repository.GoodsRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;

import java.io.IOException;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SpringDataESDemoAppTest {

    @Autowired
    private GoodsRepository goodsRepository;


    @Test
    public void testRepository(){
        Goods goods = new Goods();
        goods.setId(6);
        goods.setBrand("小米");
        goods.setTitle("小米16最新上市-全新旗舰手机");
        goods.setPrice(7777);
        //goodsRepository.save(goods);
        //goodsRepository.save(goods);

        //Optional<Goods> optional = goodsRepository.findById(6);
        //if(optional.isPresent()){
        //    Goods goods1 = optional.get();
        //    System.out.println(goods1);
        //}

        //for (Goods goods1 : goodsRepository.findAll()) {
        //    System.out.println(goods1);
        //}

        goodsRepository.deleteById(6);

    }
}

6.2.4 模板对象测试

@SpringBootTest
class SpringDataESDemoAppTest {

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;


    @Test
    public void testTempalte(){
        //Goods goods = elasticsearchTemplate.get("3", Goods.class);
        //System.out.println(goods);

        Goods goods = new Goods();
        goods.setId(6);
        goods.setBrand("小米");
        goods.setTitle("小米16最新上市");
        goods.setPrice(66666);

        //elasticsearchTemplate.save(goods);

        //elasticsearchTemplate.update(goods);

        elasticsearchTemplate.delete("6", Goods.class);
    }
}

总结:简单查询与创建索引及映射推荐使用Spring Data ElasticSearch Api;复杂查询使用ElasticSearch Java API Client,结合使用,方便开发

ES插件

腾讯云es服务提供的插件列表
在这里插入图片描述
Elasticsearch 提供了丰富的插件系统,允许用户扩展其功能。以下是 Elasticsearch 8.x 中一些常用的插件以及它们的基本用法:

1. Ingest Node 插件

  • 描述:用于在数据被索引之前进行处理。
  • 示例插件
    • ingest-user-agent:解析 User-Agent 字符串。
    • 使用方法
      # 安装插件
      bin/elasticsearch-plugin install ingest-user-agent
      
      # 使用示例
      PUT _ingest/pipeline/user_agent
      {
        "description": "Add user agent information",
        "processors": [
          {
            "user_agent": {
              "field": "agent"
            }
          }
        ]
      }
      
      POST my-index/_doc?pipeline=user_agent
      {
        "agent": "Mozilla/5.0..."
      }
      

2. Analysis Plugins(分析插件)

  • 描述:提供额外的分词器、过滤器等文本分析工具。
  • 示例插件
    • analysis-icu:提供了基于 ICU 库的 Unicode 支持。
    • 使用方法
      # 安装插件
      bin/elasticsearch-plugin install analysis-icu
      
      # 在索引设置中使用
      PUT my_index
      {
        "settings": {
          "analysis": {
            "analyzer": {
              "my_icu_analyzer": {
                "type": "icu_analyzer"
              }
            }
          }
        }
      }
      

3. Repository Plugins(仓库插件)

  • 描述:支持将快照存储到不同的存储系统中。
  • 示例插件
    • repository-s3:允许将快照存储到 Amazon S3 中。
    • 使用方法
      # 安装插件
      bin/elasticsearch-plugin install repository-s3
      
      # 配置并创建仓库
      PUT _snapshot/my_s3_repository
      {
        "type": "s3",
        "settings": {
          "bucket": "my_bucket",
          "region": "us-west"
        }
      }
      

4. Discovery Plugins(发现插件)

  • 描述:改变集群节点发现机制。
  • 示例插件
    • discovery-ec2:适用于 AWS 环境下的自动发现。
    • 使用方法
      # 安装插件
      bin/elasticsearch-plugin install discovery-ec2
      
      # 配置文件中添加必要的配置项
      discovery.ec2.tag.my_tag: my_value
      

5. Alerting and Monitoring Plugins(监控和告警插件)

  • 描述:提供对集群健康状态的监控及异常情况的通知功能。
  • 示例插件
    • X-Pack(现已被集成至默认发行版中,但需要单独启用相关功能)。
    • 使用方法
      启用后可通过 Kibana 进行管理,或者直接通过 REST API 设置监控规则。

注意事项

  • 在安装任何插件之前,请确保它与你的 Elasticsearch 版本兼容。
  • 某些插件可能需要重启 Elasticsearch 实例才能生效。
  • 对于生产环境,在安装新插件前最好先在测试环境中验证其兼容性和性能影响。

ES部分场景八股回答重点

如何在es中设计实现多层次缓存

  • 访问模式
  • es自带缓存
  • 缓存中间件(Memcached)
  • 数据分层
  • 索引优化

2es如何实现分布式事务

  • 消息队列(kafka)
  • 双写架构(2pc)
  • 分布式事务协调(saga)
  • 扩展ecs

3.Finite State Transducer是什么?有什么用

  • 有限状态机
  • 倒排索引、自动补全、拼写纠正、性能优化、索引存储

es中的Fielddata是什么?如何优化其性能?

  • 硬盘数据加载内存
  • doc_values
  • 预加载
  • 控制大小
  • 语句优化

es集群滚动升级如何实现?

  • 数据备份
  • 禁用shard分配
  • 升级单个节点

1. ES实现机器学习模型推理

Elasticsearch 通过以下方式支持机器学习模型推理:

  • ML 插件:Elasticsearch 提供了内置的机器学习插件(如 ml 模块),支持异常检测、预测等任务。例如,使用 anomaly detection 功能对时间序列数据进行实时分析。
  • 外部模型集成:通过 Inference PipelineSearch Script 调用外部机器学习模型(如 TensorFlow、PyTorch)。例如:
    POST _ml/inference/my_model/_predict
    {
      "input": {
        "features": [1.2, 3.4, 5.6]
      }
    }
    
  • Elasticsearch SQL:结合 SQL 查询与机器学习模型,实现复杂的数据分析。

2. 倒排表的 FOR 和 RBM 压缩算法

  • FOR (Frame of Reference)
    • 适用场景:数值型倒排列表(如整数 ID)。
    • 原理:将倒排列表中的文档 ID 转换为相对于某个基准值的增量值,利用 Golomb 编码压缩。例如,文档 ID [1001, 1002, 1005] 会被编码为 [0, 1, 3](基准值为 1001)。
    • 优点:高效压缩连续或近似连续的数值数据。
  • RBM (Roaring Bitmaps)
    • 适用场景:高基数集合的位图压缩。
    • 原理:将文档 ID 分割为 16 位块,每个块用不同的数据结构(如数组、位图、运行长度编码)存储。例如,块 0x1234 中的文档 ID 用位图存储。
    • 优点:快速支持集合操作(交集、并集),适合聚合查询。

3. 确保数据一致性前提下更新 ES 倒排索引

  • 版本控制:每次更新文档时,Elasticsearch 会检查版本号(_version),确保写入的原子性。
  • 主分片与副本分片同步
    1. 写请求先发送到主分片。
    2. 主分片成功写入后,同步到副本分片。
    3. 成功后返回客户端。
  • 冲突处理:若多个写请求同时修改同一文档,Elasticsearch 会拒绝并发写入,返回 VersionConflictEngineException

4. ES 中的倒排列表

  • 结构:倒排索引由 Term Dictionary(术语词典)和 Postings List(倒排列表)组成。
    • Term Dictionary:存储所有唯一术语及其对应的倒排列表指针。
    • Postings List:每个术语对应一个倒排列表,包含文档 ID、词频、位置信息等。
  • 示例
    {
      "term": "elasticsearch",
      "postings": [
        {"doc_id": 1, "tf": 3, "positions": [0, 5, 10]},
        {"doc_id": 2, "tf": 2, "positions": [2, 8]}
      ]
    }
    

5. 如何利用 ES 实现大数据聚合查询

  • 聚合类型
    • Terms 聚合:按字段值分组统计(如统计用户性别分布)。
    • Histogram 聚合:按数值范围分桶(如统计订单金额分布)。
    • Cardinality 聚合:统计唯一值数量(如统计独立访客数)。
  • 优化技巧
    • 使用 filter 替代 match 减少计算开销。
    • 对高频字段使用 keyword 类型(避免分词)。
    • 控制聚合桶数量(size 参数)。

6. ES 集群架构调优

  • 分片策略
    • 主分片数:根据数据量预估,通常设置为节点数的 3-5 倍。
    • 副本数:生产环境建议至少 1 个副本,确保高可用。
  • 节点角色
    • 数据节点:存储数据,执行搜索和聚合。
    • 主节点:管理集群状态,避免资源竞争。
    • 协调节点:处理客户端请求,减轻数据节点压力。
  • JVM 调优:限制堆内存不超过物理内存的 50%(推荐 31GB 以下)。

7. 如何优化 ES GC

  • GC 策略
    • G1 GC:默认使用 G1 垃圾收集器,适用于大堆内存。
    • 调整参数
      -XX:MaxGCPauseMillis=200  # 控制最大停顿时间
      -XX:G1HeapRegionSize=4M   # 调整区域大小
      
  • 监控工具:使用 jstat 或 Elasticsearch 内置监控 API 分析 GC 日志。
  • 减少 Full GC:避免频繁创建临时对象,合理设置缓存(如 request_cache)。

8. 为什么 ES 内存 32G 以上性能几乎无提升

  • JVM 堆内存限制
    • Elasticsearch 堆内存超过 31GB 时,会触发 Compressed Oops 的失效,导致性能下降。
    • 大堆内存增加 GC 停顿时间(G1 GC 在 31GB 以上时,Full GC 时间显著增长)。
  • 操作系统限制:Linux 系统对大内存管理效率较低,可能导致内存碎片化。

9. 如何优化 ES 写入性能

  • 批量写入
    • 使用 Bulk API 批量提交文档,减少网络往返。
    • 控制批量大小(通常 5MB-15MB)。
  • 调整刷新策略
    PUT /my_index/_settings
    {
      "index": {
        "refresh_interval": "30s"  # 减少刷新频率
      }
    }
    
  • 禁用副本:写入时临时禁用副本,写入完成后再启用。

10. ES 集群脑裂问题

  • 原因:网络分区导致部分节点无法通信,形成多个独立的“脑”。
  • 解决方法
    • 设置 discovery.zen.minimum_master_nodes(number_of_masters / 2) + 1
    • 使用 Zen2Elasticsearch 7.0+cluster.initial_master_nodes 配置。
  • 预防:部署奇数个主节点,避免偶数节点导致的投票平局。

11. ES 底层如何执行文档的更新和删除

  • 更新操作
    1. 客户端发送更新请求到协调节点。
    2. 协调节点查询主分片获取当前文档版本。
    3. 将新文档写入主分片,生成新版本。
    4. 同步到副本分片。
  • 删除操作
    1. 标记文档为 _deleted,不立即物理删除。
    2. 定期通过 Merge 操作(段合并)清除已删除文档。

12. 如何优化文档评分

  • 相关性算法
    • BM25:默认评分算法,平衡词频和文档长度。
    • TF-IDF:适用于简单匹配场景。
  • 自定义评分
    • 使用 function_score 结合权重、脚本评分:
      {
        "function_score": {
          "query": { "match": { "text": "elasticsearch" }},
          "functions": [
            { "weight": 2, "filter": { "term": { "category": "books" }}}
          ]
        }
      }
      

13. 如何处理评分偏差

  • 数据预处理
    • 对长文本使用 shingle 分词,避免关键词漏匹配。
    • 对稀有词赋予更高权重。
  • 评分模型校准
    • 使用 script_score 调整评分公式:
      {
        "script_score": {
          "query": { "match_all": {} },
          "script": {
            "source": "Math.log(1 + doc['sales'].value)"
          }
        }
      }
      

14. ES 聚合查询、组合查询

  • 组合查询
    • Bool Query:结合 mustshouldmust_not 实现复杂逻辑。
    • Multi Match:跨多个字段匹配。
  • 聚合查询
    • 嵌套聚合:支持多级分组(如按地区->城市统计销售额)。
    • Pipeline 聚合:对聚合结果进行二次计算(如求平均值)。

15. ES 深分页问题

  • 问题from/size 分页在深度分页时性能急剧下降(需加载所有前 N 条数据)。
  • 解决方案
    • Scroll API:适用于离线导出,不支持实时性:
      POST /_search?scroll=2m
      { "size": 100, "query": { "match_all": {} } }
      
    • Search After:基于排序值的分页,适合实时查询:
      POST /_search
      {
        "size": 100,
        "query": { "match_all": {} },
        "search_after": ["123456"]
      }
      

16. ES 如何实现滚动更新

  • 步骤
    1. 更新第一个节点:停止数据写入,重启节点加载新配置。
    2. 等待节点重新加入集群并恢复分片。
    3. 重复上述步骤更新其他节点。
  • 注意事项
    • 确保 cluster.blocks.read_only 未启用。
    • 使用 cluster:monitor/task/get 监控分片恢复状态。
    • 避免在滚动更新期间执行大规模写入操作。

1. ES 聚合优化

优化策略
  • 字段类型优化
    • 数值类型:确保聚合字段使用 longdouble 等数值类型,而非 text
    • 关键词聚合:使用 keyword 类型(如 text.keyword),避免全文索引。
  • Doc Values
    • 启用 doc_values(默认启用),用于高效聚合。例如:
      "fields": {
        "price": { "type": "double", "doc_values": true }
      }
      
  • 分片设计
    • 合理设置分片数,避免过多或过少。通常分片数 = 节点数 × 3~5。
    • 使用 search_after 替代 from/size 深分页,减少性能损耗。
  • 预聚合
    • 对高频查询的字段使用 scriptfielddata 预处理,减少实时计算。
示例:聚合性能提升
GET /sales/_search
{
  "size": 0,
  "aggs": {
    "avg_price": {
      "avg": { "field": "price" }
    },
    "top_regions": {
      "terms": {
        "field": "region.keyword",
        "size": 10
      },
      "aggs": {
        "region_avg": { "avg": { "field": "price" } }
      }
    }
  }
}

2. ES 中如何去重

方法一:cardinality 聚合
  • 原理:基于 HyperLogLog 算法,速度快但结果是近似值(误差约 0.5%)。
  • 示例
    GET /your_index/_search
    {
      "size": 0,
      "aggs": {
        "unique_user_count": {
          "cardinality": {
            "field": "user_id.keyword",
            "precision_threshold": 10000
          }
        }
      }
    }
    
方法二:terms 聚合 + size
  • 原理:通过获取所有唯一值后统计数量,结果精确但性能较差。
  • 示例
    GET /your_index/_search
    {
      "size": 0,
      "aggs": {
        "unique_users": {
          "terms": {
            "field": "user_id.keyword",
            "size": 10000
          },
          "aggs": {
            "total_unique": {
              "value_count": { "field": "user_id.keyword" }
            }
          }
        }
      }
    }
    
注意事项
  • 字段类型:确保目标字段是 keyword 类型(非 text)。
  • 分组去重:若需按分组字段(如日期)统计每组的唯一值,可嵌套聚合:
    GET /your_index/_search
    {
      "size": 0,
      "aggs": {
        "group_by_date": {
          "date_histogram": {
            "field": "@timestamp",
            "calendar_interval": "day"
          },
          "aggs": {
            "daily_unique_users": {
              "cardinality": {
                "field": "user_id.keyword",
                "precision_threshold": 1000
              }
            }
          }
        }
      }
    }
    

3. ES 实现日志关联查询

方法一:使用 join 字段(父子文档)
  • 场景:日志与上下文(如请求ID)关联。
  • 映射
    {
      "mappings": {
        "properties": {
          "log_type": { "type": "keyword" },
          "request_id": { "type": "keyword" },
          "parent": { "type": "join", "relations": { "request": "log" } }
        }
      }
    }
    
  • 查询
    POST /logs/_search
    {
      "query": {
        "has_child": {
          "type": "log",
          "query": { "match": { "log_type": "error" } }
        }
      }
    }
    
方法二:使用 script 关联字段
  • 场景:跨索引关联(如日志与用户行为)。
  • 示例
    POST /logs/_search
    {
      "query": {
        "script": {
          "source": "doc['user_id'].value == 'user123'"
        }
      }
    }
    

4. ES 中的 ANN(近似最近邻搜索)

实现方式:KNN 插件(ES 8.x)
  • 安装插件
    bin/elasticsearch-plugin install https://artifacts.elastic.co/downloads/elasticsearch-plugins/knn/knn-8.12.0.zip
    
  • 映射定义
    {
      "mappings": {
        "properties": {
          "vector": { "type": "dense_vector", "dims": 128 }
        }
      }
    }
    
  • 查询示例
    POST /my_index/_search
    {
      "size": 5,
      "query": {
        "knn": {
          "vector": [0.1, 0.2, ..., 0.128],
          "k": 5
        }
      }
    }
    

5. ES 创建只读索引

方法一:通过 API 设置
PUT /my_index/_settings
{
  "index.blocks.read_only": true
}
方法二:通过索引生命周期管理(ILM)
  • 策略示例
    PUT _ilm/policy/readonly_policy
    {
      "policy": {
        "phases": {
          "cold": {
            "min_age": "30d",
            "actions": {
              "set_read_only": {}
            }
          }
        }
      }
    }
    

6. ES 不同节点类型的区别

节点类型角色与功能
主节点管理集群元数据(索引创建、分片分配),不处理数据读写。
数据节点存储数据,执行搜索、聚合等操作,消耗 CPU/内存/磁盘资源。
协调节点路由请求、聚合结果,减轻数据节点压力,通常用于高并发查询场景。
冷热架构节点热节点:处理高频写入/查询;冷节点:存储低频访问的冷数据,优化存储成本。

7. ES 中如何管理索引

核心方法
  1. 索引生命周期管理(ILM)

    • 自动管理索引从热到冷再到删除的流程。
    • 示例策略:
      PUT _ilm/policy/logs_policy
      {
        "policy": {
          "phases": {
            "hot": { "min_age": "0s", "actions": { "rollover": { "max_size": "50gb" } } },
            "warm": { "min_age": "7d", "actions": { "set_priority": 50 } },
            "delete": { "min_age": "30d", "actions": { "delete": {} } }
          }
        }
      }
      
  2. 索引模板

    • 自动应用映射、设置到新索引。
    • 示例:
      PUT _index_template/logs_template
      {
        "index_patterns": ["logs-*"],
        "template": {
          "settings": { "number_of_shards": 3, "number_of_replicas": 1 },
          "mappings": { "dynamic": "strict" }
        }
      }
      
  3. 快照备份

    • 使用 repository 配置存储路径,定期备份索引。
    • 示例:
      PUT _snapshot/my_backup/snapshot_1?wait_for_completion=true
      {
        "indices": "my_index",
        "ignore_unavailable": true
      }
      

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

~Yogi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值