# 初识 elasticsearch

# 了解 ES

# elasticsearch 作用

elasticsearch 是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容

# ELK 技术栈

elasticsearch 结合 kibana、Logstash、Beats,也就是 elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

image-20210720194008781

而 elasticsearch 是 elastic stack 的核心,负责存储、搜索、分析数据。

image-20210720194230265

# 倒排索引

倒排索引的概念是基于 MySQL 这样的正向索引而言的

# 正向索引

逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难

# 倒排索引

# 两个重要概念

  1. 文档( Document ):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
  2. 词条( Term ):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

# 创建倒排索引的流程

  1. 将每一个文档利用算法分词,得到一个词条
  2. 创建表,每行数据包括词条、词条所在文档 id、位置等信息
  3. 因为词条唯一性,可以给词条创建索引,例如 hash 表结构索引

image-20210720200457207

# 倒排索引的搜索流程

  1. 用户输入条件 "华为手机" 进行搜索
  2. 对用户输入内容分词,得到词条: 华为手机
  3. 拿着词条在倒排索引中查找,可以得到包含词条的文档 id:1、2、3
  4. 拿着文档 id 到正向索引中查找具体文档

image-20210720201115192

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档 id 都建立了索引,查询速度非常快!无需全表扫描

# 正向和倒排

# 介绍

  1. 正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
  2. 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是根据词条找文档的过程

# 正向索引优劣

  1. 优点:
    1. 可以给多个字段创建索引
    2. 根据索引字段搜索、排序速度非常快
  2. 缺点:
    1. 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描

# 倒排索引优劣

  1. 优点:
    1. 根据词条搜索、模糊搜索时,速度非常快
  2. 缺点:
    1. 能给词条创建索引,而不是字段
    2. 无法根据字段做排序

# ES 的基本概念

# 文档和字段

elasticsearch 是面向 ** 文档(Document)** 存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为 json 格式后存储在 elasticsearch 中

image-20210720202707797

而 Json 文档中往往包含很多的字段(Field),类似于数据库中的列

# 索引和映射

索引(Index),就是相同类型的文档的集合

  • 所有用户文档,就可以组织在一起,称为用户的索引;
  • 所有商品的文档,可以组织在一起,称为商品的索引;
  • 所有订单的文档,可以组织在一起,称为订单的索引;

image-20210720203022172

索引相当于数据库中的表

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束

# mysql 和 elasticsearch

MySQL Elasticsearch 说明
Table Index 索引 (index),就是文档的集合,类似数据库的表 (table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是 JSON 格式
Column Field 字段(Field),就是 JSON 文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL 是 elasticsearch 提供的 JSON 风格的请求语句,用来操作 elasticsearch,实现 CRUD
  1. Mysql:擅长事务类型操作,可以确保数据的安全和一致性
  2. Elasticsearch:擅长海量数据的搜索、分析、计算

image-20210720203534945

# 安装 ES、kibana

# 部署单点 ES

# 创建网站

因为我们还需要部署 kibana 容器,因此需要让 es 和 kibana 容器互联。这里先创建一个网络

docker network create es-net

# 加载镜像与运行

docker run -d \
	--name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1
  • -e "cluster.name=es-docker-cluster" :设置集群名称
  • -e "http.host=0.0.0.0" :监听的地址,可以外网访问
  • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" :内存大小
  • -e "discovery.type=single-node" :非集群模式
  • -v es-data:/usr/share/elasticsearch/data :挂载逻辑卷,绑定 es 的数据目录
  • -v es-logs:/usr/share/elasticsearch/logs :挂载逻辑卷,绑定 es 的日志目录
  • -v es-plugins:/usr/share/elasticsearch/plugins :挂载逻辑卷,绑定 es 的插件目录
  • --privileged :授予逻辑卷访问权
  • --network es-net :加入一个名为 es-net 的网络中
  • -p 9200:9200 :端口映射配置

http://192.168.150.101:9200 即可看到 elasticsearch 的响应结果

# 部署 kibana

kibana 可以给我们提供一个 elasticsearch 的可视化界面,便于我们学习

# 部署

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1
  • --network es-net :加入一个名为 es-net 的网络中,与 elasticsearch 在同一个网络中
  • -e ELASTICSEARCH_HOSTS=http://es:9200" :设置 elasticsearch 的地址,因为 kibana 已经与 elasticsearch 在一个网络,因此可以用容器名直接访问 elasticsearch
  • -p 5601:5601 :端口映射配置

http://192.168.150.101:5601,即可看到结果

# DevTools

image-20210506102630393

这个界面中可以编写 DSL 来操作 elasticsearch。并且对 DSL 语句有自动补全功能

# 安装 ik 分词器

# 在线安装 ik 分词器插件

# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch

# 离线安装 ik 分词器插件

安装插件需要知道 elasticsearch 的 plugins 目录位置,而我们用了数据卷挂载,因此需要查看 elasticsearch 的数据卷目录,通过下面命令查看

docker volume inspect es-plugins

显示结果:

[
    {
        "CreatedAt": "2022-05-06T10:06:34+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

说明 plugins 目录被挂载到了: /var/lib/docker/volumes/es-plugins/_data 这个目录中

解压缩分词器安装包

下面我们需要把课前资料中的 ik 分词器解压缩,重命名为 ik

image-20210506110249144

上传到 es 容器的插件数据卷中

也就是 /var/lib/docker/volumes/es-plugins/_data

image-20210506110704293

重启容器

# 4、重启容器
docker restart es
# 查看 es 日志
docker logs -f es

测试

  1. ik_smart :最少切分
  2. ik_max_word :最细切分

# 扩展词词典

随着互联网的发展,“造词运动” 也越发的频繁。出现了很多新的词语

1)打开 IK 分词器 config 目录:

image-20210506112225508

2)在 IKAnalyzer.cfg.xml 配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!-- 用户可以在这里配置自己的扩展字典 *** 添加扩展词典 -->
        <entry key="ext_dict">ext.dic</entry>
</properties>

3)新建一个 ext.dic,可以参考 config 目录下复制一个配置文件进行修改

