Elasticsearch7.6.0

ElasticSerach 7.6.0

6.x和7.x的区别十分大

1.1 概念

概念:开源的高扩展的分布式全文搜索引擎,可以近乎实时的存储,检索数据。可扩展到上百台服务器,处理PB级别(大数据)的数据。Es是使用java开发的并使用Lucene(也是java开发的工具包)作为核心来实现所有索引和搜索的功能,通过简单的RestfulApi来隐藏Lucene的复杂性,从而简化全文搜索。ElasticSearch已超过Solr称为排名第一的搜索引擎应用。Solr也是基于Java开发的,基于Lucene的搜索引擎。

1.2 使用

维基百科、国外新闻网站、StackOverFlow、Github、电商网商、日志数据分析ELK(ElasticSearch+Logstash+Kibana)

ELK:

Logstash:用于数据收集

ElasticSearch:数据存储分析和搜索

Kibana:用于结果展示

1.3 Es和Solr对比

  1. Es开箱使用,相比Solr更简单。
  2. Solr使用Zookeeper进行分布式管理,而Es自身带有分布式协调管理功能。
  3. Solr支持更多格式数据,如Json、Xml、Csv。而Es只支持Json格式。
  4. Solr官方提供功能更多,而Es本身更注重核心功能,高级功能多有第三方插件提供,例如图形化界面由Kibana支持
  5. Solr查询速度更快,但更新索引慢,用于电商查询多的应用,Es建立索引快,查询相比Solr慢。Solr是传统搜索应用的有力解决方案,Es适用于新兴的实时搜索应用。
  6. Solr比较成熟,社区活跃。Es相对开发维护者较少,更新快,学习成本高。

1.4 安装准备工作

JDK1.8:ES要求的JDK版本最低是1.8

