Elasticsearch7学习笔记

一、ElasticSearch简介:

ElasticSearch 是一个基于 Apache Lucene 的开源分布式全文搜索引擎,广泛用于全文搜索、日志分析、实时数据处理等应用场景。它通过提供高效、可扩展的搜索和分析功能,成为许多现代应用程序中不可或缺的技术组件。ElasticSearch 由 Elastic.co 公司开发,是 Elastic Stack(简称 ELK Stack)的一部分,通常与 Logstash(日志收集和处理)和 Kibana(数据可视化)一起使用,构成强大的日志管理和数据分析平台。

相关用途:

1、全文搜索:

ElasticSearch 被广泛用于构建搜索引擎,可以对大量文档进行全文索引,并支持复杂的查询、排序和高亮显示等功能。例如,Google、Wikipedia 和大型电商平台常用它来为用户提供快速的搜索结果。

2、日志分析:

ElasticSearch 是日志分析系统中常用的工具,尤其是在配合 Logstash 和 Kibana 时。可以实时收集、存储、搜索和分析日志数据,帮助开发者和运维人员进行系统监控和故障排查。

3、数据分析与报告:

ElasticSearch 不仅可以用于存储和检索数据,还支持对数据进行高级聚合分析。它可以帮助企业实时跟踪关键业务指标和数据趋势。

4、推荐系统:

基于用户行为和兴趣,ElasticSearch 可以作为推荐引擎的核心组件,提供个性化的推荐服务。

5、地理位置搜索:

ElasticSearch 支持地理位置数据的索引和查询,广泛用于提供基于位置的服务,如地图应用和位置相关的搜索。

6、监控和告警系统:

ElasticSearch 可以作为系统监控和告警的基础,实时收集并分析各种监控数据,如 CPU 使用率、内存、硬盘空间等。

7、安全信息与事件管理(SIEM):

ElasticSearch 被用于安全数据的收集、存储和分析,帮助组织检测和响应安全威胁。

 

二、Docker-ElasticSearch安装:

# 拉取镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.17.24

# 创建network
docker network create es-net

# 修改tag
docker image tag docker.elastic.co/elasticsearch/elasticsearch:7.17.24 elasticsearch:7.17.24

# 创建自定义目录
mkdir -p /opt/install/data/es-data/data
mkdir -p /opt/install/data/es-data/plugins

# 运行es
docker run -d \
 --name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/opt/install/data/es-data/data \
    -v es-plugins:/opt/install/data/es-data/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.17.24

# 安装kibana镜像
docker pull kibana:7.17.24

# 启动容器
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.17.24

# 在线安装IK分词插件,进入容器内部
docker exec -it es /bin/bash

# 在线下载并安装(https://release.infinilabs.com/analysis-ik/stable/)
./bin/elasticsearch-plugin install https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.17.24.zip

#退出
exit

# 重启容器
docker restart es

Kibana可视化界面访问地址:http://localhost:5601

 

 

三、ElasticSearch基础架构:

1、Elasticsearch与MySQL的类比:

MySQL

Elasticsearch

数据库(database)

索引(index)

表(table)

类型(type)(6.0.0废弃)

行(row)

文档(document)

列(column)

字段(field)

表结构(schema)

映射(mapping)

 

2、Elasticsearch的属性类型:

类型

字段

字符串

Text(会被分词)、keyword(整体,不会被分词)

数值(不会被分词)

long、integer、short、byte、double、float、half float、scaled float

日期(不会被分词)

date

布尔值(不会被分词)

boolean

二进制(不会被分词)

binary

注:在ES中除了text类型分词,其他类型不分词,整体匹配

 

3、Elasticsearch的核心概念:

(1)、倒排索引:

在Elasticsearch中,底层检索使用的是倒排索引。所谓的倒排索引(Inverted Index)也叫反向索引,倒排索引会在存储数据时对文档进行分词形成词项,并将每个词项与文档ID进行关联,存储在倒排表中。查询时,会将查询内容进行分词后,在倒排索引中查找相关词项,最终匹配出相关的文档。

有反向索引必有正向索引,通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。

(2)、分析器(Analyzer):

在Elasticsearch中,分析器(Analyzer)是用于处理和分析文本数据的组件,分析器的作用是将原始文本(如用户输入的查询或文档内容)转换成词项(tokens),这些词项用于构建 倒排索引(inverted index),从而加速全文检索过程。