传智播客
奥力给

4)重启 elasticsearch

docker restart es
# 查看 日志
docker logs -f elasticsearch

image-20201115230900504

日志中已经成功加载 ext.dic 配置文件

5)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业超过90%,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用 Windows 记事本编辑

# 停用词词典

在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。

IK 分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。

1)IKAnalyzer.cfg.xml 配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!-- 用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict">ext.dic</entry>
         <!-- 用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典 -->
        <entry key="ext_stopwords">stopword.dic</entry>
</properties>

2)在 stopword.dic 添加停用词

习大大

3)重启 elasticsearch

# 重启服务
docker restart elasticsearch
docker restart kibana
# 查看 日志
docker logs -f elasticsearch

日志中已经成功加载 stopword.dic 配置文件

4)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业率超过95%,习大大都点赞,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用 Windows 记事本编辑

# 索引库操作

# mapping 映射属性

mapping 是对索引库中文档的约束,常见的 mapping 属性包括:

  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为 true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

# 索引库的 CRUD

# 创建索引库和映射

# 基本语法

  • 请求方式:PUT
  • 请求路径:/ 索引库名,可以自定义
  • 请求参数:mapping 映射

# 格式

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      //... 略
    }
  }
}

# 示例

PUT /baozi
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "email":{
        "type": "keyword",
        "index": "falsae"
      },
      "name":{
        "properties": {
          "firstName": {
            "type": "keyword"
          }
        }
      },
      //... 略
    }
  }
}

# 查询索引库

# 基本语法

  • 请求方式:GET
  • 请求路径:/ 索引库名
  • 请求参数:无

# 格式

GET /索引库名

# 示例

GET /baozi

# 修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping

虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响

# 语法说明

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

# 示例

PUT /baozi/_mapping
{
	"properties": {
		"age": {
			"type": "integer"
		}
	}
}

# 删除索引库

# 语法

  • 请求方式:DELETE
  • 请求路径:/ 索引库名
  • 请求参数:无

# 格式

DELETE /索引库名字

# 示例

DELETE /baozi

# 文档操作

# 新增文档

# 语法

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}

# 示例

POST /baozi/_doc/1
{
    "info": "包子",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

# 相应

image-20210720212933362

# 查询文档

根据 REST 风格,新增是 post,查询应该是 get,不过查询一般都需要条件,这里我们把文档 id 带上

# 语法

GET /{索引库名称}/_doc/{id}

# 示例

GET /baozi/_doc/1

# 查看结果

image-20210720213345003

# 删除文档

删除使用 DELETE 请求,同样,需要根据 id 进行删除

# 语法

DELETE /{索引库名}/_doc/id值

# 示例

# 根据id删除数据
DELETE /baozi/_doc/1

# 结果

image-20210720213634918

# 修改文档

# 全量修改

# 介绍

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的 id 删除文档
  • 新增一个相同 id 的文档

注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

# 语法

PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    //... 略
}

# 示例

