Docker搭建Elasticsearch集群、整合Springboot

1、全文搜索

1.1、倒排索引

倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。
正排索引:
在这里插入图片描述
转化成倒排索引:
在这里插入图片描述
说明:“单词ID”一栏记录了每个单词的单词编号;第二栏是对应的单词;第三栏即每个单词对应的倒排列表。比如单词“谷歌”,其单词编号为1,倒排列表为{1,2,3,4,5},说明文档集合中每个文档都包含了这个单词。
而事实上,索引系统还可以记录除此之外的更多信息,在单词对应的倒排列表中不仅记录了文档编号,还记载了单词频率信息(TF),即这个单词在某个文档中的出现次数,之所以要记录这个信息,是因为词频信息在搜索结果排序时,计算查询和文档相似度是很重要的一个计算因子,所以将其记录在倒排列表中,以方便后续排序时进行分值计算。
在这里插入图片描述
倒排索引还可以记载更多的信息,除了记录文档编号和单词频率信息外,额外记载了两类信息,即每个单词对应的“文档频率信息”,以及在倒排列表中记录单词在某个文档出现的位置信息。
在这里插入图片描述

1.2、全文搜索

全文搜索两个最重要的方面是:
相关性(Relevance) 它是评价查询与其结果间的相关程度,并根据这种相关程度对结果排名的一种能力,这种计算方式可以是 TF/IDF 方法、地理位置邻近、模糊相似,或其他的某些算法。
分析(Analysis) 它是将文本块转换为有区别的、规范化的 token 的一个过程,目的是为了创建倒排索引以及查询倒排索引。

2、Elasticsearch集群

2.1、集群节点

ELasticsearch的集群是由多个节点组成的,通过cluster.name设置集群名称,并且用于区分其它的集群,每个节点通过node.name指定节点的名称。
在Elasticsearch中,节点的类型主要有4种:

  • master节点
    配置文件中node.master属性为true(默认为true),就有资格被选为master节点。master节点用于控制整个集群的操作。比如创建或删除索引,管理其它非master节点等。
  • data节点
    配置文件中node.data属性为true(默认为true),就有资格被设置成data节点。data节点主要用于执行数据相关的操作。比如文档的CRUD。
  • 客户端节点
    配置文件中node.master属性和node.data属性均为false。该节点不能作为master节点,也不能作为data节点。可以作为客户端节点,用于响应用户的请求,把请求转发到其他节点。
  • 部落节点
    当一个节点配置tribe.*的时候,它是一个特殊的客户端,它可以连接多个集群,在所有连接的集群上执行搜索和其他操作。
2.2、使用docker搭建集群
mkdir /haoke/es-cluster
cd /haoke/es-cluster
mkdir node01
mkdir node02
#复制安装目录下的elasticsearch.yml、jvm.options文件,做如下修改
#node01的配置:
cluster.name: es-itcast-cluster
node.name: node01
node.master: true
node.data: true
network.host: 192.168.55.105
http.port: 9200
discovery.zen.ping.unicast.hosts: ["192.168.55.105"]
discovery.zen.minimum_master_nodes: 1
http.cors.enabled: true
http.cors.allow-origin: "*"
#node02的配置:
cluster.name: es-itcast-cluster
node.name: node02
node.master: false
node.data: true
network.host: 192.168.55.105
http.port: 9201
discovery.zen.ping.unicast.hosts: ["192.168.55.105"]
discovery.zen.minimum_master_nodes: 1
http.cors.enabled: true
http.cors.allow-origin: "*"
#创建容器
docker create --name es-node01 --net host -v /haoke/es-
cluster/node01/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
-v /haoke/es-cluster/node01/jvm.options:/usr/share/elasticsearch/config/jvm.options
-v /haoke/es-cluster/node01/data:/usr/share/elasticsearch/data elasticsearch:6.5.4
docker create --name es-node02 --net host -v /haoke/es-
cluster/node02/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
-v /haoke/es-cluster/node02/jvm.options:/usr/share/elasticsearch/config/jvm.options
-v /haoke/es-cluster/node02/data:/usr/share/elasticsearch/data elasticsearch:6.5.4
#启动容器
docker start es-node01 && docker logs -f es-node01
docker start es-node02 && docker logs -f es-node02
#提示:启动时会报文件无权限操作的错误,需要对node01和node02进行chmod 777 的操作

查询集群状态:http://192.168.55.105:9200/_cluster/health

{
  "cluster_name": "es-itcast-cluster",
  "status": "green",
  "timed_out": false,
  "number_of_nodes": 2,
  "number_of_data_nodes": 2,
  "active_primary_shards": 5,
  "active_shards": 10,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 0,
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks": 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 100
}

