当前位置:   article > 正文

微服务入门:elasticsearch与RestClient(1)_elasticsearch restclient

elasticsearch restclient

微服务入门:elasticsearch与RestClient(1)

一、elasticsearch的作用

在做一些小项目的话,通常使用MP来查询或者条件查询,速度上还是挺快的

但是在实际开发中,我们的数据肯定是上万甚至百万级的,这时候就应该使用elasticsearch在进行查找

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

elasticsearch是基于Restful风格实现的

Mysql采用的是正向索引,如下图所示

在这里插入图片描述

如果是根据id索引查询,那么相对而言效率比较快

如果是根据title查询,那么需要一行一行对比才可以得知是不是符合要求

因此,假如有一千万条数据,要查询的数据在第一千万条,那么就需要对比一千万次,效率极奇慢,用户体验非常不好

而elasticsearch采用的是倒排索引(相对而言的),如下图所示

在这里插入图片描述

倒排索引中有两个非常重要的概念:

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

倒排索引的搜索流程如下(以搜索"华为手机"为例):

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

在这里插入图片描述

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

正向索引

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

倒排索引

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

二、elasticsearch的相关概念

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

在这里插入图片描述

索引,也就是相同类型的文档的集合。也可以理解为是数据库中的表。**映射(mapping)**是索引中文档的字段约束信息,类似表的结构约束。

在这里插入图片描述


三、elasticsearch的安装

elasticsearch的安装也是基与docker来安装部署的

首先,需要创建一个网络。因为除了部署elasticsearch还需要部署kibana容器,然后使得两个容器互联

docker network create es-net
  • 1

接着将镜像放置在虚拟机内,这里镜像就不提供了,自己网上找一下吧

然后加载镜像

# 导入数据
docker load -i es.tar
  • 1
  • 2

同样的,还有kibana的tar包也需要这样做

接着运行下面的docker命令,部署单点的es:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

命令解释:

  • -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:端口映射配置

注意:

第一个端口9200是指暴露的http端口,供用户使用

第二个端口是9300是es容器各个结点之间互连的端口

部署完成之后,浏览器访问http://192.168.220.132:9200/即可查看elasticsearch的响应结果(这里的IP换自己对应的IP)

在这里插入图片描述

出现这个界面,说明elasticsearch安装成功。

安装完elasticsearch,还需要安装一个kibana

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

运行docker命令,部署kibana

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

安装成功之后,我们在浏览器访问http://192.168.220.132:5601/即可查看

在这里插入图片描述

出现这个界面就表示安装成功


四、安装IK分词器

前面提到了会将查询的内容进行分词,但是中文分词一般不是太完美,所以需要安装一个IK分词器,提高中文分词的准确度

将IK分词器解压到windows先,然后使用docker命名查看elasticsearch的plugins目录位置

docker volume inspect es-plugins
  • 1

在这里插入图片描述

然后将IK分词器解压放到这里的目录去

接着重启容器

# 4、重启容器
docker restart es
  • 1
  • 2

这样就完成了

知识点:

  • ik_smart:最少切分

  • ik_max_word:最细切分


五、索引库操作

索引库就类似数据库表,mapping映射就类似表的结构。

我们要向es中存储数据,必须先创建“库”和“表”。


1. mapping映射属性

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

例如下面的json