ElasticSearch:7.6.0版本(https://www.elastic.co/cn/downloads/elasticsearch)

Kibana:需要与ES版本保持一致,也是7.6.0版本(https://www.elastic.co/cn/downloads/kibana)

IK分词器:中文分词器,需要与ES版本保持一致,也是7.6.0版本(https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.6.0)

ElasticSearch-Head:连接ES的图形化客户端,需要Node.js环境,先安装Node.js(http://nodejs.cn/download/、https://github.com/mobz/elasticsearch-head)

1.5 安装

将上述的安装包,下载后进行解压。

image-20220617194433628

image-20220617194516099

image-20220617194457136

image-20220617194536378

ES运行(单机):elasticsearch-7.6.0\bin\elasticsearch.bat 双击直接执行 http://127.0.0.1:9200

Kibana运行:kibana-7.6.0-windows-x86_64\bin\kibana.bat http://127.0.0.1:5601

ElasticSearch-Head:先进行编译,npm install,安装完成后直接 npm run start运行即可 http://127.0.0.1:9100

IK分词器:直接将IK分词器目录直接copy到ES的plugins下,并改名为目录名为analysis-ik(否则加载不到),ES启动时就会自动加载插件

1.6 启动访问

1.6.1 启动ES、ES-Head

访问ES http://127.0.0.1:9200 出现下面结果,ES启动成功,ES对外API端口是9200,ES集群内部通讯端口为9300

image-20220617200137646

访问ES-Head http://127.0.0.1:9100,发现连接 ES http://127.0.0.1:9200 出现跨域问题

image-20220617195557423

1.6.2 Head工具跨域问题

解决ES-Head连接ES的跨域问题:

配置 elasticsearch-7.6.0\config\elasticsearch.yml 配置文件,添加跨域支持,然后重启ES,即可

# 开启跨域
http.cors.enabled: true
# 所有人访问
http.cors.allow-origin: "*"

连接成功

image-20220617200256969

1.6.3 启动 Kibana

访问 http://127.0.0.1:5601

image-20220617201738171

测试IK分词器,能运行成功并且成功分词,就代表IK分词器配置成功
    
GET _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}

GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}

image-20220617201940763

image-20220617202000124

1.7 分词器

分词器,作用是将一段字符串拆分成一个个的关键字,我们在搜索的时候,会将我们输入的关键字进行分词,会将索引库中的数据进行分词,然后进行一个匹配操作(搜索出匹配度相关的数据),ES相似度匹配的结果与所选择的分词器类别息息相关。ES默认的分词器为standard,是将每个字看做一个词,但显然不符合我们中文的场景,所以需要使用IK分词器,进行词组的划分。

分词器:

  1. Standard:单字切分法,一个字(对于英文为一个单词)切分成一个词,ES默认内置分词器。
  2. CJKAnalyzer: 二元切分法, 把相邻的两个字, 作为一个词。
  3. SmartChineseAnalyzer: 对中文支持较好, 但是扩展性差, 针对扩展词库、停用词均不好处理。
  4. Whitespace分词器:去除空格,不支持中文,对生成的词汇单元不进行其他标准化处理。
  5. language分词器:特定语言的分词器,不支持中文。
  6. IK-analyzer: 在做中文搜索时,最受欢迎的分词器,支持自定义词库。

1.8 IK分词器

1.8.1 IK分词模式

IK分词器有两种分词模式:ik_max_wordik_smart模式。

1、ik_max_word(最细粒度的拆分)

会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。

2、ik_smart(粗粒度的拆分)

会做最粗粒度的拆分,但不会重复,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。

1.8.2 自定义分词字典

image-20220618002242829

有时候,ik分词的结果,不符合我们的预期,比如:我是程序猿,预期结果:我、是、程序猿,但是ik分词器将【程序猿】三个字也拆分了,这个时候我们需要自定义扩展字典,并注入到配置文件中。

  1. 编写自定义词典(在elasticsearch-7.6.0\plugins\analysis-ik\config,下面新建 my.dic文件(名字自定义),并写入【程序猿】字典值到my.dic中)

  2. 注入到字典配置文件中(elasticsearch-7.6.0\plugins\analysis-ik\config\IKAnalyzer.cfg.xml),并重启ES。


    my.dic

image-20220618002431942

1.9 ES的核心概念

ES是面向文档

关系型数据库(例:Mysql) ES
数据库DataBase 索引Indices
表Table 类型types(慢慢弃用,7.0已经过时,8.0就会彻底弃用)
行rows 文档documents
列字段Columns 字段fields

ES集群中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型下又包含多个文档(行),每个行又包含多个字段(列)。

物理设计

image-20220617234327618

ES在后台把每个索引划分成多个分片,每个分片可以在集群中的的不同服务器间迁移,ES将数据分别存储在每个分片上,分片又存储在不同的节点上,保证了高可用。

一个集群至少有一个节点,而一个节点就是一个ES进程,如果你创建索引,那么索引将会默认有5个分片(primary shard,又称主分片)构成的,每一个主分片会有一个副本(replica shard ,又称复制分片)。

集群(cluster): 由一个或多个节点组成, 并通过集群名称与其他集群进行区分

节点(node): 单个 ElasticSearch 实例. 通常一个节点运行在一个隔离的容器或虚拟机中

索引(index): 在 ES 中, 索引是一组文档的集合

分片(shard): 因为 ES 是个分布式的搜索引擎, 所以索引通常都会分解成不同部分, 而这些分布在不同节点的数据就是分片. ES自动管理和组织分片, 并在必要的时候对分片数据进行再平衡分配, 所以用户基本上不用担心分片的处理细节.主分片数在索引创建时指定,后续不允许修改,除非 Reindex,重建索引

副本(replica): ES 默认为一个索引创建 5 个主分片(7.0版本以后,默认为1个主分片), 并分别为其创建1个副本分片. 也就是说每个索引都由 5 个主分片组成, 而每个主分片都相应的有一个 copy。对于分布式搜索引擎来说, 分片及副本的分配将是高可用及快速搜索响应的设计核心.主分片与副本都能处理查询请求,它们的唯一区别在于只有主分片才能处理索引请求.副本对搜索性能非常重要,同时用户也可在任何时候添加或删除副本。额外的副本能给带来更大的容量, 更高的呑吐能力及更强的故障恢复能力。主分片可读可写,副本只能读。

逻辑设计:

文档:

ES是面向文档的,索引和搜索数据的最小单位是文档。

  1. 自我包含,一篇文档同时包含字段和对应的值,也就是同时包含key:value,例如:name:张三。
  2. 可以是层次性的,一个文档中包含子文档,复杂的逻辑实体就是这么来的。就是一个json对象
  3. 灵活的数据结构,文档不依赖于预先定义的模式,关系型数据库中需要提前定义字段才能使用,在ES中,对于字段是非常灵活的,有时候可以忽略该字段或者动态的添加一个新的字段。

类型:

类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如name映射为字符串类型。虽说文档是无模式的,不需要拥有映射中所定义的所有字段,比如新增一个字段,那么ES会自动的将新增的字段加入映射,但是这个字段不确定是什么类型的,ES就会自动判断字段类型,但是可能判断的是错误的,所以最安全的方式是提前定义好所需要的映射,这点跟关系型数据库很像,先定义好字段在使用,避免出现问题。

索引:

索引是映射类型的容器,ES中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置。然后被存储到各个分片上。

2.0 倒排索引(反向索引)

ES使用的是一种称为倒排索引的结构,采用Lucene倒排索引为底层,这种结构适用于全文搜索。实现比关系型数据库更快的过滤方法。有利于ES在不扫描全部文档的情况下,找到哪些关键字存在于哪些文档中。

正排索引:

当用户发起查询时(假设查询为一个关键词),搜索引擎会先扫描索引库中的所有文档,找出所有包含关键词的文档,这种从文档中去查找是否含有关键词的方法叫做正向索引

正排索引:基于文档建立索引,建立方便,维护简单,检索效率低。扫描文档 --->关键字

倒排索引:

倒排索引:基于字、词建立索引,建立维护复杂,检索效率高。关键字 --> 文档

在这里插入图片描述

倒排索引的核心分为两部分:

第一部分为单词词典(Term Dictionary),记录所有文档的单词以及单词到倒排列表的关联关系。在实际生产中,单词量会非常大,所以实际会采用 B+ 树和哈希拉链法去存储单词的词典,以满足高性能的插入与查询。

第二部分是倒排列表(Posting List),它记录了单词对应的文档id,倒排列表是由倒排索引(Posting) 组成。,找到文档的id,在去找对应的文档。

倒排索引的使用场景:

搜索引擎,根据关键字,找到所有关联的文章。

2.1 RestfulApi

字段类型:

Text, 字符串类型(可被分词)
Keyword, 字符串类型(不可被分词,是一个整体)
Integer,
Long,
Date,(日期类型,可自定义格式)
Float,
Double,
Boolean,
Object,
Auto, 自动检测类型
Nested, 嵌套类型(Object数据类型的特殊版本)
Ip, IP地址类型(适用于ip段的查询106.94.0.0/16,查询更快)
请求方式 url 功能
PUT ip:port / 索引名称 / 类型名称 / 文档id 创建文档(指定id)
POST ip:port / 索引名称 / 类型名称 创建文档(随机id)
POST ip:port / 索引名称 / 类型名称 / 文档id / _update 修改指定文档
DELTE ip:port/索引名称/类型名称/文档id 删除指定文档
GET ip:port/索引名称/类型名称/文档id 获取指定文档
POST ip:port/索引名称/类型名称/_search 查询所有文档

可以直接使用kibana测试,类型Type在7.0版本后就开始过时了,8.0彻底废弃,不建议在使用,直接使用默认的 _doc

2.1.1 创建索引:

image-20220618004320855

查看ES索引数据存储结果:

image-20220618004400714

2.1.2 创建索引规则:

image-20220618005018062

image-20220618011651394

image-20220618011745594

2.1.3 获取索引信息:

image-20220618010936641

2.1.4 默认索引字段类型:

image-20220618012023889

image-20220618012223425

2.1.5 扩展命令:

GET /_cat/indices?v 查看ES中所有索引的统计信息。

image-20220618012855574

2.1.6 修改索引:

  1. 使用PUT命令(不推荐使用,全量覆盖,参数中缺少的字段,es会将其更新为空) PUT /索引/类型/id
  2. 使用POST命令(增量更新,参数中缺少的字段,es不会更新为空) POST/索引/类型/id/_update

PUT更新前:

image-20220618084359987

PUT更新后:

image-20220618084507760

image-20220618084446095

POST更新后:

image-20220618084610018

image-20220618084546327

2.1.7 删除索引:

通过DELETE 命令,可以删除 索引或文档

2.1.8 查询(重点)

原生restful的api针对于入门学习,实际开发中我们并不会这样调用,开发中使用es封装的api

image-20220618092052905

简单查询

根据id获取:GET /索引/类型/文档id
根据条件获取:GET /索引/类型/_search?q=属性:值

match和term的区别

  1. match会进行分词,对分词进行匹配,对字段进行模糊匹配;

  2. term不会进行分词,它只对字段的分词进行精确匹配;

  3. 比如,对于文档中text类型的字段name=张三,match查询条件name:张三能查到,但term不能;

    因为张三分词后是张,三;term会到倒排索引中找张三但是无法匹配
    再比如name=张三,如果查询条件为name:张,无论是term还是match都会匹配到
    match和term都会从倒排索引里面找,重点在于match会将条件进行分词,而term不会对条件分词

_search 查询

GET /索引/类型/_search

match查询:match查询会将我们的查询的关键字进行分词(分为 程序猿+甲),然后去匹配,但由于我们未设置索引的name字段类型,es默认name为text类型,es对索引的字段name也会进行分词分为 程序猿+甲+乙+丙几种,所以能匹配到包含程序猿的

GET /li/_doc/_search
{
  "query": {
    "match": {
      "name":"程序猿甲"
    }
  }
}
image-20220618100205987

operator:但我们也可以使用operator 要求搜索的关键字进行分词后,使用and(默认是or 例如上面:程序猿 or 甲)

GET /li/_doc/_search
{
  "query": {
    "match": {
      "name": {
        "query": "程序猿甲",
        "operator": "and"
      }
    }
  }
}

image-20220618111903427


_source 限制返回属性

ES默认是返回文档中的所有字段,我们可以使用_source进行限制字段返回

GET /li/_doc/_search
{
  "query": {
    "match": {
      "name":"程序猿"
    }
  },
  "_source":["name","age"]
}
image-20220618102319070
sort 排序
GET /li/_doc/_search
{
  "query": {
    "match": {
      "name": "程序猿"
    }
  },
  "sort": {
    "age": {
      "order": "desc"
    }
  }
}
image-20220618102959844
from、size分页

from:开始下标 size:每页查询条数

GET /li/_doc/_search
{
  "query": {
    "match": {
      "name": "程序猿"
    }
  },
  "sort": {
    "age": {
      "order": "desc"
    }
  },
  "from": 0,
  "size": 1
}

image-20220618103323289


bool 多条件组合查询
关键字 对应mysql中的关键字
must 等同于mysql中的 and
should 等同于mysql中的 or
must_not 等同于mysql中的 not
gt >
lt <
gte >=
lte <=
GET /li/_doc/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "程序猿乙"
          }
        },
        {
          "match": {
            "age": "26"
          }
        }
      ]
    }
  }
}