常见分析器

描述

Standard Analyzer

默认内置分析器,适用于多种语言,通常用于英文和其他基于拉丁字母的语言。它会基于Unicode字符集和标准规则进行分词,按单词切分,并且会删除绝大多数标点符号,转换成小写。

POST /_analyze

{

  "analyzer": "standard",

  "text": "MY the test, 你好"

}

Whitespace Analyzer

内置分析器,按照空格、制表符等分隔符切分文本,但不进行小写转换,不会去除标点符号或停用词

POST /_analyze

{

  "analyzer": "whitespace",

  "text": "MY the test, 你好"

}

Simple Analyzer

内置分析器,将文本按单词切分,并且将文本转换成小写,同时去除标点符号和其他非字母字符

POST /_analyze

{

  "analyzer": "simple",

  "text": "MY the test, 你好"

}

Stop Analyzer

内置分析器,小写处理,过滤the,a,is等停用词,不会处理标点符号

POST /_analyze

{

  "analyzer": "stop",

  "text": "MY the test, 你好"

}

Keyword Analyzer

内置分析器,不会进行分词,直接将整个输入文本当作一个单独的词项输出

POST /_analyze

{

  "analyzer": "keyword",

  "text": "MY the test, 你好"

}

IK Analyzer

开源的中文分词器,分为两种模式:ik_max_word(细粒度分词)和 ik_smart(智能分词)。IK分词器能够根据字典、规则进行分词,适用于中文文本的处理

POST /_analyze

{

  "analyzer": "ik_smart",

  "text": "MY the test, 你好"

}

分析器主要由三部分组成:

1)、字符过滤器(Character Filters):

字符过滤器用于在文本分词之前对文本进行修改

功能

描述

去除HTML标签

清除HTML标记,避免将HTML标签当作词项处理

替换字符

将某些字符替换成其他字符,如将全角字符转为半角字符

删除字符

移除文本中的特定字符

2)、分词器(Tokenizer):

分词器是分析器中的核心组件,它负责将文本切割成单个的词项。

常见分词器

描述

Standard分词器

适用于多种语言,通常用于英文和其他基于拉丁字母的语言。它会基于Unicode字符集和标准规则进行分词,按单词切分,并且会删除绝大多数标点符号,转换成小写

Whitespace分词器

按照空格、制表符等分隔符切分文本,但不进行小写转换,不会去除标点符号或停用词

Simple分词器

将文本按单词切分,并且将文本转换成小写,同时去除标点符号和其他非字母字符

IK分词器

开源的中文分词器,分为两种模式:ik_max_word(细粒度分词)和 ik_smart(智能分词)。IK分词器能够根据字典、规则进行分词,适用于中文文本的处理

3)、词项过滤器(Token Filter):

词项过滤器是在分词之后对词项进行进一步处理的工具

过滤器

描述

小写转换(Lowercase Filter)

将所有词项转换为小写,以便进行不区分大小写的搜索

去除停用词(Stop Words Filter)

去除常见的无实际意义的词(如 "the", "and" 等)

同义词处理(Synonym Filter)

将同义词转换为相同的词项

拼写修正(Fuzzy Filter)

对拼写错误的词项进行纠正

词干提取(Stemmer Filter)

将词项转换为它的词根或基础形式(如将 "running" 转换为 "run")

 

四、ElasticSearch-CURL操作:

1、创建索引:

基础结构:

PUT /索引名

{

  "settings": {

    "number_of_shards": 设置索引的分片数目,

    "number_of_replicas": 设置副本的数量

  },

  "mappings": {

    "properties": {

      "属性字段":{

        "type": "属性类型"

      }

    }

  }

}

案例:

Kibana

RESTful

PUT /my_test

{

  "settings": {

    "number_of_shards": 1,

    "number_of_replicas": 0

  },

  "mappings": {

    "properties": {

      "name":{

        "type": "keyword"

      },

      "created_at":{

        "type": "date"

      },

      "description":{

        "type": "text"

      },

      "type":{

        "type": "integer"

      }

    }

  }

}

curl -X PUT 'http://localhost:9200/my_test' \

--header 'Content-Type: application/json' \

--data '{

  "settings": {

    "number_of_shards": 1,

    "number_of_replicas": 0

  },

  "mappings": {

    "properties": {

      "name":{

        "type": "keyword"

      },

      "created_at":{

        "type": "date"

      },

      "description":{

        "type": "text"

      },

      "type":{

        "type": "integer"

      }

    }

  }

}';

