ElasticSearch 8.x 教程

ElasticSearch 8.x 教程

MySQL 和 ElasticSearch 的区别

MySQL 架构天生不适合海量数据查询,它只适合海量数据存储,但无法应对海量数据下各种复杂条件的查询。原因如下:

  1. 加索引确实可以提升查询速度,但是如果没有索引满足搜索条件,就会触发全表扫描。即使使用了组合索引,也要符合最左前缀原则才能命中索引。而在海量数据多种查询条件下,很有可能不符合最左前缀原则而导致索引失效。

  2. 存储都是需要成本的,如果针对每一种情况都加索引,以 innoDB 为例,每加一个索引,就会创建一颗 B+ 树,如果是海量数据,将会增加很大的存储成本。

  3. 有些查询条件是 MySQL 加索引都解决不了的,比如我要查询商品中所有 title 带有「格力空调」的关键词,如果用 MySQL 写,会写出如下代码:该代码无法命中任何索引,会触发全表扫描。一旦关键词记错,即使全表扫描也无法查询到任何商品。

    SELECT * FROM product WHERE title like '%格力空调%' 
    

术业有专攻,海量数据查询还得用专门的搜索引擎。以 ES 为例,它是基于 Lucene 引擎构建的开源分布式搜索分析引擎,可以提供针对 PB 数据的近实时查询,广泛用在全文检索、日志分析、监控分析等场景。ES 有以下三个特点:

  1. 轻松支持各种复杂的查询条件:它是分布式实时文件存储,会把每一个字段都编入索引 (倒排索引),利用高效的倒排索引,以及自定义打分、排序能力与丰富的分词插件等,能实现任意复杂查询条件下的全文检索需求。
  2. 可扩展性强:天然支持分布式存储,通过极其简单的配置实现几百上千台服务器的分布式横向扩容,轻松处理 PB 级别的结构化或非结构化数据。
  3. 高可用,容灾性能好:通过使用主备节点,以及故障的自动探测与恢复,有力地保障了高可用。

简而言之,MySQL 和 ElasticSearch 的更多区别可以参考:

  1. 为什么要用搜索引擎,MySQL 不香吗
  2. 何时使用 Elasticsearch 而不是 MySql

ELK 安装

ELK 是 Elasticsearch、Logstash、Kibana 三大开源框架首字母大写简称。市面上也被成为 Elastic Stack。

  1. Elasticsearch 是一个基于 Lucene、分布式、通过 Restful 方式进行交互的近实时搜索平台框架。
  2. Logstash 是 ELK 的中央数据流引擎,用于从不同目标(文件 / 数据存储 / MQ)收集的不同格式数据,经过过滤后支持输出到不同的目的地(文件 / MQ / redis / elasticsearch / kafka 等)。
  3. Kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 es 索引中的数据,提供实时分析的功能。它操作简单,基于浏览器的用户界面可以快速创建仪表板 dashboard,实时显示查询动态。

市面上很多开发只要一提到 ELK 能够一致说出它是一个日志分析架构技术栈总称,但实际上 ELK 不仅仅适用于日志分析,它还可以支持其他任何数据分析和收集的场景。

windows 下安装 Elasticsearch

  1. 最新版本下载地址:Download Elasticsearch | Elastic
  2. 熟悉目录:
bin:启动文件
config:配置文件
     log4j2:日志配置文件
     jvm.options:java虚拟机相关的配置
     elasticsearch.yml:elasticsearch的配置文件!默认9200端口!跨域!
lib:相关jar包
logs:日志
modules:功能模块
plugins:插件!
  1. 启动:进入解压后的目录,运行下面的命令:
./bin/elasticsearch

# 如果这时报错 "max virtual memory areas vm.max *map*count [65530] is too low"
sudo sysctl -w vm.max_map_count=262144
  1. 访问 localhost:9200
curl localhost:9200

默认情况下,Elastic 只允许本机访问,如果需要远程访问,可以修改 Elastic 安装目录的 config/elasticsearch.yml 文件,去掉 network.host 的注释,将它的值改成 0.0.0.0,然后重新启动 Elastic。

线上服务不要这样设置,要设成具体的 IP。

Docker-Compose 安装 ELK

  1. 创建 docker-compose.yaml 文件:注意 ES 和 Kibana 的版本要一致
