Bootstrap

Elasticsearch(高性能分布式搜索引擎)-上篇

Elasticsearch(高性能分布式搜索引擎)


黑马商城作为一个电商项目,商品的搜索肯定是访问频率最高的页面之一。目前搜索功能是基于数据库的模糊搜索来实现的,存在很多问题。

首先,查询效率较低。

由于数据库模糊查询不走索引,在数据量较大的时候,查询性能很差。黑马商城的商品表中仅仅有不到9万条数据,基于数据库查询时,搜索接口的表现如图:

在这里插入图片描述

改为基于搜索引擎后,查询表现如下:

需要注意的是,数据库模糊查询随着表数据量的增多,查询性能的下降会非常明显,而搜索引擎的性能则不会随着数据增多而下降太多。目前仅10万不到的数据量差距就如此明显,如果数据量达到百万、千万、甚至上亿级别,这个性能差距会非常夸张。

其次,功能单一

数据库的模糊搜索功能单一,匹配条件非常苛刻,必须恰好包含用户搜索的关键字。而在搜索引擎中,用户输入出现个别错字,或者用拼音搜索、同义词搜索都能正确匹配到数据。

综上,在面临海量数据的搜索 ,或者有一些复杂搜索需求的时候,推荐使用专门的搜索引擎来实现搜索功能。

目前全球的搜索引擎技术排名如下:

在这里插入图片描述

1 初识elasticsearch

1.1 认识和安装

elasticsearch结合kibana、Logstash、Beats,是一整套技术栈,被叫做ELK。被广泛应用在日志数据分析、实时监控等领域。

1.2 倒排索引

传统数据库(如MySQL)采用正向索引,例如给下表(tb goods)中的id创建索引:

在这里插入图片描述

elasticsearch采用倒排索引:

  • 文档(document):每条数据就是一个文档
  • 词条(term):文档按照语义分成的词语

在这里插入图片描述

1.3 IK分词器

中文分词往往需要根据语义分析,比较复杂,这就需要用到中文分词器,例如IK分词器。IK分词器是林良益在2006年开源发布的,其采用的正向迭代最细粒度切分算法一直沿用至今。

在Kibana的DevTools中可以使用下面的语法来测试IK分词器:

语法说明:

  • POST:请求方式
  • /_analyze:请求路径,这里省略了http://虚拟机ip:9200 有kibana帮我们补充
  • 请求参数,json风格:
    • analyzer:分词器类型,这里默认是standard分词器
    • text:要分词的内容

在这里插入图片描述

1.4 基础概念

elasticsearch中的文档数据会被序列化为json格式后存储在elasticsearch中。

在这里插入图片描述

索引(index): 相同类型的文档的集合,可以称为数据库

映射(mapping): 索引中文档的字段约束信息,类似表的结构约束

在这里插入图片描述

1.4.1 elasticsearch与数据库对比

在这里插入图片描述

2 索引库的操作

2.1 Mapping映射属性

mapping是对索引库中文档的约束,常见的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": "黑马程序员Java讲师",
    "email": "[email protected]",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

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

字段名字段类型类型说明是否 参与搜索是否参与分词分词器
ageinteger整数——
weightfloat浮点数——
isMarriedboolean布尔——
infotext字符串,但需要分词IK
emailkeyword字符串,但是不分词——
scorefloat只看数组中元素类型——
firstNamekeyword字符串,但是不分词——
lastNamekeyword字符串,但是不分词——

name是个object类型,有两个properties,每个子字段都需要单独指定类型

2.2 索引库操作

Elasticsearch提供的所有API都是Restful的接口,遵循Restful的基本规范:

在这里插入图片描述

创建索引库和mapping的请求语法如下:

在这里插入图片描述
在这里插入图片描述