2、新增数据:

(1)、自定义文档_id:

基础结构:

POST /索引名/_doc/自定义文档id  

{

  "属性字段":"属性值"

}

案例:

Kibana

RESTful

POST /my_test/_doc/1  

{

  "name":"名字1",

  "created_at":"2024-10-01T00:00:00Z",

  "description":"描述1",

  "type":1

}

curl -X POST 'http://localhost:9200/my_test/_doc/1' \

--header 'Content-Type: application/json' \

--data '{

  "name":"名字1",

  "created_at":"2024-10-01T00:00:00Z",

  "description":"描述1",

  "type":1

}';

(2)、自动生成文档_id:

基础结构:

POST /索引名/_doc/

{

  "属性字段":"属性值"

}

案例:

Kibana

RESTful

POST /my_test/_doc/

{

  "name":"名字2",

  "created_at":"2024-10-01T00:00:00Z",

  "description":"描述2",

  "type":1

}

curl -X POST 'http://localhost:9200/my_test/_doc/' \

--header 'Content-Type: application/json' \

--data '{

  "name":"名字2",

  "created_at":"2024-10-01T00:00:00Z",

  "description":"描述2",

  "type":1

}';

(3)、已有索引新增属性字段:

基础结构:

POST /索引名/_mapping

{

    "properties":{

        "属性字段":{

            "type":"属性类型"

        }

    }

}

案例:

Kibana

RESTful

POST /my_test/_mapping

{

    "properties":{

        "status":{

            "type":"integer"

        }

    }

}

curl -X POST 'http://localhost:9200/my_test/_mapping' \

--header 'Content-Type: application/json' \

--data '{

    "properties":{

        "status":{

            "type":"integer"

        }

    }

}';

3、查询数据:

(1)、查看所有索引的基础信息:

Kibana

#查看所有索引基础信息(占用空间):

GET _cat/indices?v

#查看集群中所有索引的详细信息(fielddata占用大小):

GET _cat/indices?v&h=index,health,status,fielddata.memory_size&s=fielddata.memory_size:desc

# 显示每个节点字段fielddata所占的堆空间 并按照所占空间降序排列:

GET _cat/fielddata?v&s=size:desc

RESTful

curl -X GET 'http://localhost:9200/_cat/indices?v';

响应结果

表头

含义

health

当前服务器健康状态: green(集群完整) yellow(单点正常、集群不完整) red(单点不正常)

status

索引打开、关闭状态

index

索引名

uuid

索引统一编号

pri

主分片数量

rep

副本数量

docs.count

可用文档数量

docs.deleted

文档删除状态(逻辑删除)

store.size

主分片和副分片整体占空间大小

pri.store.size

主分片占空间大小

(2)、查看指定索引基础信息:

基础结构:

GET /索引名

案例:

Kibana

RESTful

GET /my_test

curl -X GET 'http://localhost:9200/my_test';

(3)、查看指定索引的属性映射信息:

基础结构:

GET /索引名/_mappings?pretty

案例:

Kibana

RESTful

GET /my_test/_mappings?pretty

curl -X GET 'http://localhost:9200/my_test/_mappings?pretty';

(4)、查看指定索引的所有数据信息:

基础结构:

#方式一:

GET /索引名/_search

#方式二:

GET /索引名/_search

{

  "query": {

    "match_all": {}

  }

}

案例:

Kibana

RESTful

GET /my_test/_search

curl -X GET 'http://localhost:9200/my_test/_search';

(5)、根据文档_id查看指定索引的指定数据:

基础结构:

GET /索引名/_doc/文档id

案例:

Kibana

RESTful

GET /my_test/_doc/2

curl -X GET 'http://localhost:9200/my_test/_doc/2';

(6)、条件查询:

方式一、单条件匹配:

基础结构:

GET /索引名/_doc/_search

{

  "_source":["属性字段"],  //结果过滤属性字段

  "query":{

    "match":{  //match:全文搜索,term:精确匹配

      "属性字段": "条件"

    }

  },

  "sort":[

    {

      "属性字段":{

        "order":"desc"  //排序顺序,asc或desc

      }

    }

  ],

  "from":页码,  //第几页

  "size":页数,   //每页个数

  "track_total_hits": true   // true:开启精确的总命中数统计,数据集大影响一定性能

}