集群状态的三种颜色:green 所有主要分片和复制分片都可用;yellow 所有主要分片可用,但不是所有复制分片都可用;red 不是所有的主要分片都可用。
Elasticsearch入门及安装包方式搭建集群

2.3、分片和副本

为了将数据添加到Elasticsearch,我们需要索引(index)——一个存储关联数据的地方。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)”.
一个分片(shard)是一个最小级别“工作单元(worker unit)”,它只是保存了索引中所有数据的一部分。我们需要知道是分片就是一个Lucene实例,并且它本身就是一个完整的搜索引擎。应用程序不会和它直接通信。
分片可以是主分片(primary shard)或者是复制分片(replica shard)。索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据。复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的shard取回文档。
当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。

2.4、故障转移

为了测试故障转移,需要再向集群中添加一个节点,并且将所有节点的node.master设置为true。

docker create --name es-node03 --net host -v /haoke/es-
cluster/node03/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v
/haoke/es-cluster/node03/jvm.options:/usr/share/elasticsearch/config/jvm.options -v
/haoke/es-cluster/node03/data:/usr/share/elasticsearch/data elasticsearch:6.5.4
docker stop es-node01 es-node02
docker start es-node01 es-node02 es-node03

查看集群状态:此时,node02为主节点。
创建test索引:6分片1副本
将node01停止:docker stop es-node01
连接到node2的端口9201进行查看状态:当前集群状态为黄色,表示主节点可用,副本节点不完全可用。过一段时间观察,发现节点列表中看不到node01,副本节点分配到了node02和node03,集群状态恢复到绿色。
将node01恢复:docker start es-node01 ;node01恢复后,重新加入了集群,并且重新分配了节点信息。

将主节点node02停止:docker stop es-node02
连接到node1的端口9201进行查看状态:集群对master进行了重新选举,选择node01为master。并且集群状态变成黄色。过一段时间观察,集群状态恢复到绿色。
恢复node02节点:docker start es-node02 ;重启之后,发现node02加不到集群中了。这其实是集群中脑裂问题。
在这里插入图片描述
解决方案:设置minimum_master_nodes的大小为2,官方推荐:(N/2)+1,N为集群中节点数。思路:不能让节点很容易的变成master,必须有多个节点认可后才可以。

2.5、分布式文档
2.5.1、路由

首先,来看个问题:
在这里插入图片描述
如图所示:当我们想一个集群保存文档时,文档该存储到哪个节点呢? 是随机吗? 是轮询吗?实际上,在ELasticsearch中,会采用计算的方式来确定存储到哪个节点,计算公式如下:

shard = hash(routing) % number_of_primary_shards
routing值是一个任意字符串,它默认是_id但也可以自定义。
这个routing字符串通过哈希函数生成一个数字,然后除以主切片的数量得到一个余数(remainder),余数的范围永远是0到number_of_primary_shards - 1,这个数字就是特定文档所在的分片。
这就是为什么创建了主分片后,不能修改的原因。

2.5.2、文档的写操作

新建、索引和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上。
在这里插入图片描述
在主分片和复制分片上成功新建、索引或删除一个文档必要的顺序步骤:

  1. 客户端给 Node 1 发送新建、索引或删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。它转发请求到 Node 3 ,分片 0 位于这个节点上。
  3. Node 3 在主分片上执行请求,如果成功,它转发请求到相应的位于 Node 1 和 Node 2的复制节点上。当所有的复制节点报告成功, Node 3 报告成功到请求的节点,请求的节点再报告给客户端。客户端接收到成功响应的时候,文档的修改已经被应用于主分片和所有的复制分片。你的修改生效了。
2.5.3、搜索文档(单个文档)

文档能够从主分片或任意一个复制分片被检索。
在这里插入图片描述
在主分片或复制分片上检索一个文档必要的顺序步骤:

  1. 客户端给 Node 1 发送get请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。分片 0 对应的复制分片在三个节点上都有。此时,它转发请求到 Node 2 。
  3. Node 2 返回文档(document)给 Node 1 然后返回给客户端。

对于读请求,为了平衡负载,请求节点会为每个请求选择不同的分片——它会循环所有分片副本。可能的情况是,一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上。这时复制分片会报告文档未找到,主分片会成功返回文档。一旦索引请求成功返回给用户,文档则在主分片和复制分片都是可用的。

2.5.4、全文搜索