image-20220618104610267

GET /li/_doc/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "desc": "最美不过程序猿"
          }
        },
        {
          "match": {
            "age": "26"
          }
        }
      ]
    }
  }
}

image-20220618104934499

GET /li/_doc/_search
{
  "query": {
    "bool": {
      "must_not": [
        {
          "match": {
            "desc": "最美不过程序猿"
          }
        },
        {
          "match": {
            "age": "26"
          }
        }
      ]
    }
  }
}

image-20220618105034599

GET /li/_doc/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "name": "程序猿"
          }
        }
      ],
      "filter": {
        "range": {
          "age": {
            "gt": 25
          }
        }
      }
    }
  }
}

image-20220618105634134


term 精确查询

term是直接去倒排索引进行词条的精确匹配。

构建测试数据:

name为text

desc为keyword

image-20220618113106594

PUT /li2
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "desc": {
        "type": "keyword"
      }
    }
  }
}

PUT /li2/_doc/1
{
  "name":"程序猿甲",
  "desc":"谈一个对象 desc"
}

PUT /li2/_doc/2
{
  "name":"程序猿乙",
  "desc":"new一个对象 desc"
}

开始查询

GET /li2/_doc/_search
{
  "query":{
    "term":{
      "name":"程"
    }
  }
}

image-20220618114715919