Kibana案例

GET /my_test/_doc/_search

{

  "_source":["name", "created_at", "type"],

  "query":{

    "match":{

      "type": 1

    }

  },

  "sort":[

    {

      "name":{

        "order":"desc"  

      }

    }

  ],

  "from":0,

  "size":5,

  "track_total_hits": true

}

等价SQL

SELECT name,created_at,type FROM `my_test` WHERE type = 1 ORDER BY name desc LIMIT 0,5

RESTful

curl -X GET 'http://localhost:9200/my_test/_doc/_search' \

--header 'Content-Type: application/json' \

--data '{

  "_source":["name", "created_at", "type"],

  "query":{

    "match":{

      "type": 1

    }

  },

  "sort":[

    {

      "name":{

        "order":"desc"  

      }

    }

  ],

  "from":0,

  "size":5

}';

备注:

1、term查询:

(1)、适用场景:适用于精确匹配,不会对查询字符串进行分词处理,直接去倒排索引查找精确的值

(2)、效率:通常情况下,term查询的效率更高,因为它直接在倒排索引中查找精确的词条,不需要进行分词处理。

(3)、使用场景:适合用于搜索关键词字段,如ID、状态码、类别等。

(4)、text 类型字段在索引时会被分词,term查询用于处理text类型字段时,其行为可能会导致意外的结果,不推荐使用

2、match查询:

(1)、适用场景:适用于全文搜索,会经过分析(analyer)的,也就是说,文档是先被分析器处理了,根据不同的分析器,分析出的结果也会不同,再根据分词结果进行匹配

(2)、效率:相对于term查询,match查询的效率较低,因为需要进行分词处理,然后在倒排索引中查找多个词条。

(3)、使用场景:适合用于搜索文本字段,如文章内容、描述等

方式二、复合条件匹配:

基础结构:

GET /索引名/_doc/_search

{

  "query": {

  //布尔查询:允许组合多个子查询,使用must、must_not、should和filter子句

      "bool": {

          "must": [  //定义满足的条件,相当于SQL的AND

              {

                  "terms": {  //匹配多个条件值,相当于SQL的IN

                      "属性字段": [ "条件1", "条件2"... ]

                  }

              },

              {

                  "bool": {

                      "should": [  //定义应该满足的条件,相当于SQL的OR

                          {

                              "term": {

                                  "属性字段": "条件"

                              }

                          },

                          {

                              "term": {

                                  "属性字段": "条件"

                              }

                          }

                      ],

         //1:表示至少满足一个should子句中的条件,该语句可省略,默认OR

                      "minimum_should_match": 1

                  }

              }

          ],

          "must_not": [  //定义不能满足的条件,相当于SQL的NOT

              {

                  "term": {

                      "属性字段": "条件"

                  }

              }

          ],

          "filter": {  //定义范围过滤条件

              "range": {

                  "属性字段": {

         //gt(大于)、gte(大于等于)、lt(小于)、lte(小于等于)、from和 to组合

                      "gte": "条件",

                     "lt": "条件"

                  }

              }

          }

      }

  }

}

Kibana案例

GET /my_test/_doc/_search

{

  "query": {

      "bool": {

          "must": [

              {

                  "terms": {

                      "name": [ "名字1", "名字2" ]

                  }

              },

              {

                  "bool": {

                      "should": [

                          {

                              "term": {

                                  "status": 1

                              }

                          },

                          {

                              "term": {

                                  "status": 2

                              }

                          }

                      ],

                      "minimum_should_match": 1

                  }

              }

          ],

          "must_not": [

              {

                  "term": {

                      "type": 2

                  }

              }

          ],

          "filter": {

              "range": {

                  "created_at": {

                      "gte": "2024-01-01T00:00:00Z",

                     "lt": "2025-01-01T00:00:00Z"

                  }

              }

          }

      }

  }

}

等价SQL

SELECT * FROM `my_test` WHERE name IN ("名字1", "名字2") AND (status = 1 OR status = 2)

AND type != 2 AND created_at >= "2024-01-01T00:00:00Z"

AND created_at < "2025-01-01T00:00:00Z";

RESTful

curl -X GET 'http://localhost:9200/my_test/_doc/_search' \

--header 'Content-Type: application/json' \

