ElasticSearch 分布式搜索引擎 详解

1、ElasticSearch 概述

The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。Elaticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个 Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。

Elasticsearch 的官方地址:https://www.elastic.co/cn/

直接前往官网当中进行下载,下载完成之后直接进行解压缩即可,解压后,进入 bin 文件目录,点击 elasticsearch.bat 文件启动 ES 服务,解压后直接对9200端口进行访问验证。
ElasticSearch 分布式搜索引擎 详解_第1张图片
2、ElasticSearch之HTTP 操作

2.1、索引操作

2.1.1、新增索引
对比关系型数据库,创建索引就等同于创建数据库,在 Postman 中,向 ES 服务器发 PUT 请求:http://localhost:9200/user
ElasticSearch 分布式搜索引擎 详解_第2张图片
2.1.2、查看所有索引
在 Postman 中,向 ES 服务器发 GET 请求 :localhost:9200/_cat/indices?v
ElasticSearch 分布式搜索引擎 详解_第3张图片
2.1.3、查看单个索引
查看索引向 ES 服务器发送的请求路径和创建索引是一致的。但是 HTTP 方法不一致,改为GET请求
ElasticSearch 分布式搜索引擎 详解_第4张图片
2.1.4、删除索引
改为DELETE请求
ElasticSearch 分布式搜索引擎 详解_第5张图片
2.2、文档操作

2.2.1、创建文档
这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式
在 Postman 中,向 ES 服务器发 POST 请求 :http://localhost:9200/user/_doc
ElasticSearch 分布式搜索引擎 详解_第6张图片
但是这里返回的数据当中的id是es生成的,那我们要使用自己定义的id又如何设置呢?可以在创建时指定,在最后给定id值,如下的1
ElasticSearch 分布式搜索引擎 详解_第7张图片
2.2.2、查看文档
查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询,在 Postman 中,向 ES 服务器发 GET 请求 :http://localhost:9200/user/_doc/1
ElasticSearch 分布式搜索引擎 详解_第8张图片
2.2.3、修改文档
输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖在 Postman 中,向 ES 服务器发 POST 请求,而当我们发出PUT请求回对所有数据进行覆盖
ElasticSearch 分布式搜索引擎 详解_第9张图片
2.2.4、修改字段
修改数据时,也可以只修改某一给条数据的局部信息
ElasticSearch 分布式搜索引擎 详解_第10张图片
2.2.5、删除文档
删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。在 Postman 中,向 ES 服务器发 DELETE 请求
ElasticSearch 分布式搜索引擎 详解_第11张图片
2.3、高级查询

2.3.1、条件查询文档
通过get请求http://localhost:9200/user/_search,通过body体进行查询

{
    "query":{
        "match":{
            "sex":"男"
        }
    }
}

ElasticSearch 分布式搜索引擎 详解_第12张图片
并且我们可以进行设置from(起始位置)和size(每页条数)进行设置分页查询,_source(查指定字段)、sort(根据指定字段排序)
ElasticSearch 分布式搜索引擎 详解_第13张图片
2.3.2、范围查询
range 查询找出那些落在指定区间内的数字或者时间。range 查询允许以下字符

操作符 说明
gt 大于>
gte 大于等于>=
lt 小于<
lte 小于等于<=

如下对age进行范围查询
ElasticSearch 分布式搜索引擎 详解_第14张图片
3、ElasticSearch之Java API操作

首先我们创建一个maven项目,之后我们导入es所需要的依赖。

    <dependencies>
        <dependency>
            <groupId>org.elasticsearchgroupId>
            <artifactId>elasticsearchartifactId>
            <version>7.15.0version>
        dependency>
        
        <dependency>
            <groupId>org.elasticsearch.clientgroupId>
            <artifactId>elasticsearch-rest-high-level-clientartifactId>
            <version>7.15.0version>
        dependency>
        
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-apiartifactId>
            <version>2.8.2version>
        dependency>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-coreartifactId>
            <version>2.8.2version>
        dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.coregroupId>
            <artifactId>jackson-databindartifactId>
            <version>2.9.9version>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
    dependencies>