GET /li2/_doc/_search
{
  "query":{
    "term":{
      "desc":"new一个对象 desc"
    }
  }
}

image-20220618113252660


tem的多条件精确匹配
GET /li2/_doc/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "name": "甲"
          }
        },
        {
          "term": {
            "desc": "谈一个对象 desc"
          }
        }
      ]
    }
  }
}

image-20220618115308217


highlight 高亮查询
GET /li2/_doc/_search
{
  "query": {
    "match":{
      "name":"程序"
    }
  },
  "highlight": {
    "fields":{
      "name":{}
    }
  }
}

image-20220618115756712

自定义高亮标签

pre_tags:前标签

post_tags:后标签

GET /li2/_doc/_search
{
  "query": {
    "match":{
      "name":"程序"
    }
  },
  "highlight": {
    "pre_tags":"<p style='color:red'>",
    "post_tags":"</p>",
    "fields":{
      "name":{}
    }
  }
}

image-20220618120041862

3. 集成SpringBoot

  1. 找ElasticSearch官方文档

  2. 引入官方依赖,找到关键对象RestHighLevelClient

    <!--elasticsearch依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    
  3. 创建cilent对象 RestHighLevelClient

    RestHighLevelClient restClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http"),new HttpHost("127.0.0.1", 9201, "http")));
    

3.1 创建项目

创建maven项目,创建module(使用springboot的骨架创建)

image-20220618130954604

image-20220618142134659

3.2 修改ES核心包的版本

我们看到2.2.5的springboot中集成的是es的 6.8.6版本,所以需要修改版本号为我们的es版本,7.6.0,找到es版本的标签,直接在我们的pom文件中进行覆盖

image-20220618184531138

3.3 配置ES客户端对象

操作ES的对象 类型 备注
Transport Client 客户端 在ElasticSearch8中将被移除,在spring-data-elasticsearch:4.0版本中被标为过期,不推荐使用
RestHighLevelClient 客户端 高级Restful客户端,灵活度更高,但操作对象需要格式转换。推荐使用
ElasticsearchOperations 接口 直接注入使用,具体使用的客户端要看实现类所配置的客户端,也能使用,但是复杂查询可能比较麻烦
ElasticsearchRepository 接口 Spring-data-elasticsearch 提供好的接口,只需要继承 ElasticsearchRepository 接口即可,底层使用的是ElasticsearchOperations 接口的方法(对ElasticsearchOperations接口的再次扩展)
ElasticsearchTemplate 实现类 是 ElasticsearchOperations的实现,使用的是 Transport Client,在spring-data-elasticsearch:4.0版本中被标为过期,被ElasticsearchRestTemplate代替,不推荐使用
ElasticsearchRestTemplate 实现类 是 ElasticsearchOperations的实现,使用的是 RestHighLevelClient

