admin管理员组

文章数量:1567031

文章目录

  • ElasticSearch
    • 一、ElasticSearch简介
      • 1.1、ES的由来
      • 1.2、ES的优点
    • 二、ElasticSearch功能
      • 2.1、搜索
      • 2.2、全文检索
      • 2.3、倒排索引
        • 2.3.1、概念
        • 2.3.2、举例
          • 2.3.2.1、原始数据分词
          • 2.3.2.2、建立索引
          • 2.3.2.3、搜索
    • 三、ElasticSearch 部署
      • 3.1、ElasticSearch 安装
      • 3.2、安装 Kibana
      • 3.3、安装 IK 分词器
        • 3.3.1、IK分词器简介
        • 3.3.2、IK分词器的安装
    • 四、ElasticSearch 核心概念
    • 五、elasticsearch基本操作
      • 5.1、index(数据库)
      • 5.2、document(行)/(ES的CURD操作)
        • 5.2.1、 创建文档
        • 5.2.2、 查看文档
        • 5.2.3、修改文档
        • 5.2.4、修改局部属性
        • 5.2.5、删除文档
        • 5.2.6、批量操作
      • 5.3、mapping(映射)/表结构
        • 5.3.1、查看映射
        • 5.3.2、创建 mapping
    • 六、DSL高级查询
      • 6.1、DSL概述
      • 6.2、查询全部
      • 6.3、匹配查询
      • 6.4、多字段匹配 [业务]
      • 6.5、前缀匹配
      • 6.6、关键字不分词
      • 6.7、多个不分词的关键字
      • 6.8、范围查询 [业务]
      • 6.9、bool查询 [业务]
    • 七、进阶查询
      • 7.1、聚合
        • 7.1.1、概念
        • 7.1.2、测试数据
        • 7.1.3、Hello World
        • 7.1.4、并列的聚合
        • 7.1.5、嵌套的聚合
        • 7.1.6、其它聚合方式
      • 7.2、指定查询结果字段
      • 7.3、高亮效果
      • 7.4、分页
        • 7.4.1、常规分页
        • 7.4.2、滚动分页
      • 7.5、排序
      • 7.6、模糊查询
    • 八、Java api 操作 ElasticSearch
      • 8.1 High Level client
        • 8.1.1 搭建环境
          • 8.1.1.1 Maven 配置 POM
            • 8.1.1.1.1 父工程
            • 8.1.1.1.2 依赖
            • 8.1.1.1.3 构建插件可选
          • 8.1.1.2 SpringBoot
            • 8.1.1.2.1 配置文件
            • 8.1.1.2.2 主启动类
            • 8.1.1.2.3 配置类
            • 8.1.1.2.4 测试类
        • 8.1.2 索引相关操作
          • 8.1.2.1 创建索引
          • 8.1.2.2 查看索引
          • 8.1.2.3 删除索引
        • 8.1.3 文档相关操作
          • 8.1.3.1 创建文档
          • 8.1.3.2 更新文档
          • 8.1.3.3 根据 id 查询文档
          • 8.1.3.4 批量操作
          • 8.1.3.4 删除文档
        • 8.1.4 DSL查询
          • 8.1.4.1 关键词匹配
          • 8.1.4.2 高亮查询
          • 8.1.4.3 聚合查询
      • 8.2 SpringData ElasticSearch
        • 8.2.1 搭建环境
          • 8.2.1.1 Maven 配置 POM
            • 8.2.1.1.1 父工程
            • 8.2.1.1.2 依赖
          • 8.2.1.2 SpringBoot
            • 8.2.1.2.1 配置文件
            • 8.2.1.2.2 配置类
          • 8.2.1.3 主启动类
        • 8.2.2 数据建模
          • 8.2.2.1 实体类
          • 8.2.2.2 Dao
          • 8.2.2.3 测试类
        • 8.2.3 数据操作
          • 8.2.3.1 保存文档
          • 8.2.3.2 根据 id 查询文档
          • 8.2.3.3 查询全部并排序
        • 8.2.4 自定义方法
          • 8.2.4.1 概述
          • 8.2.4.2 声明自定义方法
          • 8.2.4.3 测试自定义方法
    • 九、ElasticSearch 集群
      • 9.1、需求
      • 9.2、概述
        • 9.2.1、空集群
        • 9.2.2、拥有一个索引的单节点集群
        • 9.2.3、拥有两个节点的集群
        • 9.2.4、拥有三个节点的集群
        • 9.2.5、将参数 number_of_replicas 调大到 2
        • 9.2.6、关闭了一个节点后的集群
      • 9.3、核心概念
        • 9.3.1、集群 Cluster
        • 9.3.2、节点 Node
        • 9.3.3、分片 Shards
        • 9.3.4、副本 Replicas
        • 9.3.5、分配 Allocation
        • 9.3.6、节点类型
          • 9.3.6.1、master
          • 9.3.6.2、DataNode
        • 9.3.7、总结
      • 9.4、搭建
        • 9.4.1、创建目录
        • 9.4.2、复制`ES`目录
        • 9.4.3、修改配置文件
          • 9.4.3.1、实例 9301
          • 9.4.3.2、实例 9302
          • 9.4.3.3、实例 9303
        • 9.4.4、修改 Kibana 配置
        • 9.4.5、启动测试
        • 9.4.6、查看集群信息的图形化界面工具
        • 9.4.7、数据测试
      • 9.5、内部机制
        • 9.5.1、写流程
        • 9.5.2、读流程

ElasticSearch

一、ElasticSearch简介

ElasticSearch:智能搜索,分布式的搜索引擎

Elaticsearch,简称为 ES, 是一个开源的高扩展的分布式全文检索引擎,特点:

  • 近乎实时的存储、检索数据;

  • 扩展性好,可以扩展到上百台服务器,处理PB级别的数据;

  • 使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能;

  • 通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

    Lucene就是一个jar包,里面包含了各种建立倒排索引的方法,java开发的时候只需要导入这个jar包就可以开发了。

    Lucene不是分布式的。

    ES的底层就是Lucene,ES是分布式的。(Elasticsearch 在 Lucene 的基础上进行封装,实现了分布式搜索引擎)

1.1、ES的由来

因为Lucene有两个难以解决的问题,

  1. 数据越大,存不下来,那我就需要多台服务器存数据,那么我的Lucene不支持分布式的,那就需要安装多个Lucene然后通过代码来合并搜索结果。这样很不好

  2. 数据要考虑安全性,一台服务器挂了,那么上面的数据不就消失了。

ES就是分布式的集群,每一个节点其实就是Lucene,当用户搜索的时候,会随机挑一台,然后这台机器自己知道数据在哪,不用我们管这些底层、

1.2、ES的优点

  • 分布式的功能
  • 数据高可用,集群高可用
  • API更简单
  • API更高级。
  • 支持的语言很多
  • 支持PB级别的数据
  • 完成搜索的功能和分析功能

二、ElasticSearch功能

2.1、搜索

  1. 百度,谷歌。我们可以通过他们去搜索我们需要的东西。但是我们的搜索不只是包含这些。
  2. 互联网的搜索:电商网站。招聘网站。新闻网站。各种APP(百度外卖,美团等等)
  3. windows系统的搜索,OA软件,淘宝SSM网站,前后台的搜索功能