3.1、创建索引

    public static void main(String[] args) throws Exception {
        // 创建客户端
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost",9200,"http"))
        );
        // 创建索引
        CreateIndexRequest request = new CreateIndexRequest("order");
        CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);

        boolean isAcknowledged = response.isAcknowledged();

        System.out.println(isAcknowledged);
        // 关闭
        client.close();
    }

报错 ElasticsearchStatusException[Elasticsearch exception [type=invalid_index_name_exception, reason=Invalid index name [esIndex], must be lowercase]] 这个报错表示索引需使用小写,改为小写即可。
报错 java.lang.ClassNotFoundException: org.elasticsearch.common.CheckedConsumer 这个是因为es的版本不对,将对应的版本以及clinent版本调整为一致即可。

3.2、查看索引

        // 创建客户端
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost",9200,"http"))
        );
        // 查看索引
        GetIndexRequest request = new GetIndexRequest("order");
        GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);

        System.out.println(response.getAliases());
        System.out.println(response.getMappings());
        System.out.println(response.getSettings());

        // 关闭
        client.close();

3.3、删除索引

        // 删除索引
        DeleteIndexRequest request = new DeleteIndexRequest("order");
        AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);

3.4、新增文档

        // 修改数据
        UpdateRequest updateRequest = new UpdateRequest();
        updateRequest.index("order").id("0001");
        updateRequest.doc(XContentType.JSON,"price",9999.98);
        UpdateResponse response = client.update(updateRequest,RequestOptions.DEFAULT);

3.5、修改文档

        // 修改数据
        UpdateRequest updateRequest = new UpdateRequest();
        updateRequest.index("order").id("0001");
        updateRequest.doc(XContentType.JSON,"price",9999.98);
        UpdateResponse response = client.update(updateRequest,RequestOptions.DEFAULT);

3.6、查看文档

        // 查看数据
        GetRequest indexRequest = new GetRequest();
        indexRequest.index("order").id("0001");

        GetResponse response = client.get(indexRequest,RequestOptions.DEFAULT);

3.7、删除文档

        // 查看数据
        DeleteRequest indexRequest = new DeleteRequest();
        indexRequest.index("order").id("0001");

        DeleteResponse response = client.delete(indexRequest,RequestOptions.DEFAULT);

3.8、批量新增文档

        // 创建数据
        BulkRequest indexRequest = new BulkRequest();

        indexRequest.add(new IndexRequest().index("order").id("1001").source(XContentType.JSON,"name","笔"));
        indexRequest.add(new IndexRequest().index("order").id("1002").source(XContentType.JSON,"name","墨"));
        indexRequest.add(new IndexRequest().index("order").id("1003").source(XContentType.JSON,"name","纸"));
        indexRequest.add(new IndexRequest().index("order").id("1004").source(XContentType.JSON,"name","砚"));

        BulkResponse response = client.bulk(indexRequest, RequestOptions.DEFAULT);

3.9、批量删除文档

        // 创建数据
        BulkRequest indexRequest = new BulkRequest();

        indexRequest.add(new DeleteRequest().index("order").id("1001"));
        indexRequest.add(new DeleteRequest().index("order").id("1002"));
        indexRequest.add(new DeleteRequest().index("order").id("1003"));
        indexRequest.add(new DeleteRequest().index("order").id("1004"));

        BulkResponse response = client.bulk(indexRequest, RequestOptions.DEFAULT);

3.10、全量、条件查询文档

        // 全量查询
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("order");

        // 全量
        //searchRequest.source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()));

        // 条件查询
        searchRequest.source(new SearchSourceBuilder().query(QueryBuilders.termQuery("price",49.99)));

        SearchResponse response = client.search(searchRequest,RequestOptions.DEFAULT);

3.11、分页、排序、字段选择查询文档

        // q全量查询
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("order");

        SearchSourceBuilder  builder =new SearchSourceBuilder().query(QueryBuilders.matchAllQuery());

        // 分页
        builder.from(0);
        builder.size(10);
        // 排序
        builder.sort("price", SortOrder.DESC);
        // 过滤字段
        String [] exclude = {};
        String [] include = {"name"};
        builder.fetchSource(include,exclude);
        searchRequest.source(builder);