version: "3.9"
services:
  elasticsearch:
    image: elasticsearch:8.7.1
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
      - xpack.security.enabled=false
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ports:
      - target: 9200
        published: 9200
    networks:
      - elastic

  kibana:
    image: kibana:8.7.1
    ports:
      - target: 5601
        published: 5601
    depends_on:
      - elasticsearch
    networks:
      - elastic

volumes:
  es_data:
    driver: local

networks:
  elastic:
    name: elastic
    driver: bridge
  1. 在该文件所在目录,执行:docker-compose up

  2. 访问 http://localhost:9200/,返回:

6ab0dc860d1",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "YE5ZOkzbQoejhluBtOT_eQ",
  "version" : {
    "number" : "8.7.1",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "f229ed3f893a515d590d0f39b05f68913e2d9b53",
    "build_date" : "2023-04-27T04:33:42.127815583Z",
    "build_snapshot" : false,
    "lucene_version" : "9.5.0",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
  1. 访问 http://localhost:5601/app/home#/ 得到 Elastic 的可视化界面

  2. 如果容器意外关闭的话:

    1. 服务器版:可以使用 docker logs -f 容器id 查看日志

    2. 桌面版:点击 view details 进入 详情页

      image-20230510171535792

  3. 如果不再使用这些容器,可以删除:docker-compose down

  4. 重启 windows 前请务必先关掉容器

ES 的核心概念

集群,节点,索引,类型,文档,分片,映射是什么?

基本概念

Node 与 Cluster

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。

Index

Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。

下面的命令可以查看当前节点的所有 Index:

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

Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。

Document 使用 JSON 格式表示,下面是一个例子。

{
  "user": "张三",
  "title": "工程师",
  "desc": "数据库管理"
}

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

关系行数据库和 Elasticsearch 对比

Elasticsearch 面向文档

Relational DB ElasticSearch
数据库 (database) 索引 (indices)
表 (tables) types(弃用)
行 (rows) documents
字段 (columns) fields

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

物理设计

ES 在后台把每个索引划分为多个分片,每个分片可以在集群中的不同服务器间迁移。一个人就是一个集群,默认的集群名 elasticsearch。

节点和分片如何工作

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

img

上图是一个有 3 个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉了,数据也不至于丢失。实际上,一个分片是一个 Lucene 索引,一个包含倒排索引的文件目录,倒排索引的结构使得 es 在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。

什么是倒排索引?

比如我们通过博客标签来搜索博客文章。那么倒排索引列表就是这样的一个结构:

img

如果我们需要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据将会快的多。只需要查看标签这一栏,然后获取相关的文章 ID 即可。

逻辑设计

一个索引类型中,包含多个文档,比如说文档 1、文档 2。当我们索引一篇文档时,可以通过这样的一个顺序找到它:索引 (数据库) \(\to\) 类型 (表)\(\to\) 文档 ID (行),通过这个组合我们就能索引到某个具体的文档。

注意:ID 不必是整数,实际上它是个字符串。

  1. 文档:es 是面向文档的,索引和搜索数据的最小单位是文档,文档有如下属性:
    1. 自我包含:一篇文档同时包含字段和对应的值,也就是同时包含 key:value
    2. 可以是层次型的:个文档中包含自文档,就是一个 json 对象
    3. 灵活的结构:文档不依赖于预先定义的模式。在关系数据库中,要提前定义字段才能用;在 es 中,字段是非常灵活的,可以动态添加一个新的字段
  2. 类型:类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如 name 映射为字符串类型。
  3. 索引:也就是数据库,索引是映射类型的容器,es 的索引是一个非常大的文档集合,索引存储了映射类型的字段和其他设置,然后将其存储在各个分片上

在 es 中安装插件

elasticsearch-analysis-ik 分词器为例:

安装

方法一:直接进入容器下载

# 1. 进入运行中的容器
docker exec -it 容器id bash
# 2. 安装插件
bin/elasticsearch-plugin install https://github.com/ElisaMin/elasticsearch-analysis-ik/releases/download/8.7.1-17/elasticsearch-analysis-ik-8.7.1.zip
# 3. 查看插件是否安装成功
bin/elasticsearch-plugin list
# 3. 退出容器
exit
# 4. 重启容器
docker restart 容器id

方法二:本地下载

# 1. 下载插件安装包,然后将其拷贝到容器中
docker cp D:\Software\Elasticsearch\Docker\plugins\elasticsearch-analysis-ik-8.7.1.zip 容器id:/tmp/
# 2. 进入运行中的容器
docker exec -it 容器id bash
# 3. 安装插件
bin/elasticsearch-plugin install file:///tmp/elasticsearch-analysis-ik-8.7.1.zip

使用

  1. 进入 [Console - Dev Tools - Elastic]
  2. 在控制台输入:其中 ik_smart 为最少切分;ik_max_word 为最细粒度划分
GET _analyze
{
  "analyzer": "ik_smart",
  "text": "北京邮电大学"
}

GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "北京邮电大学"
}