总结:搜索无处不在。通过一些关键字,给我们查询出来跟这些关键字相关的信息

2.2、全文检索

全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

全文检索的方法主要分为按字检索和按词检索两种。按字检索是指对于文章中的每一个字都建立索引,检索时将词分解为字的组合。对于各种不同的语言而言,字有不同的含义,比如英文中字与词实际上是合一的,而中文中字与词有很大分别。按词检索指对文章中的词,即语义单位建立索引,检索时按词检索,并且可以处理同义项等。英文等西方文字由于按照空白切分词,因此实现上与按字处理类似,添加同义处理也很容易。中文等东方文字则需要切分字词,以达到按词索引的目的,关于这方面的问题,是当前全文检索技术尤其是中文全文检索技术中的难点,在此不做详述。

2.3、倒排索引

2.3.1、概念

全文搜索引擎目前主流的索引技术就是倒排索引的方式。

  • 传统查询:根据 id 查询数据
  • 倒排索引:根据关键词匹配分词结果,然后找到 id
2.3.2、举例
2.3.2.1、原始数据分词
文档 ID文档内容
1谷歌地图之父跳槽Facebook
2谷歌地图之父加盟Facebook
3谷歌地图创始人拉斯离开谷歌加盟Facebook
4谷歌地图之父跳槽Facebook,与Wave项目取消有关
5谷歌地图之父拉斯加盟社交网站Facebook
文档 ID文档内容
1[谷歌] [地图] [之父] [跳槽] [Facebook]
2[谷歌] [地图] [之父] [加盟] [Facebook]
3[谷歌] [地图] [创始] [创始人] [拉斯] [离开] [加盟] [Facebook]
4[谷歌] [地图] [之父] [跳槽] [Facebook] [与] [Wave] [项目] [取消] [有关]
5[谷歌] [地图] [之父] [拉斯] [加盟] [社交] [网站] [Facebook]
2.3.2.2、建立索引
单词 ID单词关联文档的 ID
1谷歌1 2 3 4 5
2地图1 2 3 4 5
3之父1 2 4 5
4跳槽1 4
5Facebook1 2 3 4 5
6加盟2 3 5
7创始3
8创始人3
9拉斯3 5
10离开3
114
12Wave4
13项目4
14取消4
15有关4
16社交5
17网站5
2.3.2.3、搜索

用户搜索关键词:拉斯

到索引记录中查询,得知“拉斯”这个词曾出现在id为3和id为5的文档中。

然后就可以根据3和5查询文档本身的数据。

另外,全文索引还会根据匹配程度进行打分,匹配程度高的记录得分高,得分高的记录在搜索结果中靠前。

比如搜索“拉斯离开谷歌”那么3号记录得分就会高。


随笔:综上所诉,ElasticSearch主要是用于将搜索内容进行分词,然后对分词后的结果查询。


三、ElasticSearch 部署

3.1、ElasticSearch 安装

下载地址

解压elasticsearch-7.8.0-windows-x86_64.zip,目录结构:

目录说明
bin可执行脚本目录
config配置目录
jdk内置jdk目录
lib类库
logs日志目录
modules模块目录
plugins插件目录

解压完成后进入bin目录,双击运行elasticsearch.bat

测试访问: http://localhost:9200/

  1. 注意事项一:

    • ElasticSearch是使用java开发的,且本版本的ES需要JDK版本要是1.8以上,所以安装ElasticSearch之前保证JDK1.8+安装完毕,并正确的配置好JDK环境变量,否则启动ElasticSearch失败。
  2. 注意事项二

    • 出现闪退,通过路径访问发现“空间不足”
  3. 修改config/jvm.options文件的22行23行,把2改成1,让Elasticsearch启动的时候占用1个G的内存。

    • -Xmx512m:设置JVM最大可用内存为512M。

    • -Xms512m:设置JVM初始内存为512m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

3.2、安装 Kibana

ElasticSearch 对外都是基于 RESTFul 风格提供 HTTP 服务,所以可以使用 Postman 来操作 ES。 另外,还可以使用 Kibana 这样的图形化界面客户端使操作更简单。下载地址: 下载后解压。

进入到config目录,修改kibana.yml文件(解开注释):

server.port: 5601

elasticsearch.hosts: [“http://localhost:9200”]

i18n.locale: “zh-CN”

运行kibana-7.8.0-windows-x86_64\bin\kibana.bat文件。再通过浏览器访问:

开始访问:http://127.0.0.1:5601

3.3、安装 IK 分词器

3.3.1、IK分词器简介

IKAnalyzer是一个开源的,基于Java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Lucene为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。

IK分词器3.0的特性如下:

  1. 采用了特有的“正向迭代最细粒度切分算法“,具有60万字/秒的高速处理能力。

  2. 采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。

  3. 对中英联合支持不是很好,在这方面的处理比较麻烦.需再做一次查询,同时是支持个人词条的优化的词典存储,更小的内存占用。

  4. 支持用户词典扩展定义。

  5. 针对Lucene全文检索优化的查询分析器IKQueryParser;采用歧义分析算法优化查询关键字的搜索排列组合,能极大的提高Lucene检索的命中率。

3.3.2、IK分词器的安装
  1. 下载:

GitHub仓库地址

下载地址

  1. 解压安装IK插件

直接解压到plugins下,注意目录结构,解压后的zip不要放在plugins目录下

  1. 重新启动ElasticSearch

  2. 测试

在kibana中测试:

测试分词器:

l ik_max_word:会将文本做最细粒度的拆分

ik_smart:会做最粗粒度的拆分,智能拆分

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

四、ElasticSearch 核心概念

Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:

{
 "name" :     "John",
 "sex" :      "Male",
 "age" :      25,
 "birthDate": "1990/05/01",
 "about" :    "I love to go rock climbing",
 "interests": [ "sports", "music" ]
}

用Mysql这样的数据库存储就会容易想到建立一张User表,有各个字段等,在ElasticSearch里这就是一个文档,当然这个文档会属于一个User的类型,各种各样的类型存在于一个索引当中。这里有一份简易的将Elasticsearch和关系型数据术语对照表:

ElasticSearchMySQL
index(索引库)Database(数据库)
mapping(映射)表结构
type(类型)Table(表)
document(文档)Row(行)
field(字段)Column(列)

ElasticSearch较高的版本已经废除了type这个概念。

ElasticSearch 中字符串有两种类型:

  • text:分词,不能用来排序和聚合
  • keyword:不分词,可以被用来检索过滤、排序和聚合

五、elasticsearch基本操作

5.1、index(数据库)

  1. PUT /aura_index 增加一个aura_index的index库
  2. GET _cat/indices 命令查询ES中所有的index索引库
  3. GET /my_index 命令查看my_index索引
  4. DELETE /aura_index 删除一个aura_index的index库

5.2、document(行)/(ES的CURD操作)

数据对应的类型
null不添加字段
true 或 falseboolean
字符串text
数值long
小数float
日期date
类型说明
PUT类似于SQL中的增
DELETE类似于SQL中的删
POST类似于SQL中的改
GET类似于SQL中的查
5.2.1、 创建文档
PUT /索引名称/_doc/文档id
{
 json 格式数据
}

注意:我们插入数据的时候,如果我们的语句中指明了index和type,如果ES里面不存在,默认帮我们自动创建

操作举例:

PUT /my_index/_doc/1
{
"title": "小米手机",
"category": "小米",
"images": "http://www.gulixueyuan/xm.jpg",
"price": 3999
}
5.2.2、 查看文档

语法:GET /{索引名称}/{类型}/{id} / 语法:GET /{数据库}/{表}/{id}

GET  /my_index/_doc/1
5.2.3、修改文档

使用POST来修改数据,其实使用PUT也可以实现修改数据

POST的方式进行修改数据,POST是局部更新数据,别的数据不动。PUT是全局更新

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

注意:下面这种操作也是成功的,会丢数据,是全局的修改

PUT /my_index/_doc/1
{
"category": "小米"
}
5.2.4、修改局部属性

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

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

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

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

操作举例:DELETE /my_index/_doc/1

5.2.6、批量操作

批量操作通常分成三部分:

  • 请求方式部分:POST _bulk
  • 元数据部分:
    • 指定 bulk 动作
    • 指定执行批量操作的 index、type、文档 id 等信息
  • 数据部分:执行批量操作用到的数据
POST _bulk
{ action: { metadata }}
{ request body        }
{ action: { metadata }}
{ request body        }

总的语法规则:

  • 每个 JSON 串都不能换行,只能放在同一行。
  • 各个 JSON 串之间通过换行分隔,而不是逗号
  • 多个操作不会因为某个操作失败而终止,返回结果会显示失败的详细原因

批量操作类型:

  • create:批量创建文档。如果文档存在则返回错误
  • index:如果文档不存在就创建,如果文档存在就更新
  • update:更新一个文档,如果文档不存在就返回错误
  • delete:删除一个文档,如果要删除的文档id不存在,就返回错误

批量保存

POST _bulk
{"create": {"_index":"my_index", "_id":1}}
{"emp_id":5,"emp_name":"name01","emp_age":20}
{"create": {"_index":"my_index", "_id":2}}
{"emp_id":6,"emp_name":"name02","emp_age":21}
{"create": {"_index":"my_index", "_id":3}}
{"emp_id":7,"emp_name":"name03","emp_age":22}

批量删除

POST _bulk
{"delete": {"_index":"my_index", "_id":2}}
{"delete": {"_index":"my_index", "_id":3}}

5.3、mapping(映射)/表结构

有了索引库,等于有了数据库中的database。

接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)