#创建索引库并设置mapping映射
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_smart",
        "index": true
      },
      "age":{
        "type": "byte"
      },
      "email":{
        "type": "keyword",
        "index": false
      },
      "name":{
        "type": "object",
        "properties": {
          "firstName": {
            "type": "keyword"
          },
          "lastName":{
            "type": "keyword"
          }
        }
      }
    }
  }
}

#查询索引库
GET /heima

#删除索引库
DELETE /heima

索引库和mapping一旦创建无法修改,但是可以添加新的字段,语法如下:

在这里插入图片描述
在这里插入图片描述

3 文档操作

3.1 文档CRUD

新增文档的请求格式如下:

在这里插入图片描述

查看文档请求格式:

删除索引库的请求格式:

# 新增文档
POST /heima/_doc/1
{
  "info": "黑马程序员Java讲师",
  "age": 35,
  "email": "[email protected]",
  "name": {
    "first": "云",
    "lastname": "赵"
  }
}

# 查询文档
GET /heima/_doc/1

# 删除文档
DELETE /heima/_doc/1

修改索引库:

  • 方式一:全量修改,会删除旧文档,添加新文档

    # 全量修改
    PUT /heima/_doc/1
    {
        "info": "黑马程序员JAVA讲师",
      "age": 40,
      "email": "[email protected]",
      "name": {
        "first": "云",
        "lastname": "赵"
      }
    }
    

    如果修改的id不存在,则会直接创建一个此id的新文档

  • 方式二:增量修改,修改指定字段值

    # 增量修改
    POST /heima/_update/1
    {
      "doc": {
        "email": "[email protected]"
      }
    }
    

3.2 批量处理

Elasticsearch中允许通过一次请求中携带多次文档操作,也就是批量处理,语法格式如下:

# 批量新增
POST /_bulk
{"index":{"_index":"heima","_id":"3"}}
{"info":"黑马程序员C++讲师","email":"[email protected]","name":{"firstName":"五","lastName":"王"}}
{"index":{"_index":"heima","_id":"4"}}
{"info":"黑马程序员前端讲师","email":"[email protected]","name":{"firstName":"三","lastName":"张"}}

# 批量删除
POST /_bulk
{"delete":{"_index":"heima","_id":"3"}}
{"delete":{"_index":"heima","_id":"4"}}

4 JavaRestClient

4.1 客户端初始化

Elasticsearch目前最新版本是8.0,其]ava客户端有很大变化。不过大多数企业使用的还是8以下版本,所以我们选择使用早期的)avaRestClient客户端来学习

>

  1. 引入es的RestHighLevelClient依赖:

    D

  2. 因为SpringBoot默认的ES版本是7.17.0,所以需要覆盖默认的ES版本:

  3. 初始化RestHighLevelClient:

在这里插入图片描述

4.2 商品表Mapping映射

要实现商品搜索,那么索引库的字段肯定要满足页面搜索的需求:

在这里插入图片描述

#商品索引库
PUT /hmall
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_smart"
      },
      "price":{
        "type": "integer"
      },
      "image":{
        "type": "keyword",
        "index":false
      },
      "category":{
        "type":"keyword"
      },
      "brand":{
        "type": "keyword"
      },
      "sold":{
        "type": "integer"
      },
      "commentCount":{
        "type": "integer",
        "index": false
      },
      "isAD":{
        "type": "boolean"
      },
      "updateTime":{
        "type": "date"
      }
    }
  }
}

4.3 索引库操作

创建索引库 的Java与Restful接口API对比:

删除索引库:

查询索引库信息:

public class ElasticTest {

    private RestHighLevelClient client;

    @Test
    void testConnection(){
        System.out.println("client=" + client);
    }

    @Test
    void testCreateIndex() throws IOException {
        //1. 准备Request对象
        CreateIndexRequest request = new CreateIndexRequest("items");
        //2. 准备请求参数
        request.source(MAPPING_TEMPLATE, XContentType.JSON);
        //3. 发送请求
        client.indices().create(request, RequestOptions.DEFAULT);
    }