restful 风格说明

restful 风格是一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

基本 rest 说明:

method url 地址 描述
PUT localhost:9200 / 索引名称 / 类型名称 / 文档 id 创建文档(指定文档 id)
POST localhost:9200 / 索引名称 / 类型名称 创建文档(随机文档 id)
POST localhost:9200 / 索引名称 / 类型名称 / 文档 id/_update 修改文档
DELETE localhost:9200 / 索引名称 / 类型名称 / 文档 id 删除文档
GET localhost:9200 / 索引名称 / 类型名称 / 文档 id 查询文档通过文档 id
POST localhost:9200 / 索引名称 / 类型名称 /_search 查询所有数据

关于索引的基本操作

  1. 创建一个索引
# ElasticSearch 的 7.x.x 版本
PUT /索引名/类型/文档id
{
    请求体
}

# ElasticSearch 的 8.x.x 版本,类型已经弃用,默认写_doc
PUT /索引名/_doc/文档id
{
    请求体
}

# 举个栗子
PUT /test1/_doc/1
{
    "name": "locke",
	"age": 3,
	"birthday": "1997-04-06"
}

Kibana 返回:
{
  "_index": "test1",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

官网类型文档地址

# 字符串类型
text、keyword
# 数值类型
long,integer,short,byte,double,float,half float,scaled float
# 日期类型
date
# te布尔值类型
boolean·
# 二进制类型
binary
  1. 创建具体的索引规则:
PUT test2/
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "long"
      },
      "birthday": {
        "type": "date"
      }
    }
  }
}

Kibana 返回:
{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "test2"
}
  1. 获得索引规则
GET test2

如果自己的文档字段没有指定,那么 es 就会给我们默认配置字段类型!

  1. 扩展命令
GET _cat/health      # 查看健康值
GET _cat/indices?v   # 查看所有东西的版本信息
  1. 修改:POST
POST test1/_update/1
{
  "doc": {
    "name": "lockegogo"
  }
}
  1. 删除:DELETE,根据请求来判断是删除索引还是删除文档
DELETE test1

关于文档的基本操作

基本操作
# 增
PUT /test/_doc/1
{
  "name": "locke",
  "age": 26,
  "desc": "喜欢大熊猫花花",
  "tags": ["圆滚滚","可爱","三角形"]
}
PUT /test/_doc/2
{
  "name": "feifei",
  "age": 28,
  "desc": "喜欢大熊猫福宝",
  "tags": ["活泼","调皮","花园杀手"]
}

# 查
GET /test/_doc/1
GET /test/_doc/2

# 改
POST /test/_update/1
{
  "doc": {
  	"name": "locke"
  }
}
简单操作:条件查询

如果有多条记录,匹配度越高,分数越高:

# 支持模糊查询
GET /test/_search?q=name:locke
复杂操作
  1. 排序 & 分页
# "_source": 过滤字段查询
# "sort": 排序
# "from": 分页查询,从第几条数据开始查询
# "size": 返回多少数据

GET test/_search
{
 "query": {
   "match": {
     "name": "feifei"
   }
 },
 "_source": ["name", "desc"],
 "sort": [
   {
     "age": {
       "order": "desc"
     }
   }
 ],
 "from": 0,
 "size": 1
}

  1. 布尔值查询
    1. must = and:既符合 A 也符合 B
    2. should = or:符合 A 或符合 B
    3. must_not = not:不符合 A 或不符合 B
GET test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "feifei"
          }
        },
        {
          "match": {
            "age": "29"
          }
        }
      ]
    }
  }
}
  1. 过滤查询:filter