5.3.1、查看映射

语法格式:GET /索引名称/_mapping,

结果示例:

{
"my_index" : {
 "mappings" : {
   "properties" : {
     "category" : {
       "type" : "text",
       "fields" : {
         "keyword" : {
           "type" : "keyword",
           "ignore_above" : 256
         }
       }
     },
     "id" : {
       "type" : "long"
     },
     "images" : {
       "type" : "text",
       "fields" : {
         "keyword" : {
           "type" : "keyword",
           "ignore_above" : 256
         }
       }
     },
     "price" : {
       "type" : "long"
     },
     "title" : {
       "type" : "text",
       "fields" : {
         "keyword" : {
           "type" : "keyword",
           "ignore_above" : 256
         }
       }
     }
   }
 }
}
}
5.3.2、创建 mapping
数据对应的类型
null字段不添加
trueflase
字符串text
数值long
小数float
日期date

注意:我们通常不会单独创建 mapping,mapping 也无法脱离 index 单独存在。 所以创建 mapping 也都是在创建 index 的时候同时进行

语法格式:PUT /索引名称

{
 "mappings":{
     "properties":{
         "字段名":{
             "type":"当前字段的数据类型",
             "index":布尔值,表示是否为当前字段建立索引,
             "store":布尔值,表示当前字段是否存储,
             "analyzer":"当前字段使用的分词器名称",
             "search_analyzer":"搜索关键词使用的分词器"
         },
         "字段名":{
             ……
         }
     }
 }
}

操作举例:

PUT /index_song
{
"mappings": {
 "properties": {
   "song_id":{
     "type": "long",
     "index": true,
     "store": true
   },
   "song_name":{
     "type": "text",
     "index": true,
     "store": true,
     "analyzer": "ik_max_word",
     "search_analyzer": "ik_smart"
   },
   "singer":{
     "type": "keyword",
     "index": true,
     "store": true
   },
   "song_album":{
     "type": "text",
     "index": true,
     "store": true,
     "analyzer": "ik_max_word"
   },
   "song_lyric":{
     "type": "text",
     "index": true,
     "store": false,
     "analyzer": "ik_max_word"
   },
   "song_img":{
     "type": "keyword",
     "index": false,
     "store": true
   }
 }
}
}

字段属性说明

  1. 是否分词

    • 需要分词的情况:分词之后,分词结果是有意义的、会被搜索的。例如:歌曲名称
    • 不分词的情况:分词之后,分词结果没有意义,不会被搜索。例如:歌曲图片地址、地名、人名
  2. 是否索引

    • 需要索引的情况:要搜索这个字段,就基于它建立索引。例如:歌曲名称
    • 不需要索引的情况:不会被搜索的字段,不必建立索引。例如:歌曲图片地址
  3. 是否存储

    是否存储是指:该字段是否在 _source 之外再做一个额外的存储。这样对于做了存储的字段可以通过 stored_fields 方式获取。 这一点我们作为初学者不必深究,我们只需要知道:即使我们设置了 store 为 false,查询结果中 _source 这里也仍然能够显示该字段。 所以通常是没有影响的。

六、DSL高级查询

6.1、DSL概述

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

测试数据:

POST db_song/_doc
{
"song_name":"give me an apple",
"song_singer":"tom",
"song_album":"apple pie",
"song_lyrics":"i want to give you an apple",
"song_price":10
}

POST db_song/_doc
{
"song_name":"happy tears",
"song_singer":"jerry",
"song_album":"apple farmer",
"song_lyrics":"i will go to London",
"song_price":20
}

POST db_song/_doc
{
"song_name":"tears of happy",
"song_singer":"jerry",
"song_album":"apple farmer",
"song_lyrics":"i will go to NewYork",
"song_price":30
}

POST db_song/_doc
{
"song_name":"fly me",
"song_singer":"bigger one",
"song_album":"two star",
"song_lyrics":"i put an apple on your table",
"song_price":40
}

6.2、查询全部

query下match_all

# 	DSL:查询全部
# 	从DSL语法角度来说,第一行是请求方式和请求地址,第一行下面是一个JSON格式的请求体
GET /db_song/_search
{
  "query": {
    "match_all": {}
  }
}

6.3、匹配查询

query下match

#	DSL:关键词匹配一个字段查询
GET /db_song/_search
{
  "query": {
    "match": {
      "song_name": "apple"
    }
  }
}
#	补充条件删除
POST /db_song/_delete_by_query
{
  "query": {
    "match": {
     "song_name": "apple"
    }
  }
}