--data '{

  "query": {

      "bool": {

          "must": [

              {

                  "terms": {

                      "name": [ "名字1", "名字2" ]

                  }

              },

              {

                  "bool": {

                      "should": [

                          {

                              "term": {

                                  "status": 1

                              }

                          },

                          {

                              "term": {

                                  "status": 2

                              }

                          }

                      ],

                      "minimum_should_match": 1

                  }

              }

          ],

          "must_not": [

              {

                  "term": {

                      "type": 2

                  }

              }

          ],

          "filter": {

              "range": {

                  "created_at": {

                      "gte": "2024-01-01T00:00:00Z",

                      "lt": "2025-01-01T00:00:00Z"

                  }

              }

          }

      }

  }

}';

备注:

一、term与terms查询

1、使用场景:

term查询:适用于需要精确匹配单个值的场景。

terms查询:适用于需要精确匹配多个值的场景。

2、字段类型:

确保查询的字段类型是适合精确匹配的类型,例如 keyword 类型。对于 text 类型的字段,Elasticsearch 会进行分词处理,导致 term 和 terms 查询可能无法按预期工作。

3、大小写敏感:

term 和 terms 查询都是大小写敏感的。如果需要忽略大小写,可以在索引时对字段进行适当的分析,或者在查询时进行预处理。

二、在执行filter和query时,先执行filter逻辑再执行query逻辑

方式三、聚合条件匹配:

 

基础结构

分组

GET /索引名/_search

{

  "aggs": {

    "自定义标识": {

      "terms": {

        "field": "属性字段"

      }

    }

  }

}

最大值(max)

最小值(min)

平均值(avg)

求和(sum)

GET /索引名/_search

{

  "aggs": {

    "自定义标识": {

      "max": {

        "field": "属性字段"

      }

    }

  }

}

聚合

案例

GET /my_test/_search

{

  "query": {

    "term": {

      "type": {

        "value": "2"

      }

    }

  },

  "aggs": {

    "type_group": {

      "terms": {

        "field": "type"

      },

      "aggs": {

        "max_status": {

          "max": {

            "field": "status"

          }

        }

      }

    }

  }

}

等价SQL

SELECT MAX(`status`) FROM `my_test` WHERE type = 2 GROUP BY `type`;

备注:

text类型是不支持聚合的

4、更新数据:

(1)、先删除原始文档,在将更新文档以新的内容写入:

基础结构:

PUT /索引名/_doc/文档id

{

  "属性字段": "属性值"

}

案例:

Kibana

RESTful

PUT /my_test/_doc/5

{

  "status": 6

}

curl -X PUT 'http://localhost:9200/my_test/_doc/5' \

--header 'Content-Type: application/json' \

--data '{

  "status": 6

}';

(2)、将数据原始内容保存,并在此基础上进行更新(推荐):

基础结构:

POST /索引名/_doc/文档id/_update

{

    "doc" : {

        "属性字段": "属性值"

    }

}

案例:

Kibana

RESTful

POST /my_test/_doc/5/_update

{

    "doc" : {

        "status" : 6

    }

}

curl -X POST 'http://localhost:9200/my_test/_doc/5/_update' \

--header 'Content-Type: application/json' \

--data '{

    "doc" : {

        "status" : 6

    }

}';

5、删除数据:

(1)、删除索引:

基础结构:

DELETE /索引名

(2)、删除指定索引的指定文档id的数据:

基础结构:

DELETE /索引名/_doc/文档id

(3)、清空指定索引的所有数据:

基础结构:

POST /索引名/_delete_by_query

{

  "query": {

    "match_all": {}

  }

}

(4)、清理指定索引的fielddata缓存:

基础结构:

POST /索引名/_cache/clear?fielddata=true

(5)、清理所有索引的fielddata缓存:

基础结构:

POST /索引名/_delete_by_query

{

  "query": {

    "match_all": {}

  }

}

 

五、SpringBoot-ElasticSearch7整合:

添加依赖:

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

YML配置:

#es连接配置
elasticsearch-config:
  host: xxx.xxx.xxx.xxx
  port: 9200
  username: xxx
  password: xxx

连接ES配置:

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.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ES连接配置
 *
 */
@Configuration
public class ElasticSearchClientConfig {

    @Value("${elasticsearch-config.host}")
    private String host;
    @Value("${elasticsearch-config.port}")
    private String port;
    @Value("${elasticsearch-config.username}")
    private String username;
    @Value("${elasticsearch-config.password}")
    private String password;

