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


    微服务入门: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.clientgroupId>
        <artifactId>elasticsearch-rest-high-level-clientartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

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

    <properties>
        <java.version>1.8java.version>
        <elasticsearch.version>7.12.1elasticsearch.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

  • 相关阅读:
    java毕业设计——基于Java+Javamail的邮件收发系统设计与实现(毕业论文+程序源码)——邮件收发系统
    LeetCode //C - 109. Convert Sorted List to Binary Search Tree
    解决Docker启动之npm版本不兼容问题
    【Spring】依赖注入(set ,
    [Linux入门]---管理者操作系统
    多台云服务器的 Kubernetes 集群搭建
    【RUST】HashMap、vector和String
    参与开源之夏 x OpenTiny 跨端跨框架 UI 组件库贡献,可以赢取奖金🏆!这份《OpenTiny 开源贡献指南》请收好🎁!
    Linux常用命令——clear命令
    机器学习:基于梯度下降算法的线性拟合实现和原理解析
  • 原文地址:https://blog.csdn.net/weixin_51146329/article/details/125901581