6.4、多字段匹配 [业务]

query下multi_match

# 	DSL:关键词匹配多个字段
# 	被命中的document的字段中,和关键词匹配的越多,在搜索结果中得分越高,说明匹配度越高
GET /db_song/_search
{
  "query": {
    "multi_match": {
      "query": "apple",
      "fields": [
        "song_name",
        "song_album",
        "song_singer",
        "song_lyrics"
      ]
    }
  }
}

6.5、前缀匹配

query下prefix

# 	DSL查询:前缀匹配
# 	分词结果以指定字符串开头的能匹配上
GET /db_song/_search
{
  "query": {
    "prefix": {
      "song_name": {
        "value": "app"
      }
    }
  }
}

6.6、关键字不分词

query下term

# 	DSL查询:关键词不分词
# 	原始数据:happy tears
# 	原始数据分词结果:[happy][tears]
# 	匹配方式:用happy tears和分词结果匹配,发现它和happy、tears都匹配不上
GET /db_song/_search
{
  "query": {
    "term": {
      "song_name": {
        "value": "happy tears"
      }
    }
  }
}

6.7、多个不分词的关键字

query下terms

# 	DSL查询:多个不分词的关键词
GET /db_song/_search
{
  "query": {
    "terms": {
      "song_name": [
        "happy",
        "tears"
      ]
    }
  }
}

6.8、范围查询 [业务]

query下range

# 	DSL查询:范围查询
# 	gte: 大于等于,greater than equal
# 	lte: 小于等于,less than equal
# 	gt: 大于,greater than
# 	lt: 小于,less than
GET /db_song/_search
{
  "query": {
    "range": {
      "song_price": {
        "gte": 20,
        "lte": 30
      }
    }
  }
}

6.9、bool查询 [业务]

bool 是 boolean 的缩写。表示布尔值的意思。
多个查询的组合,组合方式包括:

  • must: 各个条件都必须满足,所有条件是 and 的关系
  • should: 各个条件有一个满足即可,即各条件是 or 的关系
  • must_not: 各个条件必须不满足,相当于各条件分别取反之后再 and
  • filter: 与 must 效果等同,但是它不计算得分,效率更高点。同时也表示在已有查询结果的基础上再进行过滤
# 	DSL查询:bool查询
# 	bool查询中的must
# 	must的值是一个数组,数组中每一个对象是一个查询条件,各个条件之间是“且”的关系
GET /db_song/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "song_name": "apple"
          }
        },
        {
          "match": {
            "song_lyrics": "want"
          }
        }
      ]
    }
  }
}
# 	should的值是一个数组,数组中每一个对象是一个查询条件,各个条件之间是“或”的关系
GET /db_song/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "song_name": "apple"
          }
        },
        {
          "match": {
            "song_lyrics": "London"
          }
        }
      ]
    }
  }
}
# 	must_not:各个条件取反,取反之后是“且”的关系
GET /db_song/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "song_name": "apple"
          }
        },
        {
          "match": {
            "song_lyrics": "London"
          }
        }
      ]
    }
  }
}
# 	DSL查询:filter相当于不计算得分的must
GET /db_song/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "song_name":"apple"
          }
        },
        {
          "match": {
            "song_lyrics":"want"
          }
        }
      ]
    }
  }
}
# 	从业务的角度也可以理解为:在前面查询结果的基础上,再进一步过滤
# 	DSL查询:组合各种语法
GET /db_song/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "song_name": "apple"
          }
        }
      ],
      "filter": [
        {
          "range": {
            "song_price": {
              "gte": 10,
              "lte": 50
            }
          }
        }
      ]
    }
  }
}

七、进阶查询

7.1、聚合

7.1.1、概念

本质上就是分组,相当于 SQL 中的 group by。

7.1.2、测试数据

注意:只有不分词的字段才能聚合,text类型的字段无法参与聚合!

PUT db_hr
{
  "mappings": {
    "properties": {
      "department":{
        "type": "keyword"
      },
      "subject":{
        "type": "keyword"
      }
    }
  }
}

# _bulk 表示批量导入数据
POST db_hr/_bulk
{ "index": {}}
{ "emp_name" : "nicole adler", "emp_age" : 20, "emp_salary":1000.00, "department":"hr","subject":"hr worker","birthday":"1995-10-12"}
{ "index": {}}
{ "emp_name" : "jason alex", "emp_age" : 21, "emp_salary":1100.00, "department":"hr","subject":"hr worker","birthday":"1992-03-07"}
{ "index": {}}
{ "emp_name" : "tom green", "emp_age" : 22, "emp_salary":1200.00, "department":"hr","subject":"hr manager","birthday":"1985-11-17"}
{ "index": {}}
{ "emp_name" : "jeff hare", "emp_age" : 23, "emp_salary":1300.00, "department":"hr","subject":"hr manager","birthday":"1999-10-14"}
{ "index": {}}
{ "emp_name" : "robert hill", "emp_age" : 24, "emp_salary":1400.00, "department":"hr","subject":"hr manager","birthday":"1997-10-11"}
{ "index": {}}
{ "emp_name" : "belinda arge", "emp_age" : 30, "emp_salary":1500.00, "department":"rd","subject":"java","birthday":"1992-05-08"}
{ "index": {}}
{ "emp_name" : "amy", "emp_age" : 31, "emp_salary":2100.00, "department":"rd","subject":"java","birthday":"2014-10-13"}
{ "index": {}}
{ "emp_name" : "hoffman", "emp_age" : 32, "emp_salary":5500.00, "department":"rd","subject":"java","birthday":"1998-02-01"}
{ "index": {}}
{ "emp_name" : "joyce dabney", "emp_age" : 25, "emp_salary":2800.00, "department":"rd","subject":"php","birthday":"1990-01-01"}
{ "index": {}}
{ "emp_name" : "rae de mesa", "emp_age" : 36, "emp_salary":7500.00, "department":"rd","subject":"php","birthday":"1983-07-06"}
{ "index": {}}
{ "emp_name" : "lisa", "emp_age" : 45, "emp_salary":15100.00, "department":"fi","subject":"fi worker","birthday":"1996-06-06"}
{ "index": {}}
{ "emp_name" : "kim deziel", "emp_age" : 55, "emp_salary":25100.00, "department":"fi","subject":"fi worker","birthday":"1991-09-11"}
{ "index": {}}
{ "emp_name" : "tina-louise foster", "emp_age" : 65, "emp_salary":35100.00, "department":"fi","subject":"fi manager","birthday":"1997-07-07"}
{ "index": {}}
{ "emp_name" : "brennan foti", "emp_age" : 75, "emp_salary":45100.00, "department":"fi","subject":"fi manager","birthday":"1998-08-15"}
7.1.3、Hello World

DSL 语句:

GET db_hr/_search
{
  "aggs": {
    "agg_hello_world": {
      "terms": {
        "field": "department"
      }
    }
  }
}

节选查询结果中的聚合部分:

"aggregations" : {
    "agg_hello_world" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
            {
              "key" : "hr",
              "doc_count" : 5
            },
            {
              "key" : "rd",
              "doc_count" : 5
            },
            {
              "key" : "fi",
              "doc_count" : 4
            }
        ]
    }
}