对于全文搜索而言,文档可能分散在各个节点上,那么在分布式的情况下,如何搜索文档呢?搜索,分为2个阶段,搜索(query)+取回(fetch)。
在这里插入图片描述
查询阶段包含以下三步:

  1. 客户端发送一个 search(搜索) 请求给 Node 3 , Node 3 创建了一个长度为 from+size 的空优先级队
  2. Node 3 转发这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且结果将结果到一个大小为 from+size 的有序本地优先队列里去。
  3. 每个分片返回document的ID和它优先队列里的所有document的排序值给协调节点 Node 3 。 Node 3 把这些值合并到自己的优先队列里产生全局排序结果。
    在这里插入图片描述
    分发阶段由以下步骤构成:
  4. 协调节点辨别出哪个document需要取回,并且向相关分片发出 GET 请求。
  5. 每个分片加载document并且根据需要丰富(enrich)它们,然后再将document返回协调节点。
  6. 一旦所有的document都被取回,协调节点会将结果返回给客户端。

3、Java客户端

在Elasticsearch中,为java提供了2种客户端,一种是REST风格的客户端,另一种是Java API的客户端。本文主要介绍与springboot的整合,顾详细介绍原生客户端,需要了解的朋友可以直接查看官网文档,最新已经7.4版本了。
在这里插入图片描述

4、Spring Data Elasticsearch

Spring Data项目对Elasticsearch做了支持,其目的就是简化对Elasticsearch的操作。
地址:https://spring.io/projects/spring-data-elasticsearch

4.1、导入依赖

这里采用SpringBoot整合的方式进行。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
    </parent>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.elasticsearch.client</groupId>
      <artifactId>elasticsearch-rest-client</artifactId>
      <version>6.5.4</version>
    </dependency>
    <dependency>
      <groupId>org.elasticsearch.client</groupId>
      <artifactId>elasticsearch-rest-high-level-client</artifactId>
      <version>6.5.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.4</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.4</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <!-- java编译插件 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.2</version>
        <configuration>
        <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
4.2、编写application.properties
spring.data.elasticsearch.cluster-name=es-cluster
spring.data.elasticsearch.cluster-
nodes=192.168.55.105:9300,192.168.55.105:9301,192.168.55.105:9302

这里要注意,使用的端口是9300,而并非9200,原因是9200是RESTful端口,9300是API端口。

4.3、编写启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
  public static void main(String[] args) {
    SpringApplication.run(MyApplication.class, args);
 }
}
4.4、编写测试用例
4.4.1、编写User对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "es", type = "user", shards = 6, replicas = 1)
public class User {
  @Id
  private Long id;
  @Field(store = true)
  private String name;
  @Field
  private Integer age;
  @Field
  private String hobby;
}
4.4.2、新增数据
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestSpringBootES {
  @Autowired
  private ElasticsearchTemplate elasticsearchTemplate;
  @Test
  public void testSave(){
    User user = new User();
    user.setId(1001L);
    user.setAge(20);
    user.setName("张三");
    user.setHobby("足球、篮球、听音乐");
    IndexQuery indexQuery = new IndexQueryBuilder().withObject(user).build();
    String index = this.elasticsearchTemplate.index(indexQuery);
    System.out.println(index);
 }
}
4.4.3、批量插入数据
@Test
  public void testBulk() {
    List list = new ArrayList();
    for (int i = 0; i < 5000; i++) {
      User user = new User();
      user.setId(1001L + i);
      user.setAge(i % 50 + 10);
      user.setName("张三" + i);
      user.setHobby("足球、篮球、听音乐");
      IndexQuery indexQuery = new
IndexQueryBuilder().withObject(user).build();
      list.add(indexQuery);
   }
    Long start = System.currentTimeMillis();
    this.elasticsearchTemplate.bulkIndex(list);
    System.out.println("用时:" + (System.currentTimeMillis() - start));
 }
4.4.4、更新数据
**
  * 局部更新,全部更新使用index覆盖即可
  */
  @Test
  public void testUpdate() {
    IndexRequest indexRequest = new IndexRequest();
    indexRequest.source("age", "30");
    UpdateQuery updateQuery = new UpdateQueryBuilder()
       .withId("1001")
       .withClass(User.class)
       .withIndexRequest(indexRequest).build();
    this.elasticsearchTemplate.update(updateQuery);
 }
4.4.5、删除数据
@Test
public void testDelete(){
  this.elasticsearchTemplate.delete(User.class, "1001");
}

####### 4.4.6、搜索

@Test
public void testSearch(){
  PageRequest pageRequest = PageRequest.of(1,10); //设置分页参数
  SearchQuery searchQuery = new NativeSearchQueryBuilder()
     .withQuery(QueryBuilders.matchQuery("name", "张三")) // match查询
     .withPageable(pageRequest)
     .build();
  AggregatedPage<User> users =
this.elasticsearchTemplate.queryForPage(searchQuery, User.class);
  System.out.println("总页数:" + users.getTotalPages()); //获取总页数
  for (User user : users.getContent()) { // 获取搜索到的数据
    System.out.println(user);
 }
}
发布了105 篇原创文章 · 获赞 7 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_43792385/article/details/102824793