{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "消息",
    "email": "123456@qq.com",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

对应的每个字段映射(mapping):

  • age:类型为 integer;参与搜索,因此需要index为true;无需分词器
  • weight:类型为float;参与搜索,因此需要index为true;无需分词器
  • isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
  • info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
  • email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
  • score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
  • name:类型为object,需要定义多个子属性
    • name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
    • name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

2. 索引库的CRUD

这里的操作都是基于kibana进行操作的

(1)新增索引

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

例如:

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

(2)查询索引

  • 请求方式:GET

  • 请求路径:/索引库名

  • 请求参数:无

例如:

GET /索引库名
  • 1

(3)修改索引

索引库一旦创建,无法修改mapping

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

语法:

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(4)删除索引

语法:

  • 请求方式:DELETE

  • 请求路径:/索引库名

  • 请求参数:无

格式:

DELETE /索引库名
  • 1

六、文档操作

1. 文档的CRUD

(1)新增文档

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(2)查询文档

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

(3)删除文档

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

(4)修改文档

修改有两种方式:

  • 全量修改:直接覆盖原来的文档
  • 增量修改:修改文档中的部分字段
#全量修改
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
# 增量修改是只修改指定id匹配的文档中的部分字段。
POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

七、RestClient操作索引

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html

下面将会根据一个案例来说明RestAPI的使用

#酒店的mapping
PUT /hotel
{
  "mappings": {
    "properties": {
      "id":{
        "type": "keyword"
      },
      "name":{
        "type": "text", 
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address":{
        "type": "keyword",
        "index": false
      },
      "price":{
        "type": "integer"
      },
      "score":{
        "type": "integer"
      },
      "brand":{
        "type": "keyword",
        "copy_to": "all"
      },
      "city":{
        "type": "keyword"
      },
      "starName":{
        "type": "keyword"
      },
      "business":{
        "type": "keyword",
        "copy_to": "all"
      },
      "location":{
        "type": "geo_point"
      },
      "pic":{
        "type": "keyword",
        "index": false
      },
      "all":{
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

下面将会用JAVA代码添加上面的索引

注意:

  • location:地理坐标,里面包含精度、纬度
  • all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索

在这里插入图片描述

在这里插入图片描述


1. 初始化RestClient

首先引入依赖

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
  • 1
  • 2
  • 3
  • 4

初始化RestHighLevelClient

public class HotelIndexTest {
    private RestHighLevelClient client;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.220.132:9200")
        ));
    }

    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2. 创建索引库

@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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

其中MAPPING_TEMPLATE是自己定义的一个常量,也就是上面的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" +
            "      },\n" +
            "      \"starName\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"business\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"copy_to\": \"all\"\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" +
            "}";
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

代码分为三步:

  • 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest
  • 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
  • 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。

3. 删除索引

@Test
void deleteHotelIndex() throws IOException {
    //1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("hotel");
    //2.发送请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1)创建Request对象。这次是DeleteIndexRequest对象
  • 2)准备参数。这里是无参
  • 3)发送请求。改用delete方法

4. 判断索引是否存在

@Test
void testExitsHotelIndex() throws IOException {
    //1.创建Request对象
    GetIndexRequest request = new GetIndexRequest("hotel");
    //2.发送请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    System.out.println(exists);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1)创建Request对象。这次是GetIndexRequest对象
  • 2)准备参数。这里是无参
  • 3)发送请求。改用exists方法

八、RestClient操作文档

初始化和操作索引一样


1. 新增文档

@Test
void testAddDocument() throws IOException {
    //根据ID查询酒店数据
    Hotel hotel = hotelService.getById(61083L);
    //转换为文档类型
    HotelDoc hotelDoc = new HotelDoc(hotel);

    //1. 准备request对象
    IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
    //2. 准备JSON文档
    request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
    //3. 发送请求
    client.index(request, RequestOptions.DEFAULT);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

由于这里Hotel对象和文档有点差别,所以这里先将Hotel处理成hotelDoc再操作

  • 1)根据id查询酒店数据Hotel
  • 2)将Hotel封装为HotelDoc
  • 3)将HotelDoc序列化为JSON
  • 4)创建IndexRequest,指定索引库名和id
  • 5)准备请求参数,也就是JSON文档
  • 6)发送请求

2. 查询文档

@Test
void testGetDocumentById() throws IOException {
    //1. 准备Request
    GetRequest request = new GetRequest("hotel", "61083");
    //2. 发送请求,得到响应
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    //3. 解析响应结果
    String json = response.getSourceAsString();
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    System.out.println(hotelDoc);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1)准备Request对象。这次是查询,所以是GetRequest
  • 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
  • 3)解析结果,就是对JSON做反序列化

3. 删除文档

@Test
void testDeleteDocument() throws IOException {
    //1. 准备Request
    DeleteRequest request = new DeleteRequest("hotel", "61083");
    // 发送请求
    client.delete(request, RequestOptions.DEFAULT);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
  • 2)准备参数,无参
  • 3)发送请求。因为是删除,所以是client.delete()方法

4. 修改文档

@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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1)准备Request对象。这次是修改,所以是UpdateRequest
  • 2)准备参数。也就是JSON文档,里面包含要修改的字段
  • 3)更新文档。这里调用client.update()方法

5. 批量导入文档

@Test
void name() throws IOException {
    //批量查询数据
    List<Hotel> hotels = hotelService.list();

    //1.创建Request请求
    BulkRequest request = new BulkRequest();

    //2.准备参数,添加多个新增的Request
    for (Hotel hotel : hotels) {
    //转化为文档类型的HotelDoc
    HotelDoc hotelDoc = new HotelDoc(hotel);
    String json = JSON.toJSONString(hotelDoc);
    // 创新新增文档的Request对象
    request.add(new IndexRequest("hotel")
                    .id(hotel.getId().toString())
                    .source(json, XContentType.JSON));
    }

    //3. 发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 利用mybatis-plus查询酒店数据

  • 将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)

  • 利用JavaRestClient中的BulkRequest批处理,实现批量新增文档


6. 总结

  • 新增Index
  • 查询用Get
  • 更新用Update
  • 删除用Delete
  • 批量用Bulk

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
  

闽ICP备14008679号