3.12、组合查询文档

        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost",9200,"http"))
        );

        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("order");

        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        boolQueryBuilder.must(QueryBuilders.matchQuery("price",49.99));
        // boolQueryBuilder.must(QueryBuilders.matchQuery("name","纸"));
        boolQueryBuilder.should(QueryBuilders.matchQuery("name","砚"));
        builder.query(boolQueryBuilder);

        searchRequest.source(builder);

3.12、范围查询文档

        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost",9200,"http"))
        );

        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("order");

        SearchSourceBuilder builder = new SearchSourceBuilder();

        RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price");
        rangeQueryBuilder.gte(20);
        rangeQueryBuilder.lte(1000);

        builder.query(rangeQueryBuilder);
        searchRequest.source(builder);

3.13、模糊查询文档

        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.fuzzyQuery("name", "笔").fuzziness(Fuzziness.ONE));
        searchRequest.source(builder);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);

4、ElasticSearch 集群搭建

4.1、Windows下集群搭建

在Windows下进行集群搭建,首先修改conf文件夹下的 elasticsearch.yml,将这个作为第一个节点,

# 集群名称
cluster.name: my-application

# 节点信息
node.name: node-1
node.master: true
node.data: true

# 跨域配置
http.cors.enabled: true
http.cors.allow-origin: "*"

# ip地址
network.host: localhost
# http端口
http.port: 9201
# tcp监听端口
transport.tcp.port: 9301

而后复制一份作为节点二,节点二的配置需要新增配置:

# 去前一个结点找
discovery.seed_hosts: ["localhost:9301"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5

分别启动节点一和节点二。访问地址:http://localhost:9201/_cluster/health

4.2、Linux下单机服务搭建

首先下载好对应的gz文件上传到linux服务器上。首先因为安全问题,Elasticsearch 不允许 root 用户直接运行,所以要创建新用户,在 root 用户中创建新用户:

useradd es #新增 es 用户
passwd es #为 es 用户设置密码
userdel -r es #如果错了,可以删除再加
chown -R es:123456 /tools/es #文件夹所有者

修改/tools/es/es-node1/config/elasticsearch.yml 文件

cluster.name: elasticsearch
node.name: node-1
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]

修改/etc/security/limits.conf

# 每个进程可以打开的文件数的限制
es soft nofile 65536
es hard nofile 65536

修改/etc/security/limits.d/20-nproc.conf

# 每个进程可以打开的文件数的限制
es soft nofile 65536
es hard nofile 65536
# 操作系统级别对每个用户创建的进程数的限制
* hard nproc 4096

修改/etc/sysctl.conf

# 一个进程可以拥有的 VMA(虚拟内存区域)的数量,默认值为 65536
vm.max_map_count=655360

最后进行启动es:bin/elasticsearch 使用bin目录下的elasticsearch启动即可。

启动常出现的问题:

1、max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

每个进程最大同时打开文件数太小,可通过下面2个命令查看当前数量

ulimit -Hn
ulimit -Sn

vi /etc/security/limits.conf文件,增加配置,用户退出后重新登录生效

*               soft    nofile          65536
*               hard    nofile          65536

2、max number of threads [3818] for user [es] is too low, increase to at least [4096]

问题同上,最大线程个数太低。修改配置文件/etc/security/limits.conf(和问题1是一个文件),增加配置

*               soft    nproc           4096
*               hard    nproc           4096

可通过命令查看

ulimit -Hu
ulimit -Su

3、启动之后进行访问9200端口显示无法访问

这个的问题也就是防火墙没有关闭,将防火墙关闭即可:

# 查看防火墙状态
systemctl status firewalld

# 暂时关闭防火墙
systemctl stop firewalld

# 永久关闭防火墙
systemctl enable firewalld.service
systemctl disable firewalld.service

启动后直接访问9200端口进行验证:

ElasticSearch 分布式搜索引擎 详解_第15张图片
4.3、Linux下集群搭建

这里还是使用伪分布式来进行构建集群,首先修改conf下的elasticsearch.yml文件,将第一个节点作为一个master节点,里面的配置根据自身的linux集群进行修改即可。(比如说IP地址)这里在同一台服务器上启动三个es服务,只需要修改节点名称端口号即可。