GET test/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "feifei"
          }
        }
      ],
      "filter": [
        {
          "range": {
            "age": {
              "gte": 10,
              "lte": 30
            }
          }
        }
      ]
    }
  }
}
  1. 匹配多个条件:多个条件使用空格隔开,只要满足一个条件就可以被查出,可以通过分值进行基本判断
# 匹配多个条件
GET test/_search
{
  "query": {
    "match": {
      "tags": "活泼 花园杀手 三角形"
    }
  }
}
  1. 精确查询:term 查询是直接通过倒排索引指定的词条进行精确查找
    1. term:直接查询精确的,两种情况:text or keyword
    2. match:会使用分词器解析,先分析文档,然后在通过分析的文档进行查询
# 1. 创建数据
PUT testdb
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "desc": {
        "type": "keyword"
      }
    }
  }
}

PUT testdb/_doc/1
{
  "name": "java name",
  "desc": "java desc"
}

PUT testdb/_doc/2
{
  "name": "java name",
  "desc": "java desc2"
}

# 2. 分析数据,查看结果
# 2.1 没有被拆分
GET _analyze
{
  "analyzer": "keyword",
  "text": "java name"
}
# 2.2 被拆分了
GET _analyze
{
  "analyzer": "standard",
  "text": "java name"
}

# 3. 查看数据:keyword 类型的字段不会被分词器解析
# 3.1 两条记录都可以查到
GET testdb/_search
{
  "query": {
    "match": {
      "name": "java"
    }
  }
}
# 3.2 只能查到一条,因为 keyword 不可拆分
GET testdb/_search
{
  "query": {
    "match": {
      "desc": "java desc"
    }
  }
}

# term
GET testdb/_search
{
  "query": {
    "term": {
      "desc": "java desc"
    }
  }
}
  1. 高亮查询
# "pre_tags" + "post_tags": 自定义高亮条件

GET testdb/_search
{
  "query": {
    "match": {
      "desc": "java desc"
    }
  },
  "highlight": {
  	"pre_tags": "<p class='key' style='color:red'>",
  	"post_tags": "</p>",
    "fields": {
      "desc": {}
    }
  }
}

集成 SpringBoot

Github 地址ElasticSearch 集成 SpringBoot

依赖导入

  1. 文档
  2. 找到原生的依赖:注意 elasticsearch 的版本要和本地的版本一致
<project>
  <dependencies>

    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>8.7.1</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.12.3</version>
    </dependency>

  </dependencies>
</project>

如果出现 ClassNotFoundException: jakarta.json.spi.JsonProvider 错误,增加:

<project>
  <dependencies>
    ...
    <dependency>
      <groupId>jakarta.json</groupId>
      <artifactId>jakarta.json-api</artifactId>
      <version>2.0.1</version>
    </dependency>

  </dependencies>
</project>

注意:在构建 SpringBoot 项目时,需要添加依赖项:NoSQL \(\to\) Spring Data Elasticsearch (Access+Driver)

配置类

Java API 客户端围绕三个主要组件进行了结构化设计:

  • API 客户端类:它们提供了强类型的数据结构和用于 Elasticsearch API 的方法。由于 Elasticsearch API 较大,它被组织成命名空间,每个命名空间都有自己的客户端类。Elasticsearch 核心功能在 ElasticsearchClient 类中实现。

  • JSON 对象映射器:它将您的应用程序类映射到 JSON,并与 API 客户端无缝集成。

  • 传输层实现:这是处理所有 HTTP 请求的地方。

com.locke.demos 中新建 config 文件夹,添加 ElasticSearchClientConfig

@Configuration
public class ElasticSearchClientConfig {

    @Bean
    public ElasticsearchClient elasticsearchClient() {
        // Create the low-level client
        RestClient restClient = RestClient.builder(
                new HttpHost("localhost", 9200)).build();

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper());

        // And create the API client
        ElasticsearchClient client = new ElasticsearchClient(transport);
        return client;
    }

}

索引 CRUD

test 文件夹中的 EsApiApplicationTests 类中测试 api

@SpringBootTest
class EsApiApplicationTests {


    @Autowired
    @Qualifier("elasticsearchClient")
    private ElasticsearchClient client;

    // 创建索引
    @Test
    public void testCreateIndex() throws IOException {
        CreateIndexResponse indexResponse = client.indices().create(c -> c.index("user"));
        System.out.println(indexResponse);
    }