PUT /baozi/_doc/1
{
    "info": "包子",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

# 增量修改

增量修改是只修改指定 id 匹配的文档中的部分字段

# 语法

POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

# 示例

POST /baozi/_update/1
{
  "doc": {
    "email": "ZhaoYun@itcast.cn"
  }
}

# RestAPI

# 初始化 RestClient

在 elasticsearch 提供的 API 中,与 elasticsearch 一切交互都封装在一个名为 RestHighLevelClient 的类中,必须先完成这个对象的初始化,建立与 elasticsearch 的连接

# 引入 ES 的 RestHightClient 依赖

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

# 覆盖 ES 版本 7.6.2

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

# 初始化 RestHighLevelClient

# 初始化代码

RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
));

# 编写测试用例

public class HotelIndexTest {
    private RestHighLevelClient client;
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.150.101:9200")
        ));
    }
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

# 创建索引库

# 代码解读

image-20210720223049408

  1. 创建 Request 对象,因为创建索引库的操作,因此 Request 是 CreateIndexRequest
  2. 添加请求参数,其实就是 DSL 的 JSON 参数部分
  3. 发送请求, client.indices() 方法的返回值就是 IndicesClient 类型,封装了所有索引库操作有关的方法

# 完整案例

# 定义一个类,存储常量参数 JSON

在 hotel-demo 的 cn.itcast.hotel.constants 包下,创建一个类,定义 mapping 映射的 JSON 字符串常量

public class HotelConstants {
    public static final String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"address\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"price\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"score\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"brand\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"city\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\n" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"business\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"location\":{\n" +
            "        \"type\": \"geo_point\"\n" +
            "      },\n" +
            "      \"pic\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"all\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";
}

# 单元测试

@Test
void createHotelIndex() throws IOException {
    // 1. 创建 Request 对象
    CreateIndexRequest request = new CreateIndexRequest("hotel");
    // 2. 准备请求的参数:DSL 语句
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 3. 发送请求
    client.indices().create(request, RequestOptions.DEFAULT);
}

# 删除索引库

# 单元测试

@Test
void testDeleteHotelIndex() throws IOException {
    // 1. 创建 Request 对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    // 2. 发送请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}

# 判断索引库是否存在

# 单元测试

@Test
void testExistsHotelIndex() throws IOException {
    // 1. 创建 Request 对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    // 2. 发送请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    // 3. 输出
    System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

# RestClient 操作文档

# 新增文档

# 索引库实体类

# Hotel 实体

@Data
@TableName("tb_hotel")
public class Hotel {
    @TableId(type = IdType.INPUT)
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String longitude;
    private String latitude;
    private String pic;
}

# HotelDoc 实体

@Data
@NoArgsConstructor
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;
    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
    }
}

# 语法说明

# DSL 语句

POST /{索引库名}/_doc/1
{
    "name": "Jack",
    "age": 21
}

# Java 代码

image-20210720230027240

# 查询文档

# 语法说明

# DSL 语句

GET /hotel/_doc/{id}

# Java 代码

image-20210720230811674

# 完整代码

@Test
void testGetDocumentById() throws IOException {
    // 1. 准备 Request
    GetRequest request = new GetRequest("hotel", "61082");
    // 2. 发送请求,得到响应
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3. 解析响应结果
    String json = response.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    System.out.println(hotelDoc);
}

# 删除文档

# 语法说明

# DSL 语句

DELETE /hotel/_doc/{id}

# Java 代码

@Test
void testDeleteDocument() throws IOException {
    // 1. 准备 Request
    DeleteRequest request = new DeleteRequest("hotel", "61083");
    // 2. 发送请求
    client.delete(request, RequestOptions.DEFAULT);
}

# 修改文档

# 语法说明

# 增量修改 Java 代码

image-20210720231040875

# 完整代码

@Test
void testUpdateDocument() throws IOException {
    // 1. 准备 Request
    UpdateRequest request = new UpdateRequest("hotel", "61083");
    // 2. 准备请求参数
    request.doc(
        "price", "952",
        "starName", "四钻"
    );
    // 3. 发送请求
    client.update(request, RequestOptions.DEFAULT);
}

# 批量导入文档

# 语法说明

# Java 代码

image-20210720232431383

# 完整代码

@Test
void testBulkRequest() throws IOException {
    // 批量查询酒店数据
    List<Hotel> hotels = hotelService.list();
    // 1. 创建 Request
    BulkRequest request = new BulkRequest();
    // 2. 准备参数,添加多个新增的 Request
    for (Hotel hotel : hotels) {
        // 2.1. 转换为文档类型 HotelDoc
        HotelDoc hotelDoc = new HotelDoc(hotel);
        // 2.2. 创建新增文档的 Request 对象
        request.add(new IndexRequest("hotel")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
    }
    // 3. 发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Baozi 微信支付

微信支付

Baozi 支付宝

支付宝

Baozi 微信

微信