    @Test
    void testGetIndex() throws IOException {
        //1. 准备Request对象
        GetIndexRequest request = new GetIndexRequest("items");
        //2. 发送请求
        boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
        System.out.println(exists);
    }

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

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

    @AfterEach
    void tearDown() throws IOException {
        if(client != null){
            client.close();
        }
    }

    private static final String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\": {\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_smart\"\n" +
            "      },\n" +
            "      \"price\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"image\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\":false\n" +
            "      },\n" +
            "      \"category\":{\n" +
            "        \"type\":\"keyword\"\n" +
            "      },\n" +
            "      \"brand\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"sold\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"commentCount\":{\n" +
            "        \"type\": \"integer\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"isAD\":{\n" +
            "        \"type\": \"boolean\"\n" +
            "      },\n" +
            "      \"updateTime\":{\n" +
            "        \"type\": \"date\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";
}

在这里插入图片描述

4.4 文档操作

新增文档 的javaAPI如下:

删除文档 的JavaAPI如下:

查询文档 包含查询和解析响应结果两部分,对应的JavaAPI如下:

**文档操作: **

修改文档那个数据有两种方式:

  • 方式一:全量更新,再次写入id一样的文档,就会删除旧文档,添加新文档。与新增的JavaAPI一致
  • 方式二:局部更新。只更新指定部分字段

@SpringBootTest(properties = "spring.profiles.active=local")
public class ElasticDocumentTest {

    private RestHighLevelClient client;

    @Autowired
    private IItemService iItemService;

    @Test
    void testIndexDoc() throws IOException {
        //0. 准备文档数据
        //0.1 根据id查询数据库数据
        Item item = iItemService.getById(100000011127L);
        //0.2 把数据库数据转为文档数据
        ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);

        //设置此项,则会修改索引库中对应文档的数据,相当于修改操作
        itemDoc.setPrice(29900);

        //1. 准备Request
        IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
        //2. 准备请求参数
        request.source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON);
        //3. 发送请求
        client.index(request, RequestOptions.DEFAULT);

    }

    @Test
    void testUpdateDoc() throws IOException{
        //1. 准备Request
        UpdateRequest request = new UpdateRequest("items", "100000011127");
        //2. 准备请求的参数
        request.doc(
                "price",25600,
                "sold",45000
        );
        //2. 发送请求
        client.update(request,RequestOptions.DEFAULT);
    }

    @Test
    void testGetDoc() throws IOException {
        //1. 准备Request
        GetRequest request = new GetRequest("items","100000011127");
        //2. 发送请求
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        //3. 解析响应结果
        String json = response.getSourceAsString();
        ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
        System.out.println("doc = " + doc);
    }

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

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

    @AfterEach
    void tearDown() throws IOException {
        if (client != null) {
            client.close();
        }
    }

}

4.5 批处理

批处理代码流程与之前类似,只不过构建请求会用到一个名为BulkRequest来封装普通的CRUD请求:

批处理的API实例:

    @Test
    void testBulkDoc() throws IOException {
        int pageNo = 1, pageSize = 500;
        //1. 准备文档数据
        Page<Item> page = iItemService.lambdaQuery()
                .eq(Item::getStatus, 1)
                .page(Page.of(pageNo, pageSize));
        List<Item> records = page.getRecords();
        if(records == null || records.isEmpty()){
            return;
        }
        //1. 准备Request
        BulkRequest request = new BulkRequest();
        //2. 准备请求参数
        for (Item item : records) {
            request.add(new IndexRequest("items")
                    .id(item.getId().toString())
                    .source(JSONUtil.toJsonStr(BeanUtil.copyProperties(item,ItemDoc.class)),XContentType.JSON));
        }
//        request.add(new DeleteRequest("items").id("1"));
        //3. 发送请求
        client.bulk(request, RequestOptions.DEFAULT);
    }
;