创建RestHighLevelClient 客户端

  1. 推荐使用这种,这种配置完客户端后就可以同时使用这两个对象 RestHighLevelClient、ElasticsearchRestTemplate
package com.yuening.esapi.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

@Configuration
public class ElasticSearchClientConfig {

    /**
     * es的集群节点配置
     * 127.0.0.1:9200,127.0.0.1:9201,127.0.0.1:9202
     */
    @Value("${spring.elasticsearch.rest.uris}")
    private String uri;

    /**
     * 创建es的客户端对象
     *
     * @return
     */
    @Bean
    public RestHighLevelClient restHighLevelClient() {

        //单机版本 & 集群版本 官方文档:
        //RestHighLevelClient restClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http"),new HttpHost("127.0.0.1", 9201, "http")));

        if (StringUtils.isEmpty(uri)) {
            throw new RuntimeException("elasticsearch uri is unset to properties file");
        }
        String[] nodes = uri.split(",");
        HttpHost[] httpHosts = new HttpHost[nodes.length];
        for (int x = 0; x < nodes.length; x++) {
            String[] uris = nodes[x].split(":");
            HttpHost httpHost = new HttpHost(uris[0], Integer.parseInt(uris[1]), "http");
            httpHosts[x] = httpHost;
        }
        return new RestHighLevelClient(RestClient.builder(httpHosts));
    }
}

  1. 这种配置完客户端后还必须要手动创建 ElasticsearchRestTemplate 才可注入使用 ElasticsearchRestTemplate(否则会报找不到类)
@Configuration
public class ElasticSearchClientConfig2 extends AbstractElasticsearchConfiguration {

    /**
     * es的集群节点配置
     * 127.0.0.1:9200,127.0.0.1:9201,127.0.0.1:9202
     */
    @Value("${spring.elasticsearch.rest.uris}")
    private String uri;

    /**
     * 初始化 RestHighLevelClient
     * @return
     */
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder().connectedTo(uri.split(",")).build();
        return RestClients.create(clientConfiguration).rest();
    }

    /**
     * 初始化 ElasticsearchRestTemplate 才能使用
     * @return
     */
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate() {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }
}

3.4 简单测试

这里会使用ElasticsearchRestTemplate、ElasticsearchOperations、RestHighLevelClient

import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;

@RestController
public class TestController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Autowired
    private ElasticsearchOperations elasticsearchOperations;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @GetMapping("/test")
    public String test(){
        boolean test = elasticsearchRestTemplate.createIndex("test");
        return String.valueOf(test);
    }

    @GetMapping("/test2")
    public String test2(){
        boolean test2 = elasticsearchOperations.createIndex("test2");
        return String.valueOf(test2);
    }

    @GetMapping("/test3")
    public String test3() throws IOException {
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("test3");
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        return String.valueOf(createIndexResponse.isAcknowledged());
    }
}

3.5 索引API

import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;

@SpringBootTest
class EsApiApplicationTests {


    @Autowired
    RestHighLevelClient restHighLevelClient;

    @Test
    public void createIndex() throws IOException {
        CreateIndexRequest createIndexRequest = new CreateIndexRequest("yu");
        //可以设置索引字段规则,也可以不设置,es会自动设置
        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
                .startObject()
                .startObject("properties")
                .startObject("name").field("type", "keyword").endObject()
                .startObject("age").field("type", "integer").endObject()
                .startObject("desc").field("type", "text").endObject()
                .endObject()
                .endObject();
        //也可以直接使用DLS语句中的json字符串 例如: createIndexRequest.mapping("",XContentType.JSON);
        createIndexRequest.mapping(xContentBuilder);

        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        boolean acknowledged = createIndexResponse.isAcknowledged();
        System.out.println("索引创建成功," + acknowledged);
    }

    @Test
    public void existsIndex() throws IOException {
        GetIndexRequest getIndexRequest = new GetIndexRequest("yu");
        boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
        System.out.println("索引是否存在,"+exists);
    }

    @Test
    public void deleteIndex() throws IOException {
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("yu");
        AcknowledgedResponse delete = restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
        System.out.println("索引删除成功,"+delete.isAcknowledged());
    }

}

3.6 文档API CRUD

package com.yuening.esapi;

import com.alibaba.fastjson.JSONObject;
import com.yuening.esapi.domain.Student;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.query.WildcardQueryBuilder;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@SpringBootTest
class EsApiApplicationTests {


    @Autowired
    RestHighLevelClient restHighLevelClient;