结果说明:

  • buckets:聚合执行分组后得到的各个组
  • key:聚合字段在当前组内的值
  • doc_count:在当前组内包含document的数量
7.1.4、并列的聚合
# 并列的聚合:各聚各的,互不影响
GET /db_hr/_search
{
  "aggs": {
    "agg_dept": {
      "terms": {
        "field": "department"
      }
    },
    "agg_subject": {
      "terms": {
        "field": "subject"
      }
    }
  }
}
7.1.5、嵌套的聚合
# 嵌套的聚合:组内再分组
GET /db_hr/_search
{
  "aggs": {
    "agg_dept": {
      "terms": {
        "field": "department"
      },
      "aggs": {
        "agg_subject": {
          "terms": {
            "field": "subject"
          }
        }
      }
    }
  }
}
7.1.6、其它聚合方式
  • max:组内取最大值
  • min:组内取最小值
  • avg:组内取平均值
  • sum:组内计算总数
  • stats:组内汇总各项统计结果,包含上面的每一项
# 计算每个部门平均工资
GET /db_hr/_search
{
  "aggs": {
    "agg_dept": {
      "terms": {
        "field": "department"
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "emp_salary"
          }
        }
      }
    }
  }
}
# 按照各种方式统计
GET /db_hr/_search
{
  "aggs": {
    "agg_dept": {
      "terms": {
        "field": "department"
      },
      "aggs": {
        "agg_stats": {
          "stats": {
            "field": "emp_salary"
          }
        }
      }
    }
  }
}

7.2、指定查询结果字段

# 指定查询结果中显示哪些字段
GET /db_hr/_search
{
  "query": {
    "match": {
      "emp_name": "adler"
    }
  }, 
  "_source": [
    "emp_name",
    "emp_age",
    "emp_salary"
  ]
}

7.3、高亮效果

# 高亮效果
# 默认效果:围绕关键词加em标签
GET /db_hr/_search
{
  "query": {
    "match": {
      "emp_name": "adler"
    }
  },
  "highlight": {
    "fields": {
      "emp_name": {}
    }
  }
}

# 指定高亮效果
GET /db_hr/_search
{
  "query": {
    "match": {
      "emp_name": "adler"
    }
  },
  "highlight": {
    "fields": {
      "emp_name": {
        "pre_tags": "<span style='color:red;'>",
        "post_tags": "</span>"
      }
    }
  }
}

7.4、分页

7.4.1、常规分页

使用 from、size 指定分页参数。
计算 from 值的公式:(pageNo-1)*pageSize
适用场景:页面显示分页数据。因为from、size其实是先把所有查询结果取出再根据from、size截取,所以不适合大数据量。

# 分页:from、size 分页
GET /db_hr/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5
}
7.4.2、滚动分页

m表示分钟,s表示秒,执行滚动分页后,scroll_id会被缓存指定的时间。
适用场景:大数据量分批取出。

GET db_hr/_search?scroll=5m
{
  "size": 1
}
GET /_search/scroll
{
  "scroll":"5m", "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFGMzVEpnNFlCaWd0WVJkT1BoZFloAAAAAAAAIuAWVHpwNVBiaTBUMXFzdlBqQWd5N1lHUQ=="
}

7.5、排序

# 排序
# FIELD 位置:指定排序字段
# order 后面:排序方向
# 多字段排序:先根据第一个字段排序,在第一个字段值重复的范围内再根据第二个字段排序
GET /db_hr/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "emp_salary": {
        "order": "desc"
      }
    },
    {
      "birthday": {
        "order": "asc"
      }
    }
  ]
}

7.6、模糊查询

GET /db_hr/_search
{
  "query": {
    "fuzzy": {
      "emp_name": {
        "value": "ader",
        "fuzziness": "auto:2,5"
      }
    }
  }
}

fuzziness格式的说明:

本次查询允许的最大编辑距离,默认不开启模糊查询,相当于fuzziness=0。

支持的格式:

可以是数字(0、1、2)代表固定的最大编辑距离

自动模式,AUTO:[low],[high]的格式,含义为:

  • 查询词长度在[0-low)范围内编辑距离为0(即强匹配)
  • [low, high)范围内允许编辑一次
  • >high允许编辑2次
  • 也可以只写AUTO代表默认的自动模式,相当于AUTO:3,6

八、Java api 操作 ElasticSearch

8.1 High Level client

highlevelclient 是 高级客户端 需要通过它去操作 Elasticsearch , 它底层也是要依赖 rest-client 低级客户端

  • 大致流程

8.1.1 搭建环境
8.1.1.1 Maven 配置 POM
8.1.1.1.1 父工程
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.6.RELEASE</version>
    <relativePath/>
</parent>
8.1.1.1.2 依赖
<dependencies>
    <!--elasticsearch的高级别rest客户端-->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.8.0</version>
    </dependency>
    <!--elasticsearch的rest客户端-->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>7.8.0</version>
    </dependency>
    <!--elasticsearch的核心jar包-->
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>7.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--json转换的jar包-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.9</version>
    </dependency>
    <!-- https://mvnrepository/artifact/com.alibaba/fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.76</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </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>
</dependencies>
8.1.1.1.3 构建插件可选
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
8.1.1.2 SpringBoot
8.1.1.2.1 配置文件

application.properties

elasticsearch.host=localhost
elasticsearch.port=9200
8.1.1.2.2 主启动类

不需要加额外注解,就是一个普通的主启动类

8.1.1.2.3 配置类
package com.song.es.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
    private String host;
    private Integer port;
    public String getHost() {
        return host;
    }
    public void setHost(String host) {
        this.host = host;
    }
    public Integer getPort() {
        return port;
    }
    public void setPort(Integer port) {
        this.port = port;
    }
    @Bean
    public RestHighLevelClient restHighLevelClient(){
        RestHighLevelClient restHighLevelClient =
                new RestHighLevelClient(RestClient.builder(new HttpHost(host,port,"http")));
        return restHighLevelClient;
    }
}
8.1.1.2.4 测试类
package com.song.es.test;

import org.junit.jupiter.api.Test;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class ElasticSearchTest {

    @Autowired
    private RestHighLevelClient restHighLevelClient;
}
8.1.2 索引相关操作
8.1.2.1 创建索引

CreateIndexRequest导包时使用:org.elasticsearch.client.indices.CreateIndexRequest