#集群名称
cluster.name: cluster-es
#节点名称,每个节点的名称不能重复
node.name: node1
#ip 地址,每个节点的地址不能重复
network.host: 0.0.0.0
network.publish_host: 192.168.101.128
http.port: 9201
transport.tcp.port: 9301
#es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["192.169.101.128:9301","192.169.101.128:9302","192.169.101.128:9303"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举 master
cluster.initial_master_nodes: ["node1","node2","node3"]
gateway.recover_after_nodes: 2
http.cors.enabled: true
http.cors.allow-origin: "*"

其余的配置和当前节点一致,之后就可以直接进行启动这个该节点,其余两个节点也进行启动,启动之后可以分别进行访问对应的端口号 9201、9202、9203,可以看到集群搭建好之后所有的信息都是相同的,cluster_uuid是每个节点的id,这个是不同的。

集群节点总是kill
然后在这里启动es服务器,总是启动到一半就被killed掉,发现是内存不够了,由于ES是运行在JVM上,JVM本身除了分配的heap内存以外,还会用到一些堆外(off heap)内存。 在小内存的机器上跑ES,如果heap划分过多,累加上堆外内存后,总的JVM使用内存量可能超过物理内存限制。 如果swap又是关闭的情况下,就会被操作系统oom killer杀掉。后续原来是本身机器内存不够大,只有1G内存,而es中的jvm配置文件中,就配置了1g的堆大小,导致没有足够空间分配,故es启动不起来。,更改 jvm.options文件:

-Xmx512m
-Xms512m

4.4、ElasticSreach-head 工具

github地址:https://github.com/mobz/elasticsearch-head

npm install
npm run start

启动项目之后直接访问对应的地址,首先我们连接到我们的集群就可以看到集群的相关信息了。

5、ElasticSearch 分布式集群

5.1、单节点集群

在包含一个空节点的集群内创建名为 users 的索引,将分配 3个主分片和一份副本(每个主分片拥有一个副本分片)

PUT请求			http://localhost:9201/user

{
	"settings" : {
		"number_of_shards" : 3,
		"number_of_replicas" : 1
	}
}

集群现在是拥有一个索引的单节点集群。3个主分片都被分配在node1,通过elasticSearch-head工具可以进行查看集群的分片状态。

ElasticSearch 分布式搜索引擎 详解_第16张图片
5.2、故障转移

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。这个时候,我们只需再启动一个节点即可防止数据丢失。只需要在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。如果启动了第二个节点,集群将会拥有两个节点的集群 : 所有主分片和副本分片都已被分配。

ElasticSearch 分布式搜索引擎 详解_第17张图片
5.3、水平扩容

这个时候当启动了第三个节点,集群将会拥有三个节点的集群 : 这时会为了分散负载而对分片进行重新分配。

这时当我们想要扩容超过6个节点又该如何处理呢?

主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储的最大数据量。但是,读操作搜索和返回数据可以同时被主分片或副本分片所处理,所以当拥有越多的副本分片时,也将拥有越高的吞吐量。在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。

{
	"number_of_replicas" : 2
}

5.4、应对故障(节点宕机)

当主节点突然宕机不可用。这时的集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。在我们node1节点宕机的同时也失去了主分片1和2,并且在缺失主分片的时候索引也不能正常工作。 此时集群的状态将会变为red即不是所有主分片都在正常工作。

不过在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为yellow。这个提升主分片的过程是瞬间发生的,所以没有办法在head上看到red的状态。

5.5、路由计算

当索引一个文档的时候,文档会被存储到一个主分片中。 es是如何把一个文档应该存放到哪个分片中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是文档所在分片的位置。

5.6、分片控制

5.6.1、写流程

新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片

  1. 客户端向 Node1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片0。请求会被转发到 Node 3,因为分片0的主分片目前被分配在 Node3上。
  3. Node3在主分片上面执行请求。如果成功了,它将请求并行转发到 Node1和Node2的副本分片上。一旦所有的副本分片都报告成功, Node3将向协调节点报告成功,协调节点向客户端报告成功。

5.6.2、读流程

我们可以从主分片或者从其它任意副本分片检索文档

  1. 客户端向 Node1发送获取请求。
  2. 节点使用文档的 _id 来确定文档属于分片0。分片0的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node2。
  3. Node2将文档返回给Node1,然后将文档返回给客户端