    @Test
    public void createDocument() throws IOException {
        // PUT /yu/_doc/1
        Student stu = new Student("张三", 18, "张三说我不是李四");

        //构建索引请求,参数为索引名称
        IndexRequest indexRequest = new IndexRequest("yu");
        //设置文档id
        indexRequest.id("1");
        //将我们的数据放入请求中 json格式
        indexRequest.source(JSONObject.toJSONString(stu), XContentType.JSON);

        //发起创建索引的请求
        IndexResponse index = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        RestStatus status = index.status();
        System.out.println("文档创建成功," + status);
    }

    /**
     * 获取文档
     *
     * @throws IOException
     */
    @Test
    public void getDocument() throws IOException {
        // GET /yu/_doc/1
        GetRequest getRequest = new GetRequest("yu", "1");
        /**
         * {
         *     "_index":"yu",
         *     "_type":"_doc",
         *     "_id":"1",
         *     "_version":1,
         *     "_seq_no":0,
         *     "_primary_term":1,
         *     "found":true,
         *     "_source":{
         *         "age":18,
         *         "desc":"张三说我不是李四",
         *         "name":"张三"
         *     }
         * }
         */
        GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
        //获取文档中的_source内容,为string
        String sourceAsString = getResponse.getSourceAsString();
        Student student = JSONObject.parseObject(sourceAsString, Student.class);
        System.out.println("文档获取成功," + JSONObject.toJSONString(student));
        //获取文档中的_source内容为map
        Map<String, Object> sourceAsMap = getResponse.getSourceAsMap();
        System.out.println("文档获取成功," + JSONObject.toJSONString(sourceAsMap));
    }

    /**
     * 更新文档
     *
     * @throws IOException
     */
    @Test
    public void updateDocument() throws IOException {
        UpdateRequest updateRequest = new UpdateRequest("yu", "1");
        Student stu = new Student();
        stu.setDesc("我说你就是李四啊啊啊啊");
        // POST /yu/_doc/1/_update  {"doc":{"desc":"我说你就是李四啊啊啊啊"}}
        updateRequest.doc(JSONObject.toJSONString(stu), XContentType.JSON);
        /**
         * {
         *     "fragment":false,
         *     "id":"1",
         *     "index":"yu",
         *     "primaryTerm":1,
         *     "result":"UPDATED",
         *     "seqNo":2,
         *     "shardId":{
         *         "fragment":true,
         *         "id":-1,
         *         "index":{
         *             "fragment":false,
         *             "name":"yu",
         *             "uUID":"_na_"
         *         },
         *         "indexName":"yu"
         *     },
         *     "shardInfo":{
         *         "failed":0,
         *         "failures":[
         *
         *         ],
         *         "fragment":false,
         *         "successful":1,
         *         "total":2
         *     },
         *     "type":"_doc",
         *     "version":3
         * }
         */
        UpdateResponse update = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
        System.out.println("更新文档成功," + JSONObject.toJSONString(update));
    }

    /**
     * 删除文档
     *
     * @throws IOException
     */
    @Test
    public void deleteDocument() throws IOException {
        DeleteRequest deleteRequest = new DeleteRequest("yu", "1");
        DeleteResponse delete = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        RestStatus status = delete.status();
        System.out.println("删除文档成功," + status);
    }

    /**
     * 批量操作文档 新增、更新、删除
     */
    @Test
    public void bulkOperateDocument() throws IOException {
        //批处理请求
        BulkRequest bulkRequest = new BulkRequest();

        List<Student> studentList = new ArrayList<>();
        Student stu1 = new Student("李四", 1, "我是李四");
        Student stu2 = new Student("李四2", 2, "我是李四2");
        Student stu3 = new Student("李四3", 3, "我是李四3");
        Student stu4 = new Student("李天奎1", 4, "我是李天奎1我也是李天奎");
        Student stu5 = new Student("李天奎2", 5, "我是李天奎2我也是李天奎");
        Student stu6 = new Student("李天奎3", 6, "我是李天奎3");
        studentList.add(stu1);
        studentList.add(stu2);
        studentList.add(stu3);
        studentList.add(stu4);
        studentList.add(stu5);
        studentList.add(stu6);

        studentList.forEach(p -> {
            //也可以创建批量更新、批量删除
            //UpdateRequest updateRequest = new UpdateRequest("yu","1");
            //DeleteRequest deleteRequest = new DeleteRequest("yu","1");

            IndexRequest indexRequest = new IndexRequest("yu");
            //indexRequest.id(); 可不设置id,es会默认生成一个字符串id
            //将我们需要批量处理的数据 放入source中
            indexRequest.source(JSONObject.toJSONString(p), XContentType.JSON);
            //将请求添加到 批处理请求中
            bulkRequest.add(indexRequest);
        });

        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
        boolean b = bulk.hasFailures();//是否有失败
        BulkItemResponse[] items = bulk.getItems();//获取此次批处理执行的结果集合

        System.out.println("批量插入文档成功,是否全部成功:" + !b + "," + JSONObject.toJSONString(items[0]));

    }