@Test
public void createIndex() throws IOException {
    CreateIndexRequest createIndexRequest = new CreateIndexRequest(INDEX_NAME);
//	CreateIndexRequest createIndexRequest = new CreateIndexRequest(INDEX_NAME);该方法是创建一个ES中的索引,在mysql中则是创建一个数据库,INDEX_NAME则是名称可设置在外面例如private final String INDEX_NAME = "test_es";注意不可以有大写
    
    String mappingJSON = "{\n" +
            "    \"properties\": {\n" +
            "      \"name\": {\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true,\n" +
            "        \"store\": true\n" +
            "      },\n" +
            "      \"age\": {\n" +
            "        \"type\": \"integer\",\n" +
            "        \"index\": true,\n" +
            "        \"store\": true\n" +
            "      },\n" +
            "      \"remark\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"index\": true,\n" +
            "        \"store\": true,\n" +
            "        \"analyzer\": \"ik_max_word\",\n" +
            "        \"search_analyzer\": \"ik_smart\"\n" +
            "      }\n" +
            "    }\n" +
            "  }";
//	现在插件网址里将创建的语句先写好,再转为字符串并在idea中添加转义字符
    
    createIndexRequest.mapping(mappingJSON, XContentType.JSON);
//	ES中的映射,在这的意思即将内容映射上去
    
    CreateIndexResponse createIndexResponse =
            restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);

    boolean createIndexResult = createIndexResponse.isAcknowledged();
    System.out.println("createIndexResult = " + createIndexResult);
}
8.1.2.2 查看索引

GetIndexRequest导包时使用:

import org.elasticsearch.client.indices.GetIndexRequest;

@Test
public void getIndexTest() throws IOException {
    GetIndexRequest getIndexRequest = new GetIndexRequest(INDEX_NAME);

    GetIndexResponse getIndexResponse = restHighLevelClient.indices().get(getIndexRequest, RequestOptions.DEFAULT);

    Map<String, MappingMetadata> mappings = getIndexResponse.getMappings();
    Set<String> mappingsKeySet = mappings.keySet();
    for (String key : mappingsKeySet) {
        MappingMetadata mappingMetadata = mappings.get(key);
        System.out.println("key = " + key);
        System.out.println("mappingMetadata = " + mappingMetadata.sourceAsMap());
    }

    Map<String, Settings> settings = getIndexResponse.getSettings();
    Set<String> settingsKeySet = settings.keySet();
    for (String key : settingsKeySet) {
        Settings setting = settings.get(key);
        System.out.println("key = " + key);
        System.out.println("setting = " + setting);
    }

}
8.1.2.3 删除索引
@Test
public void testDelIndex() throws IOException {
    DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(INDEX_NAME);

    AcknowledgedResponse response =
            restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);

    boolean deleteResult = response.isAcknowledged();

    System.out.println("deleteResult = " + deleteResult);
}
8.1.3 文档相关操作
8.1.3.1 创建文档
@Test
public void testSaveDoc() throws IOException {
    // 获取到索引(数据库)
    IndexRequest indexRequest = new IndexRequest(INDEX_NAME);
    
    //设置索引id为1
    indexRequest.id("1");

    //	传入实体类,即传入要生成的行
    User user = new User();
    user.setAge(20);
    user.setName("tom");
    user.setRemark("Good boy");
	
    //	将传入的行,转换为JSON格式发送
    indexRequest.source(JSON.toJSONString(user), XContentType.JSON);
	
    //	f封装为响应体
    IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

    DocWriteResponse.Result result = indexResponse.getResult();
    System.out.println("result = " + result);
}
8.1.3.2 更新文档
@Test
public void testUpdateDoc() throws IOException {
    UpdateRequest updateRequest = new UpdateRequest(INDEX_NAME, "1");

    User user = new User();
    user.setRemark("bab boy");

    updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);

    UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);

    DocWriteResponse.Result result = updateResponse.getResult();
    System.out.println("result = " + result);
}
8.1.3.3 根据 id 查询文档
@Test
public void testGetDoc() throws IOException {
    GetRequest getRequest = new GetRequest(INDEX_NAME);

    getRequest.id("1");

    GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);

    String sourceAsString = getResponse.getSourceAsString();
    System.out.println("sourceAsString = " + sourceAsString);
}
8.1.3.4 批量操作
@Test
public void testBulk() throws IOException {
    BulkRequest bulkRequest = new BulkRequest(INDEX_NAME);

    for (int i = 0; i < 10; i++) {
        User user = new User();
        user.setAge(i * 10);
        user.setName("name " + i);
        user.setRemark("remark " + i);

        bulkRequest.add(
                new IndexRequest(INDEX_NAME)
                        .id("10" + i)
                        .source(JSON.toJSONString(user), XContentType.JSON));
    }

    BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

    BulkItemResponse[] items = bulkResponse.getItems();
    for (BulkItemResponse item : items) {
        System.out.println("item = " + item.isFailed());
    }
}
8.1.3.4 删除文档
@Test
public void testDelDoc() throws IOException {
    DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME);

    deleteRequest.id("105");

    DeleteResponse deleteResponse =
            restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);

    DocWriteResponse.Result result = deleteResponse.getResult();
    System.out.println("result = " + result);
}
8.1.4 DSL查询
8.1.4.1 关键词匹配

对应DSL语句中的match查询:

GET /db_song/_search
{
  "query": {
    "match": {
      "song_name": "apple"
    }
  }
}
@Test
public void testDSLQuery() throws IOException {
	// SearchRequest对象:代表发送给 ES 的整个请求
    SearchRequest searchRequest = new SearchRequest(INDEX_NAME_SONG);

	// SearchSourceBuilder对象:代表 DSL 语句
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

	// MatchQueryBuilder对象:代表 DSL 语句中query里面的match
    MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("song_name", "apple");

	// SearchSourceBuilder对象的query()方法:代表 DSL 语句中的query
    searchSourceBuilder.query(matchQuery);

	// 把SearchSourceBuilder对象构造的 DSL 语句填充到请求对象中
    searchRequest.source(searchSourceBuilder);

	// 发送请求,执行查询,接收响应
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

	// 对照 ES 返回的 JSON 数据格式,解析响应对象
    SearchHit[] hits = searchResponse.getHits().getHits();
    for (SearchHit hit : hits) {
        String searchResult = hit.getSourceAsString();
        System.out.println("searchResult = " + searchResult);
    }
}
8.1.4.2 高亮查询
    @Test
    public void testDSLQuery() throws IOException {
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME_SONG);

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("song_name", "apple");

        searchSourceBuilder.query(matchQuery);

        HighlightBuilder highlightBuilder = new HighlightBuilder();

        highlightBuilder.preTags("<span style='color:red;'>");
        highlightBuilder.postTags("</span>");
        highlightBuilder.field("song_name");

        searchSourceBuilder.highlighter(highlightBuilder);

        searchRequest.source(searchSourceBuilder);

        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        SearchHit[] hits = searchResponse.getHits().getHits();
        for (SearchHit hit : hits) {
            String searchResult = hit.getSourceAsString();
            System.out.println("searchResult = " + searchResult);

            Map<String, HighlightField> highMap = hit.getHighlightFields();

            Set<Map.Entry<String, HighlightField>> entries = highMap.entrySet();

            for (Map.Entry<String, HighlightField> entry : entries) {
                String key = entry.getKey();
                HighlightField value = entry.getValue();

                System.out.println("key = " + key);
                System.out.println("value = " + value.getFragments()[0].toString());
            }
        }
    }