    @Bean
    public RestHighLevelClient getEsClient() {
        RestHighLevelClient esClient = null;
        // 创建基本认证提供者并设置凭据
        final BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider();
        basicCredentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
        // 构建 RestHighLevelClient 实例
        esClient = new RestHighLevelClient(RestClient.builder(new HttpHost(host, Integer.parseInt(port)))
                .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
                    public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
                        // 禁用认证缓存
                        httpClientBuilder.disableAuthCaching();
                        // 设置异步 HTTP 客户端的连接数配置
                        httpClientBuilder.setMaxConnTotal(100);
                        httpClientBuilder.setMaxConnPerRoute(100);
                        // 设置默认凭证提供者
                        return httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider);
                    }
                }).setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
                        // 连接超时设置为 10 秒
                        .setConnectTimeout(10000)
                        // 数据传输超时设置为 300 秒
                        .setSocketTimeout(30000)
                        // 从连接池获取连接的超时设置为 20 秒
                        .setConnectionRequestTimeout(20000)
                )
        );
        return esClient;
    }

}

1、判断索引是否存在:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 判断索引是否存在
     *
     */
    @Test
    void isIndexExists() {
        try {
            GetIndexRequest getIndexRequest = new GetIndexRequest("my_test");
            getIndexRequest.humanReadable(true);
            boolean exists = esClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
            System.out.println("索引是否存在:" + exists);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

2、批量新建与更新ES数据操作:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 批量新建与更新ES数据操作:
     * 1、索引不存在则创建索引新增数据,建议提前自定义创建索引
     * 2、索引已存在,_id不存在,则新增数据
     * 3、索引已存在,_id存在,则更新已有_id数据,不替换
     * */
    @Test
    void batchCreateESInfoOpreate() {
        try {
            //索引名称
            String indexName = "my_test";
            BulkRequest request = new BulkRequest(indexName);

            List<UserInfo> list = new ArrayList<>();
            list.add(new UserInfo(1, "名字1", 1));
            for (UserInfo userInfo : list) {
                String dataJson = JSON.toJSONString(userInfo);
                IndexRequest indexReq = new IndexRequest().source(dataJson, XContentType.JSON);
                // 自定义_id,作为唯一标识
                indexReq = indexReq.id(userInfo.getId().toString());
                request.add(indexReq);
            }
            //批量提交
            BulkResponse bulkResponse = esClient.bulk(request, RequestOptions.DEFAULT);

            int successCount=0;
            for (BulkItemResponse bulkItemResponse : bulkResponse) {
                DocWriteResponse itemResponse = bulkItemResponse.getResponse();
                IndexResponse indexResponse = (IndexResponse) itemResponse;
                log.info("es 插入单条返回结果:{}", indexResponse);
                if (bulkItemResponse.isFailed()) {
                    log.info("es 批量插入返回错误{}", JSON.toJSONString(bulkItemResponse));
                }else {
                    successCount++;
                }
            }
            log.info("总数:" + list.size() + "失败总数:" + (list.size()-successCount) + "成功总数:" + successCount);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

3、查询ES数据操作:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 查询ES数据操作:
     * */
    @Test
    void getESInfoOpreate() {
        try {
            // 初始化查询条件
            SearchSourceBuilder builder = new SearchSourceBuilder();
            // 页码、页数、开启精确的总命中数统计
            builder.from(0).size(50).trackTotalHits(true);
            builder.sort("name", SortOrder.DESC);
            if (false) {
                // 简单查询
                builder.query(QueryBuilders.termQuery("type", 1));
            } else {
                // 复合查询
                BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
                List<String> nameList = Arrays.asList("名字1", "名字2");
                boolQueryBuilder.must(QueryBuilders.termsQuery("name", nameList));
                boolQueryBuilder.filter(QueryBuilders.rangeQuery("created_at")
                        .gte("2024-01-01T00:00:00Z").lt("2025-01-01T00:00:00Z"));
                builder.query(boolQueryBuilder);
            }
            SearchRequest searchRequest = new SearchRequest("my_test").source(builder);
            SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] hits = searchResponse.getHits().getHits();
            int totalHits = (int) searchResponse.getHits().getTotalHits().value;
            for (SearchHit hit : hits) {
                // 处理业务数据
                JSONObject esJsonObject = new JSONObject(hit.getSourceAsMap());
                log.info(esJsonObject.toString());
            }
            log.info("总数{}", totalHits);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

4、查询ES数据聚合(分组/最大值/最小值/均值/求和)操作:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 查询ES数据聚合(分组/最大值max/最小值min/均值avg/求和sum)操作
     * */
    @Test
    void getESInfoAggregationOpreate() {
        try {
            // 初始化查询条件
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(QueryBuilders.termQuery("type", 2)); //条件
            // 构建聚合查询
            TermsAggregationBuilder aggregation = AggregationBuilders.terms("type_group").field("type") //分组
                    .subAggregation(AggregationBuilders.max("max_status").field("status")); //最大值
            builder.aggregation(aggregation);
            SearchRequest searchRequest = new SearchRequest("my_test").source(builder);
            SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] hits = searchResponse.getHits().getHits();
            int totalHits = (int) searchResponse.getHits().getTotalHits().value;
            for (SearchHit hit : hits) {
                // 处理业务数据
                JSONObject esJsonObject = new JSONObject(hit.getSourceAsMap());
                log.info(esJsonObject.toString());
            }
            log.info("总数{}", totalHits);

            // 获取聚合结果
            Terms terms = searchResponse.getAggregations().get("type_group");
            for (Terms.Bucket bucket : terms.getBuckets()) {
                String key = bucket.getKeyAsString();
                Max maxStatus = bucket.getAggregations().get("max_status");
                log.info("key: {}, max_status的value: {}", key, maxStatus.getValue());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

5、批量删除ES文档操作:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 批量删除ES文档操作:
     * 根据_id进行删除,当数据存在时则删除,不存在时也不会报错,同时会被当作为成功记录
     * */
    @Test
    void batchDeleteESInfoOpreate() {
        try {
            //索引名称
            String indexName = "my_test";
            BulkRequest request = new BulkRequest(indexName);

            List<UserInfo> list = new ArrayList<>();
            list.add(new UserInfo(1, "名字1", 1));
            for (UserInfo userInfo : list) {
                DeleteRequest indexReq = new DeleteRequest(indexName);
                // 根据ID进行删除
                indexReq = indexReq.id(userInfo.getId().toString());
                request.add(indexReq);
            }
            //批量提交
            BulkResponse bulkResponse = esClient.bulk(request, RequestOptions.DEFAULT);

            int successCount=0;
            for (BulkItemResponse bulkItemResponse : bulkResponse) {
                DocWriteResponse itemResponse = bulkItemResponse.getResponse();
                DeleteResponse indexResponse = (DeleteResponse) itemResponse;
                log.info("es 删除单条返回结果:{}", indexResponse);
                if (bulkItemResponse.isFailed()) {
                    log.info("es 批量删除返回错误{}", JSON.toJSONString(bulkItemResponse));
                }else {
                    successCount++;
                }
            }
            log.info("总数:" + list.size() + "失败总数:" + (list.size()-successCount) + "成功总数:" + successCount);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

6、删除ES索引操作:

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 删除ES 索引操作
     *
     * */
    @Test
    void deleteESIndexOpreate() {
        try {
            //索引名称
            String indexName = "my_test";
            DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName);
            deleteIndexRequest.indicesOptions(IndicesOptions.LENIENT_EXPAND_OPEN);
            AcknowledgedResponse delete = esClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
            System.out.println("索引是否删除成功:" + delete.isAcknowledged());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

7、SearchAfter分页查询操作:

特性

From + Size

Search After

Scroll

推荐

分页查询的传统方式

分页查询的推荐方式

官方文档强调:不再建议使用Scroll API进行深度分页

原理

通过设置 from 来指定查询结果的起始位置,size 来控制每页返回的结果数

基于上一页返回的排序字段来查询下一页的结果,需要保证排序字段唯一,结合PIT(点时间)使用,可以保证在分页查询期间的数据不会因为数据的增删改而发生变化

基于查询上下文(游标)在 Elasticsearch 中存储游标状态,允许用户在不同请求中逐步获取数据,避免了每次查询时从头扫描数据的问题,在使用 scroll 查询时,通常会设置一个 scroll参数(例如 scroll=1m),表示游标有效期为 1 分钟

性能

随着 from 值增大,性能下降

性能好,适合深分页查询

性能较好,适合大数据量批量提取

资源消耗

较高,尤其是深分页时

资源消耗较低

高,需维持查询上下文

数据一致性

无一致性保证

可以结合 PIT(point in time) 保证一致性

保证一致性,适合长时间查询

易用性

简单直观,易于实现

需要排序字段和手动管理分页

较复杂,适合批量数据提取,需维护游标状态

适用场景

小数据量分页

深分页,尤其是大数据集

大数据量提取,数据迁移,日志分析等

    @Autowired
    private RestHighLevelClient esClient;

    /**
     * 基于SearchAfter分页查询操作
     * */
    @Test
    void searchAfterGetESInfoOpreate() {
        //初始查询,生成上下页的search_afters值, nextSearchAfters:名字4, previousSearchAfters:名字5
//        searchAfterOpreate("next", null);
        //下一页 nextSearchAfters:名字2,previousSearchAfters:名字3
//        searchAfterOpreate("next", new Object[]{"名字4"});  //传入上一次查询获得的search_afters值
        //上一页 nextSearchAfters:名字4, previousSearchAfters:名字5
        searchAfterOpreate("previous", new Object[]{"名字3"});    //传入上一次查询获得的search_afters值
    }

    /**
     * @param pageTurnType:
     *      分页类型,初始默认为next
     *      previous 上一页; next 下一页
     * @param searchAfters:
     *      参数由初始获取查询获取,初始默认为null
     *      previous时传入previousSearchAfters值,
     *      next时传入nextSearchAfters值,
     *
     * */
    private void searchAfterOpreate(String pageTurnType, Object[] searchAfters){
        try {
            // 初始化查询条件
            SearchSourceBuilder builder = new SearchSourceBuilder();
            if (true) {
                // 简单查询
                builder.query(QueryBuilders.termQuery("type", 1));
            } else {
                // 复合查询
                BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
                List<String> nameList = Arrays.asList("名字1", "名字2");
                boolQueryBuilder.must(QueryBuilders.termsQuery("name", nameList));
                boolQueryBuilder.filter(QueryBuilders.rangeQuery("created_at")
                        .gte("2024-01-01T00:00:00Z").lt("2025-01-01T00:00:00Z"));
                builder.query(boolQueryBuilder);
            }
            //分页
            //SearchAfter必须确保排序字段在所有文档中都是唯一的,避免适用_id作唯一索引,会造成fielddata飙升
            if("next".equals(pageTurnType) && !ObjectUtils.isEmpty(searchAfters)){
                //下一页
                builder.searchAfter(searchAfters);
                builder.sort("name", SortOrder.DESC);
            }else if("previous".equals(pageTurnType) && !ObjectUtils.isEmpty(searchAfters)){
                //上一页
                builder.searchAfter(searchAfters);
                builder.sort("name", SortOrder.ASC);
            }else {
                //倒叙排序
                builder.sort("name", SortOrder.DESC);
            }
            // 页数、开启精确的总命中数统计,from不设置
            builder.size(2).trackTotalHits(true);

            SearchRequest searchRequest = new SearchRequest("my_test").source(builder);
            SearchResponse searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] hits = searchResponse.getHits().getHits();
            //总数
            int totalHits = (int) searchResponse.getHits().getTotalHits().value;
            List<SearchHit> hitList = new ArrayList<>();
            if (!ObjectUtils.isEmpty(hits)) {
                hitList = Arrays.asList(hits);
            }
            // 如果是上一页请求,将结果倒序处理
            if ("previous".equals(pageTurnType)) {
                // 将结果反转
                Collections.reverse(hitList);
            }
            for (SearchHit hit : hitList) {
                // 处理业务数据
                JSONObject esJsonObject = new JSONObject(hit.getSourceAsMap());
                log.info(esJsonObject.toString());
            }
            if (hits.length > 0) {
                Object[] nextSearchAfters = searchResponse.getHits().getHits()[searchResponse.getHits().getHits().length - 1].getSortValues();
                log.info("获取下一页的search_after值nextSearchAfters:{}", nextSearchAfters);
                Object[] previousSearchAfters = searchResponse.getHits().getHits()[0].getSortValues();
                log.info("获取上一页的search_after值previousSearchAfters:{}", previousSearchAfters);
            }
            log.info("总数{}", totalHits);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

 

posted on 2024-12-13 01:07  爱文(Iven)  阅读(33)  评论(0编辑  收藏  举报

导航