5.6.3、更新流程

  1. 客户端向Node1发送更新请求。
  2. 它将请求转发到主分片所在的Node3 。
  3. Node 3 从主分片检索文档,修改 _source 字段中的JSON,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤3 ,超过retry_on_conflict次后放弃。
  4. 如果Node3成功地更新文档,它将新版本的文档并行转发到Node1和Node2上的副本分片,重新建立索引。一旦所有副本分片都返回成功, Node3向协调节点也返回成功,协调节点向客户端返回成功。

5.7、分片原理

分片是ES最小的工作单元。传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。

5.8、倒排索引

ES使用一种称为倒排索引的结构,它适用于快速的全文搜索。有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成 K-V 对,然后对关键字进行统计计数

5.9、Kibana

Kibana 是一个免费且开放的用户界面,能够让你对 Elasticsearch 数据进行可视化,并让你在 Elastic Stack 中进行导航。你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases

下载地址:https://www.elastic.co/cn/downloads/past-releases#kibana 这里一定要装与ES相同的版本

下载之后进行解压,修改kibana.yml配置文件

server.port: 5601
elasticsearch.hosts: ["http://localhost:9201"]
kibana.index : ".kibana"
i18n.locale: "zh-CN"

先启动Es,之后启动kibana(在bin目录下启动)。

6、Spring 集成 ElasticSearch

6.1、项目初始化

首先这里还是先创建一个Maven项目,后续的依赖配置再往里面添加就行。或者也可以直接使用springboot快速初始化,这里先导入相关依赖。

		
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-elasticsearchartifactId>
        dependency>

添加相对应的配置:

server.port=8989
elasticsearch.host=127.0.0.1
elasticsearch.port=9200
logging.level.com.lzq=debug