8.1.4.3 聚合查询
@Test
public void testAgg() throws IOException {
    SearchRequest searchRequest = new SearchRequest(INDEX_NAME_HR);

    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

    searchSourceBuilder.aggregation(AggregationBuilders.terms("dept_agg").field("department"));

    searchRequest.source(searchSourceBuilder);

    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    Map<String, Aggregation> aggregationMap = searchResponse.getAggregations().asMap();

    Set<String> keySet = aggregationMap.keySet();

    for (String key : keySet) {
        System.out.println("key = " + key);
        ParsedStringTerms parsedStringTerms = (ParsedStringTerms) aggregationMap.get(key);

        List<? extends Terms.Bucket> buckets = parsedStringTerms.getBuckets();

        for (Terms.Bucket bucket : buckets) {
            Object bucketKey = bucket.getKey();
            System.out.println("bucketKey = " + bucketKey);

            long docCount = bucket.getDocCount();
            System.out.println("docCount = " + docCount);
        }
    }
}

8.2 SpringData ElasticSearch

8.2.1 搭建环境
8.2.1.1 Maven 配置 POM
8.2.1.1.1 父工程
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.6.RELEASE</version>
    <relativePath/>
</parent>
8.2.1.1.2 依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
</dependencies>
8.2.1.2 SpringBoot
8.2.1.2.1 配置文件

application.properties

# ES 服务地址
elasticsearch.host=127.0.0.1
# ES 服务端口
elasticsearch.port=9200
# 配置日志级别,开启debug日志
logging.level.atguigu=debug
8.2.1.2.2 配置类

ElasticsearchRestTemplate是spring-data-elasticsearch项目中的一个类,和其他spring项目中的template类似。

在新版的spring-data-elasticsearch中,ElasticsearhRestTemplate代替了原来的ElasticsearchTemplate

原因是ElasticsearchTemplate基于TransportClient,TransportClient即将在8.x以后的版本中移除。所以,我们推荐使用ElasticsearchRestTemplate。

ElasticsearchRestTemplate基于RestHighLevelClient客户端的。需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现elasticsearchClient()抽象方法,创建RestHighLevelClient对象。

package com.song.es.config;

import lombok.Data;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;

@Data
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {

    private String host;
    private Integer port;

    @Override
    public RestHighLevelClient elasticsearchClient() {

        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(builder);
        return restHighLevelClient;

    }
}
8.2.1.3 主启动类

常规无特殊

8.2.2 数据建模
8.2.2.1 实体类

映射 ES 中的 Document

package com.song.es.mapping;

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;

@Data
@Document(indexName = "product",shards = 1, replicas = 1)
public class Product implements Serializable {
    // @Id 注解声明这个字段是 document 的 id
    @Id
    private Long id;

    // 声明普通字段,指定字段类型、分词器
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String productName;

    @Field(type = FieldType.Integer)
    private Integer store;

    @Field(type = FieldType.Double, index = true, store = false)
    private double price;
}
8.2.2.2 Dao
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
}
8.2.2.3 测试类
package com.song.es.test;

import com.song.es.dao.ProductRepository;
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.core.ElasticsearchRestTemplate;

@SpringBootTest
public class ElasticSearchTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Test
    public void testInit() {
        // IOC 容器初始化的过程中,会根据 Product 实体类创建 index、mapping
    }

}
8.2.3 数据操作
8.2.3.1 保存文档
@Test
public void testSave() {

    Product product = new Product();
    product.setId(505L);
    product.setProductName("Hello Goods");
    product.setStore(6789);
    product.setPrice(3366.77);

    productRepository.save(product);
}
8.2.3.2 根据 id 查询文档
@Test
public void testGetById() {
    Product product = productRepository.findById(505L).get();
    System.out.println("product = " + product);
}
8.2.3.3 查询全部并排序
@Test
public void testSort() {
    Iterable<Product> iterable = productRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
    for (Product product : iterable) {
        System.out.println("product = " + product);
    }
}
8.2.4 自定义方法
8.2.4.1 概述

Spring Data 的另一个强大功能,是根据方法名称自动实现功能。比如你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。当然,方法名称要符合一定的约定。
虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。

关键字命名规则解释示例
andfindByField1AndField2根据Field1和Field2获得数据findByTitleAndPrice
orfindByField1OrField2根据Field1或Field2获得数据findByTitleOrPrice
isfindByFielde根据Field获得数据findByTitlee
notfindByFieldNot根据Field获得补集数据findByTitleNot
betweenfindByFieldBetween获得指定范围的数据findByPriceBetween
lessThanEqualfindByFieldLessThan获得小于等于指定值的数据findByPriceLessThan
8.2.4.2 声明自定义方法
package com.song.es.dao;

import com.song.es.mapping.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {

    List<Product> findByPriceBetween(double price1, double price2);

}
8.2.4.3 测试自定义方法
@Test
public void testDIY() {
    List<Product> productList =
            productRepository.findByPriceBetween(2000, 5000);
    for (Product product : productList) {
        System.out.println("product = " + product);
    }
}

九、ElasticSearch 集群

9.1、需求

单台ElasticSearch服务器往往都有最大的负载能力的限制。超过这个阈值,服务器性能就会大大降低甚至不可用。

所以生产环境中,一般都需要搭建ElasticSearch集群。

除了负载能力,单点服务器也存在其他问题:

  • 单节点存储容量有限
  • 存在单点故障隐患,无法实现高可用
  • 单服务的并发处理能力有限

配置服务器集群时,集群中节点数量没有限制,大于等于2个节点就可以看做是集群了。

一般出于高性能及高可用方面来考虑,集群中节点数量都是3个以上。

9.2、概述

9.2.1、空集群

9.2.2、拥有一个索引的单节点集群

9.2.3、拥有两个节点的集群

所有主分片和副本分片都已被分配

9.2.4、拥有三个节点的集群

为了分散负载而对分片进行重新分配

9.2.5、将参数 number_of_replicas 调大到 2

9.2.6、关闭了一个节点后的集群

9.3、核心概念

9.3.1、集群 Cluster

一个集群就是由一个或多个服务器节点组织在一起,共同持有整个数据,并一起提供索引和搜索功能。

一个ElasticSearch集群有一个共同的名称,默认是”elasticsearch”。

名字一样的节点会进入同一个集群;反过来说不同集群就是用不同名称来区分的。

9.3.2、节点 Node

集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。
根据这个名字可以确定网络中的哪些服务器对应于ElasticSearch集群中的哪些节点。

一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,
这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何Elasticsearch节点,这时启动一个节点,
会默认创建并加入一个叫做“elasticsearch”的集群。

9.3.3、分片 Shards

一个索引可以存储超出单个节点硬件限制的大量数据。
比如,一个具有10亿文档数据的索引占据1TB的磁盘空间,而任一节点都可能没有这样大的磁盘空间。
或者单个节点处理搜索请求,响应太慢。
为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,每一份就称之为分片。
当你创建一个索引的时候,你可以指定你想要的分片的数量。
每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。

分片很重要,主要有两方面的原因:

  • 允许你水平分割 / 扩展你的内容容量。
  • 允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
    至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由Elasticsearch管理的,对用户透明。

我们可以在建立索引的时候创建分片信息:

