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); } }