    /**
     * 搜索文档
     *
     * @throws IOException
     */
    @Test
    public void searchDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建搜索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "李");
        searchSourceBuilder.query(termQueryBuilder);

        searchRequest.source(searchSourceBuilder);
        //发起请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();

        System.out.println("查询文档成功:" + JSONObject.toJSONString(hits));
    }

    /**
     * 搜索文档 Query && Filter Query
     *
     * Query:查询结果会计算文档得分,并根据文档得分进行返回
     * Filter Query:过滤查询,用在大数据量中筛选出查询的相关结果,不会计算文档得分
     * 而且ES对filter query进行优化,对于经常使用的filter query的查询结果进行缓存,方便下次查询
     *
     * 注意:es中一旦同时使用了 query和filter query,es会先执行filter query再去执行query(类似于mysql的where条件过滤),可以提高查询效率
     *
     * @throws IOException
     */
    @Test
    public void searchQueryAndFilterQueryDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建搜索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "李天奎3");
        //同时使用 query和filter query
        searchSourceBuilder.query(termQueryBuilder).postFilter(QueryBuilders.termQuery("age",6));

        searchRequest.source(searchSourceBuilder);
        //发起请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits hits = searchResponse.getHits();

        System.out.println("查询文档成功:" + JSONObject.toJSONString(hits));
    }

    /**
     * 搜索文档bool查询
     *
     * @throws IOException
     */
    @Test
    public void searchBoolDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建搜索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构建排序条件
        searchSourceBuilder.sort(SortBuilders.fieldSort("age").order(SortOrder.DESC));
        //构建match查询条件
        MatchQueryBuilder nameMatchQueryBuilder = QueryBuilders.matchQuery("name", "李");
        //构建range范围查询条件
        RangeQueryBuilder ageRangeQueryBuilder = QueryBuilders.rangeQuery("age").from(3).to(6);
        //构建通配符条件
        WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("desc", "*奎*");
        //构建bool查询条件
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().must(nameMatchQueryBuilder).must(ageRangeQueryBuilder).must(wildcardQueryBuilder);
        searchSourceBuilder.query(queryBuilder);
        searchRequest.source(searchSourceBuilder);
        //发起请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //获取返回值,以及总记录数
        SearchHits hits = searchResponse.getHits();
        long value = searchResponse.getHits().getTotalHits().value;
        System.out.println("bool查询文档成功,总记录数:" + value + ",详细结果:" + JSONObject.toJSONString(hits));
    }

    /**
     * 分页查询文档
     *
     * @throws IOException
     */
    @Test
    public void searchPageDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建SearchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构建分页查询 下标0开始-查5条
        searchSourceBuilder.from(0).size(5);
        //fetchSource可以指定只返回的字段,也可以指定需要排除的字段,第一个参数为:包含的字段数组 第二个参数为:需要排除的字段数组
        searchSourceBuilder.fetchSource(new String[]{"name","desc"},new String[]{});
        //构建通配符条件
        WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("desc", "*奎*");
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().must(wildcardQueryBuilder);
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //获取返回值,以及总记录数
        SearchHits hits = searchResponse.getHits();
        long value = searchResponse.getHits().getTotalHits().value;
        System.out.println("分页查询文档成功,总记录数:" + value + ",详细结果:" + JSONObject.toJSONString(hits));
    }

    /**
     * 高亮查询文档
     *
     * @throws IOException
     */
    @Test
    public void searchHighlightDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建SearchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        //构建HighlightBuilder高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("desc");
        highlightBuilder.preTags("<span style='color:red'>");
        highlightBuilder.postTags("</span>");
        //多个字段高亮的时候设置为false,否则只会高亮一个字段
        highlightBuilder.requireFieldMatch(false);
        //构建查询条件 desc中含有奎的,name中含有李的
        WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("desc", "*奎*");
        WildcardQueryBuilder wildcardQueryBuilder2 = QueryBuilders.wildcardQuery("name", "*李*");

        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery().should(wildcardQueryBuilder).should(wildcardQueryBuilder2);

        searchSourceBuilder.highlighter(highlightBuilder).query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        List<Map<String, Object>> mapList = new ArrayList<>();
        //执行搜索请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //解析高亮,返回结果中并不是直接把_source中的字段desc加上了高亮标签,而是单独返回了高亮的字段,需要我们自行进行替换
        SearchHits hits = searchResponse.getHits();
        for (SearchHit hit : hits.getHits()) {
            //获取高亮的字段
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            //获取_source map
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            //获取高亮字段desc
            HighlightField descHighlightField = highlightFields.get("desc");
            //判断是不是为空,不然你匹配的第一个结果没有高亮内容,那么就会报空指针异常
            if (null != descHighlightField) {
                //获取高亮字段的文本内容(是个数组)
                Text[] texts = descHighlightField.getFragments();
                StringBuilder textStr = new StringBuilder();
                for (Text text : texts) {
                    textStr.append(text);
                }
                //替换掉_source中的desc字段内容
                sourceAsMap.put("desc", textStr);
                mapList.add(sourceAsMap);
            }
        }
        System.out.println(JSONObject.toJSONString(mapList));
    }
}