#number_of_shards:主分片数量,默认1(6.x版本默认为5)
#number_of_replicas:每个主分片对应的副本数量,默认1
PUT /users
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 2
  }
}

  • ★表示master节点

  • ●表示DataNode节点

  • 粗线框格子为主分片,细线框为副本分片,主分片与副本分片不能同时在同一个节点上。

上述例子中,创建3个主分片,每个主分片配置了2个副本分片,加起来一共9个分片。

分片序号分别为0、1、2代表不同的数据段存储。其中0号分片的主分片在node-3机器上,node-1和node-2是它的备份分片。

注意:主分片数量一旦指定后就不允许更改,否则会影响后续的数据操作(分片位置路由是取模主分片数量)。

虽然主分片数量不可用更改,但是副本数量可以修改:

粗线框格子为主分片,细线框为副本分片,主分片与副本分片不能同时在一台机器上。

9.3.4、副本 Replicas

在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,
这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。

复制分片之所以重要,有两个主要原因:

  • 在分片/节点有可能失败的情况下,提供了高可用性。为了达到这个效果,复制分片不和主分片放在同一个节点上。
  • 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。

总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。

一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。

分片和复制的数量可以在索引创建的时候指定。在索引创建之后,可以改变复制分片数量,但不能改变主分片数量。

默认情况下,Elasticsearch中的每个索引被分片1个主分片和1个复制分片,这样的话每个索引总共就有2个分片。

如果此时集群中有两个节点,那么这两个分片分别存放在两个节点上。

9.3.5、分配 Allocation

将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。

这个过程是由master节点完成的。 即:Elasticsearch的分片分配和均衡机制。

9.3.6、节点类型

ES 中的节点类型分为:Master、DataNode。

9.3.6.1、master

ElasticSearch启动时,会选举出来一个Master节点。

大致的过程是:当某个节点启动后,使用Zen Discovery机制找到集群中的其他节点,并建立连接。

并从候选节点中选举出一个主节点。

参考配置如下:

discovery.seed_hosts: [“host1”, “host2”, “host3”]
cluster.initial_master_nodes: [“node-1”, “node-2”,“node-3”]

Master节点主要负责:

  • 管理索引(创建索引、删除索引)、分配分片
  • 维护元数据
  • 管理集群节点状态
    不负责数据写入和查询,比较轻量级。一个ElasticSearch集群中,只有一个Master节点。在生产环境中,内存可以相对小一点,但要确保机器稳定。
9.3.6.2、DataNode

数据节点可以有多个,负责:数据写入、数据检索,大部分Elasticsearch的压力都在DataNode节点上,在生产环境中,内存最好配置大一些。

9.3.7、总结

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成,它们共同承担数据和负载的压力。

当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

当一个节点被选举成为主节点时,它将负责管理集群范围内除数据外的所有变更。

例如增加、删除索引,或者增加、删除节点等。作为用户,我们可以将请求发送到集群中的任何节点,包括主节点。

每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。

无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。

Elasticsearch 对这一切的管理都是透明的。

9.4、搭建

9.4.1、创建目录

elasticsearch-cluster

9.4.2、复制ES目录

把原来的 ES 解压目录复制成三份:

注意:复制之后 data 目录要删除。

9.4.3、修改配置文件

config/elasticsearch.yml

9.4.3.1、实例 9301
#集群名称
cluster.name: my-application
#默认为true。设置为false禁用磁盘分配决定器。
cluster.routing.allocation.disk.threshold_enabled: false
#节点名称
node.name: node-1
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9201
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9301
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#elasticsearch-head 跨域解决
http.cors.allow-origin: "*"
http.cors.enabled: true
9.4.3.2、实例 9302
#集群名称
cluster.name: my-application
#默认为true。设置为false禁用磁盘分配决定器。
cluster.routing.allocation.disk.threshold_enabled: false
#节点名称
node.name: node-2
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9202
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9302
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#elasticsearch-head 跨域解决
http.cors.allow-origin: "*"
http.cors.enabled: true
9.4.3.3、实例 9303
#集群名称
cluster.name: my-application
#默认为true。设置为false禁用磁盘分配决定器。
cluster.routing.allocation.disk.threshold_enabled: false
#节点名称
node.name: node-3
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9203
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9303
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#elasticsearch-head 跨域解决
http.cors.allow-origin: "*"
http.cors.enabled: true
9.4.4、修改 Kibana 配置

Kibana 不需要复制,只需要把配置文件修改一下。
配置文件路径:config/kibana.yml

elasticsearch.hosts: ["http://localhost:9200","http://localhost:9201","http://localhost:9202","http://localhost:9203"]
9.4.5、启动测试

查看集群健康度

GET /_cluster/health

查看节点信息

GET /_cat/nodes

节点健康度指标说明:

  • 绿色:所有的主分片和副本分片都已分配。你的集群是 100% 可用的。
  • 黄色:所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果 更多的 分片消失,你就会丢数据了。把 yellow 想象成一个需要及时调查的警告。
  • 红色:至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。
9.4.6、查看集群信息的图形化界面工具

把elasticsearch-head-master.zip解压到非中文无空格目录下。
在命令行下进入解压目录,运行:npm start

9.4.7、数据测试

创建索引:

PUT /shopping
{
  "settings": {
    "index": {
      "number_of_shards": "3",
      "number_of_replicas": "2"
    }
  },
  "mappings": {
      "properties": {
        "title":{
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "subtitle":{
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "images":{
          "type": "keyword",
          "index": false
        },
        "price":{
          "type": "float",
          "index": true
        }
      }
  }
}

保存文档:

POST /shopping/_doc/1
{
    "title":"小米手机",
    "images":"http://www.gulixueyuan/xm.jpg",
    "price":3999.00
}

9.5、内部机制

9.5.1、写流程

  • 第一步: 客户端选择DataNode节点发送请求,如上图架构,假设发送到node-2节点上。此时被选择的node-2节点也称为coordinating node(协调节点)
  • 第二步: 协调节点根据路由规则计算分片索引位置。并将请求发送到分片索引对应的主分片机器上(这里假设分片计算后的值为0,那么请求会命中到node-3节点上)。
    • 计算分片索引位置: shard = hash(routing) % number_of_primary_shards,routing可以自己设定,一般默认为文档的ID。
  • 第三步: 当主分片文档写入完成后,同时将数据推送到与之对应的副本分片进行写入操作
  • 第四步: 当分片完成了写入后再由协调节点将操作结果返回给客户端
9.5.2、读流程

  • 第一步:客户端选择DataNode节点发送请求,如上图架构,假设发送到node-2节点上。此时被选择的node-2节点也称为coordinating node(协调节点)
  • 第二步: 协调节点将从客户端获取到的请求数据转发到其它节点
  • 第三步: 轮询其它节点将查询结果文档ID、节点、分片信息返回给协调节点
  • 第四步: 协调节点通过文档ID、节点信息等发送get请求给其它节点进行数据获取,最后进行汇总排序将数据返回给客户端

随笔:


ElasticSearch(搜索引擎)主要是进行字段的截取查找功能,对于我来说,则主要是对于如何在idea里整合时需要携带,使用的jar包,方法…

每日金句


花开又期,花期不同。


春日不迟,相逢终有时。

本文标签: 搜索引擎Elasticsearch