在这里添加一个实体类,用来做ES的索引映射

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Document(indexName = "product", shards = 3, replicas = 1)
public class Product {
    @Id
    private Long id;
    /**
     * type : 字段数据类型
     * analyzer : 分词器类型
     * index : 是否索引(默认:true)
     * Keyword : 短语,不进行分词
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title;
    @Field(type = FieldType.Keyword)
    private String category;
    @Field(type = FieldType.Double)
    private Double price;
    @Field(type = FieldType.Keyword, index = false)
    private String images;
}

添加一个Dao接口,继承至ElasticsearchRepository类,ElasticsearchRepository类封装了一系列对ES操作的方法,这里的这个就类似于Mybatis-Plus的BaseMapper一样。

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

最后添加一个配置类

  • ElasticsearchRestTemplate 是 spring-data-elasticsearch 项目中的一个类,和其他 spring 项目中的 template类似。
  • 在新版的 spring-data-elasticsearch 中,ElasticsearchRestTemplate 代替了原来的 ElasticsearchTemplate。原因是 ElasticsearchTemplate 基于 TransportClient,TransportClient 即将在 8.x 以后的版本中移除。
  • ElasticsearchRestTemplate 基 于 RestHighLevelClient 客户端的。需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现 elasticsearchClient()抽象方法,创建 RestHighLevelClient 对象。
@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Data
public class EsConfig extends AbstractElasticsearchConfiguration {
    private String host;
    private Integer port;

    @Override
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        RestHighLevelClient restHighLevelClient = new
                RestHighLevelClient(builder);
        return restHighLevelClient;
    }
}

6.2、索引操作

在这里就直接使用springBoot提供的test测试来进行测试,

6.2.1、创建索引
这里直接启动服务,或者通过这里的Test对程序进行编译,这里的索引会通过实体类进行创建,创建完成之后可以通过:http://localhost:9200/_cat/indices?v 进行查看所有索引

    @Test
    public void createIndex(){
        //创建索引,系统初始化会自动创建索引
        System.out.println("创建索引");
    }

6.2.2、删除索引
在这里需要先将ElasticsearchRestTemplate 这个模板进行注入,之后的操作都可以直接通过这个模板封装的方法进行操作。

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    @Test
    public void deleteIndex(){
        boolean flg = elasticsearchRestTemplate.deleteIndex(Product.class);
        System.out.println("删除索引 = " + flg);
    }

6.3、文档操作

这里的文档操作还是使用到ProductDao这个接口实现的父类提供的方法来进行操作。

    @Autowired
    private ProductDao productDao
    @Test
    public void insert(){
        Product product = new Product();
        product.setId(1L);
        product.setCategory("miao shu");
        product.setImages("image address");
        product.setPrice(50.0);
        product.setTitle("title 1");
        // 新增和修改使用的是同样的方法。判断id是否存在自动进行新增修改
        productDao.save(product);
    }

    @Test
    public void find(){
        Product product = productDao.findById(1L).get();
        System.out.println(product);
    }

    @Test
    public void findAll(){
        Iterable<Product> all = productDao.findAll();
        for (Product product : all){
            System.out.println(product);
        }
    }

    @Test
    public void delete(){
        Product product = new Product();
        product.setId(2L);
        productDao.delete(product);
    }

    @Test
    public void insertAll(){
        List<Product> productList = new ArrayList<>();
        for (int i =0;i<10;i++){
            Product product = new Product();
            product.setId(Long.valueOf(i));
            product.setTitle("title["+i+"]");
            product.setCategory("category"+i);
            product.setPrice(1999.0+i);
            product.setImages("image"+i);
            productList.add(product);
        }
        productDao.saveAll(productList);
    }

    @Test
    public void page(){
        Sort sort = Sort.by(Sort.Direction.DESC,"id");
        int currentPage=0;
        int pageSize = 5;
        //设置查询分页
        PageRequest pageRequest = PageRequest.of(currentPage, pageSize,sort);
        Page<Product> productPage = productDao.findAll(pageRequest);
        for (Product product : productPage.getContent()) {
            System.out.println(product);
        }
    }

6.4、文档搜索

    @Test
    public void termQuery() {
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", "5");
        Iterable<Product> products = productDao.search(termQueryBuilder);
        for (Product product : products) {
            System.out.println(product);
        }
    }

    @Test
    public void termQueryByPage() {
        int currentPage = 0;
        int pageSize = 5;
        //设置查询分页
        PageRequest pageRequest = PageRequest.of(currentPage, pageSize);
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", "title");
        Iterable<Product> products = productDao.search(termQueryBuilder, pageRequest);
        for (Product product : products) {
            System.out.println(product);
        }
    }

7、ElasticSearch 优化

7.1、硬件选择

Elasticsearch 的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,磁盘能处理的吞吐量越大,节点就越稳定。

7.2、分片策略

7.2.1、合理设置分片数

分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不表示分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,是不能重新修改分片数的。

并且这里的分片也不是越大越好的,这是因为:

  • 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转
  • 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就会裂开

所以这里对于分片也需要进行设置调整

  • 控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32G,参考下文的 JVM 设置原则),因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。
  • 考虑node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的 3 倍。
  • 主分片,副本和节点最大数之间数量,分配的时候可以参考以下关系:节点数<=主分片数*(副本数+1)

7.2.2、推迟分片分配

对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。通过修改参数 delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引
级别进行修改:

PUT /_all/_settings 
{
	"settings": {
		"index.unassigned.node_left.delayed_timeout": "5m" 
	}
}

7.3、路由选择

当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:routing 默认值是文档的 id,也可以采用自定义值,比如用户 id

shard = hash(routing) % number_of_primary_shards

不带 routing 查询

在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤

  • 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
  • 聚合: 协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。

带 routing 查询

查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。

7.4、写入速度优化

7.4.1、批量数据提交

ES 提供了 Bulk API 支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。
通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。

7.4.2、合理使用合并

Lucene 以段的形式存储数据。当有新的数据写入索引时,Lucene 就会自动创建一个新的段。随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及 CPU 就越多,查询效率就会下降。由于 Lucene 段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行段合并

7.4.3、减少 Refresh 的次数

Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval 为1 秒。Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh,然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。如果我们对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap 内存。

7.4.4、加大 Flush 设置

Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到512MB 或者 30 分钟时,会触发一次 Flush。index.translog.flush_threshold_size 参数的默认值是 512MB,进行修改。增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。

7.4.5、减少副本的数量

ES 为了保证集群的可用性,提供了 Replicas(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。如果需要大批量进行写入操 作,可 以先禁止Replica复 制,设 置index.number_of_replicas: 0 关闭副本。在写入完成后,Replica 修改回正常的状态。

7.5、内存设置

ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。在 ES 安装文件中包含一个 jvm.option 文件,添加如下命令来设置 ES 的堆大小,Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。

ES堆内存的分配需要满足以下两个原则:

  • 不要超过物理内存的 50%:
  • 堆内存的大小最好不要超过 32GB:

7.6、配置说明

参数名 参数值 说明
cluster.name elasticsearch 配置 ES 的集群名称,默认值是 ES,建议改成与所存数据相关的名称,ES 会自动发现在同一网段下的集群名称相同的节点
node.name node-1 集群中的节点名,在同一个集群中不能重复。节点的名称一旦设置,就不能再改变了。当然,也可以设 置 成 服 务器的主机名 称 , 例 如node.name:${HOSTNAME}。
node.master true 指定该节点是否有资格被选举成为 Master 节点,默认是 True,如果被设置为 True,则只是有资格成为Master 节点,具体能否成为 Master 节点,需要通过选举产生。
node.data true 指定该节点是否存储索引数据,默认为 True。数据的增、删、改、查都是在 Data 节点完成的。
index.number_of_shards 1 设置都索引分片个数,默认是 1 片。也可以在创建索引时设置该值,具体设置为多大都值要根据数据量的大小来定。如果数据量不大,则设置成 1 时效率最高
index.number_of_replicas 1 设置默认的索引副本个数,默认为 1 个。副本数越多,集群的可用性越好,但是写索引时需要同步的数据越多。
transport.tcp.compress true 设置在节点间传输数据时是否压缩,默认为 False,不压缩
discovery.zen.minimum_master_nodes 1 设置在选举 Master 节点时需要参与的最少的候选主节点数,默认为 1。如果使用默认值,则当网络不稳定时有可能会出现脑裂。合理的数值为 (master_eligible_nodes/2)+1 ,其中master_eligible_nodes 表示集群中的候选主节点数
discovery.zen.ping.timeout 3s 设置在集群中自动发现其他节点时 Ping 连接的超时时间,默认为 3 秒。在较差的网络环境下需要设置得大一点,防止因误判该节点的存活状态而导致分片的转移

8、ElasticSearch 总结

8.1、为什么要用 ES

系统中的数据,随着业务的发展,时间的推移,将会非常多,而业务中往往采用模糊查询进行数据的搜索,而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段作为ES的索引进行查询。

8.2、ES master节点选举流程

  • Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分
  • 对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。
  • 如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。
  • master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http功能。

8.3、ES 集群脑裂的原因和解决方案

出现脑裂的原因:

  • 网络问题:集群间的网络延迟导致一些节点访问不到 master,认为 master 挂掉了从而选举出新的master,并对 master 上的分片和副本标红,分配新的主分片
  • 节点负载:主节点的角色既为 master 又为 data,访问量较大时可能会导致 ES 停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  • 内存回收:data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。

解决方案

  • 减少误判:discovery.zen.ping_timeout 节点状态的响应时间,默认为 3s,可以适当调大,如果 master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6),可适当减少误判。
  • 选举触发: discovery.zen.minimum_master_nodes:该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(n/2)+1,n 为主节点个数(即有资格成为主节点的节点个数)
  • 角色分离:即 master 节点与 data 节点分离,限制角色
    主节点配置为:node.master: true node.data: false
    从节点配置为:node.master: false node.data: true

8.4、ES 查询文档流程

  1. 协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片:

    shard = hash(document_id) % (num_of_primary_shards)

  2. 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 Memory Buffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 Memory Buffer 到 Filesystem Cache 的过程就叫做 refresh;

  3. 当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush;

  4. 在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync 将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。

  5. flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时;

8.5、ES 更新删除文档流程

  1. 删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;
  2. 磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。
  3. 在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

8.6、在并发情况下,ES 数据如果保证读写一致

  1. 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;
  2. 另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。
  3. 对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数_preference 为 primary 来查询主分片,确保文档是最新版本。

你可能感兴趣的