3.7 聚合查询

聚合:英文为Aggregation,简称Aggs ,聚合查询是es除了搜索功能外提供的针对es数据做数据统计分析的功能。聚合有助于根据搜索查询提供聚合数据。聚合查询是数据库中重要的功能特征。它是基于查询条件来对数据进行分桶、计算的方法。有点类似于sql中的group by再加一下函数方法的操作。

注意:text类型的字段是不支持聚合的(text是支持分词的,无法聚合统计)

es中的聚合,例如:terms分组、max、min、avg、sum

kibana的语法:
3.7.1 字段分组统计

size:如何不设置为0,会默认返回hits集合,默认返回前10条数据,而我们只需要聚合结果即可,设置为0即可

age_group:自定义的聚合的名字,一般以 字段+聚合函数类型,此处是按照age分组,所以命名为age_group

GET /yu/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "size":0,
  "aggs":{
    "age_group":{
      "terms":{
        "field":"age"
      }
    }
  }
}

image-20220620232356218

3.7.2 最大值&最小值
GET /yu/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "size":0,
  "aggs":{
    "age_max":{
      "max":{
        "field":"age"
      }
    }
  }
}

image-20220620232447973

GET /yu/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "size":0,
  "aggs":{
    "age_min":{
      "min":{
        "field":"age"
      }
    }
  }
}

image-20220620232800757

3.7.3 平均值
GET /yu/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "size":0,
  "aggs":{
    "age_avg":{
      "avg":{
        "field":"age"
      }
    }
  }
}

image-20220620232829121

3.7.4 求和
GET /yu/_doc/_search
{
  "query":{
    "match_all":{}
  },
  "size":0,
  "aggs":{
    "age_sum":{
      "sum":{
        "field":"age"
      }
    }
  }
}

image-20220620232916179

RestHighLevel的写法:
/**
     * 聚合分组统计查询
     * 例如:查询各年龄段的人数
     *
     * @throws IOException
     */
    @Test
    public void searchAggsDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建SearchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构建查询,size默认为0 && 构建聚合
        searchSourceBuilder.query(QueryBuilders.matchAllQuery())
                .size(0)
                .aggregation(AggregationBuilders.terms("age_group").field("age"))
                //.aggregation(AggregationBuilders.terms("name_group").field("name"))
                ;

        searchRequest.source(searchSourceBuilder);

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

        //获取聚合统计分组的结果,因为年龄是int类型,使用ParsedLongTerms接收
        ParsedLongTerms parsedLongTerms = aggregations.get("age_group");

        //如果是name字段,那就用ParsedStringTerms类接收
        //ParsedStringTerms parsedStringTerms = aggregations.get("name_group");

        List<? extends Terms.Bucket> buckets = parsedLongTerms.getBuckets();
        buckets.forEach(p -> {
            System.out.println("age:" + p.getKey() + ",count:" + p.getDocCount());
        });
    }

    /**
     * 聚合Max、Min、Sum、Avg查询
     *
     * @throws IOException
     */
    @Test
    public void searchAggsMaxMinSumAvgDocument() throws IOException {
        //构建搜索请求
        SearchRequest searchRequest = new SearchRequest("yu");
        //构建SearchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //构建查询,size默认为0 && 构建聚合
        searchSourceBuilder.query(QueryBuilders.matchAllQuery())
                .size(0)
                .aggregation(AggregationBuilders.max("age_max").field("age")) //设置max聚合查询
                .aggregation(AggregationBuilders.min("age_min").field("age")) //设置min聚合查询
                .aggregation(AggregationBuilders.sum("age_sum").field("age")) //设置sum聚合查询
                .aggregation(AggregationBuilders.avg("age_avg").field("age")) //设置avg聚合查询
        ;

        searchRequest.source(searchSourceBuilder);

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

        ParsedMax parsedMax = aggregations.get("age_max");
        ParsedMin parsedMin = aggregations.get("age_min");
        ParsedSum parsedSum = aggregations.get("age_sum");
        ParsedAvg parsedAvg = aggregations.get("age_avg");
        System.out.println("age_max:"+parsedMax.getValue());
        System.out.println("age_min:"+parsedMin.getValue());
        System.out.println("age_sum:"+parsedSum.getValue());
        System.out.println("age_avg:"+parsedAvg.getValue());
    }

总结

以上就是ES的入门级简单总结,学无止境,继续努力!!!

posted @ 2022-06-23 08:30  AI枂柠  阅读(699)  评论(0)    收藏  举报