    // 查询索引
    @Test
    public void testGetIndex() throws IOException {
        GetIndexResponse getindexResponse = client.indices().get(i -> i.index("user"));
        System.out.println(getindexResponse);
    }

    // 判断索引是否存在
    @Test
    public void testExistsIndex() throws IOException {
        BooleanResponse booleanResponse = client.indices().exists(e -> e.index("user"));
        System.out.println(booleanResponse.value());
    }

    // 删除索引
    @Test
    public void testDeleteIndex() throws IOException {
        DeleteIndexResponse deleteIndexResponse = client.indices().delete(d -> d.index("user"));
        System.out.println(deleteIndexResponse.acknowledged());
    }

}

Document CRUD

com.locke.demos 中新建 pojo 文件夹,添加 User

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class User {
    private String name;
    private Integer age;
}

test 文件夹中的 EsApiApplicationTests 类中测试 api

@SpringBootTest
class EsApiApplicationTests {


    @Autowired
    @Qualifier("elasticsearchClient")
    private ElasticsearchClient client;

    // 测试添加文档:可同时创建索引
    @Test
    public void testAddDocument() throws IOException {
        User user = new User("locke", 18);
        IndexResponse indexResponse = client.index(i -> i.index("user").id("1").document(user));
    }

    // 更新文档
    @Test
    public void testUpdateDocument() throws IOException {
        UpdateResponse<User> updateResponse = client.update(u -> u.index("user")
                .id("1")
                .doc(new User("feifei", 13)), User.class);
    }

    // 判断文档是否存在
    @Test
    public void testExistsDocument() throws IOException {
        BooleanResponse booleanResponse = client.exists(e -> e.index("user").id("1"));
        System.out.println(booleanResponse.value());
    }

    // 查询文档
    @Test
    public void testGetDocument() throws IOException {
        GetResponse<User> getResponse = client.get(g -> g.index("user").id("1"), User.class);
        System.out.println(getResponse.source());
    }

    // 删除文档
    @Test
    public void testDeleteDocument() throws IOException {
        DeleteResponse deleteResponse = client.delete(d -> d.index("user").id("1"));
        System.out.println(deleteResponse.id());
    }

    // 批量插入文档
    @Test
    public void bulkTest() throws IOException {
        List<User> userList = new ArrayList<>();
        userList.add(new User("hello world", 11));
        userList.add(new User("hello java", 12));
        userList.add(new User("hello es", 13));
        userList.add(new User("hello spring", 14));
        userList.add(new User("user", 15));
        List<BulkOperation> bulkOperationArrayList = new ArrayList<>();
        // 遍历添加到 bulk 中
        for (User user : userList) {
            bulkOperationArrayList.add(BulkOperation.of(o -> o.index(i -> i.document(user))));
        }

        BulkResponse bulkResponse = client.bulk(b -> b.index("user")
                .operations(bulkOperationArrayList));

    }

    // 查询
    @Test
    public void searchTest() throws IOException {
        SearchResponse<User> search = client.search(s -> s
        .index("user")
                // 查询 name 字段包含 hello 的 document (不使用分词器精确查找)
                .query(q -> q
                        .term(t -> t
                                .field("name")
                                .value(v -> v.stringValue("hello"))
                        ))
                // 分页查询,从第 0 页开始查询 3 个 document
                .from(0)
                .size(3)
                // 按 age 降序排序
                .sort(f -> f.field(o -> o.field("age").order(SortOrder.Desc))), User.class
        );
        for (Hit<User> hit : search.hits().hits()) {
            System.out.println(hit.source());
        }
    }

}

实战:京东搜索

ElasticSearch 8.x 实战 - Lockegogo - 博客园 (cnblogs.com)

参考资料

  1. 安装并运行 Elasticsearch | Elasticsearch: 权威指南 | Elastic

  2. ElasticSearch7.6.x 最新完整教程通俗易懂【狂神说 Java】

  3. ElasticSearch7.6.x【狂神说 Java】

  4. 全文搜索引擎 Elasticsearch 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)

  5. SpringBoot 集成 ElasticSearch8.x 基本应用 CRUD 操作 环境安装_Sidney_Jack_1101 的博客

  6. 为什么要用搜索引擎,MySQL 不香吗

  7. 何时使用 Elasticsearch 而不是 MySql

posted @ 2023-05-12 10:27  Lockegogo  阅读(1727)  评论(0编辑  收藏  举报