Loading

谷粒商城-elasticsearch入门

Elasticsearch简介

1 概念

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。

2 和MySQL的检索对比

MySQL中也有检索数据的方式,可是为什么不适合呢?因为术业有专攻,MySQL是专攻于数据的持久化的存储与管理,但是要做海量数据的检索和分析,Elasticsearch更在行。

3 基本概念

概述

Elasticsearch是面向文档的,这意味着它可以存储整个对象或文档。然而它不仅仅是存储,还会索引每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库如下:

关系型数据库:Database --> Table --> Row --> Column

Elasticsearch:Index --> Type --> Document --> Field

核心概念

Index(索引)

动词,相当于MySQL的insert

名称,相当于MySQL的Database

Type(类型)

在index(索引)中,可以定义一个或多个类型。

类似于MySQL中的Table;每一种类型的数据放在一起。

Document(文档)

保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是Json格式的,Document就像是MySQL中的某个Table里面的内容。

image-20220928100921418

倒排索引机制

image-20220928102204788

安装Elasticsearch

1 下载镜像文件

// 安装elasticsearch
docker pull elasticsearch:7.4.2

// 安装Elasticsearch的开源分析和可视化平台
docker pull kibana:7.4.2

2 创建实例

ElasticSearch

// 提前创建相关目录,为了将elasticsearch的配置和数据挂载到w
// -p:创建多级不存在的目录
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data

// 允许被远程的任何机器访问(注意:冒号后面需要一个空格——yml的格式)
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

// 9200:接收api请求的端口,9300:分布式集群下节点之间的通信端口
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
属性 含义
"discovery.type=single-node" 单节点启动
ES_JAVA_OPTS="-Xms64m -Xmx128m" -Xms64m:启动时占用64M,最大128M。(不指定的话,elasticsearch启动后会将所有内存占掉)

image-20220928113732695

注:想要外部访问虚拟机内的elasticsearch,可能还需要开放防火墙的部分端口

// 查看防火墙状态
systemctl status firewalld

// 开启一个端口
// 1.添加端口,--permanent表示永久生效,没有此参数重启后会无效
firewall-cmd --zone=public --add-port=9200/tcp --permanent
// 2.添加端口外部访问权限
firewall-cmd --add-port=9200/tcp
// 3.重新载入,添加端口后重新载入才能生效
firewall-cmd --reload

然后再外部访问到此数据才算成功

image-20220928114230976

Kibana

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.121.138:9200 \
-p 5601:5601 \
-d kibana:7.4.2

http://192.168.121.138记得要改成自己的虚拟机地址

kibana可视化页面汉化方法:https://blog.csdn.net/qq_42184699/article/details/90934018

image-20220928144835743

image-20220928144925348

初步检索

1 _cat

GET / _cat/nodes:查看集群中所有节点

GET / _cat/health:查看es健康状况

GET / _cat/master:查看主节点

GET / _cat/indecis:查看所有索引(类似于 show databases;)

2 索引一个文档(保存)

保存一个数据,保存在哪个索引的哪个类型下,指定用哪一个唯一标识。

// 在customer索引下的external类型下保存1号数据
POST(PUT) / customer/external/1

// 请求体
{
	"name": "John Doe"
}

POST和PUT都可以。

POST新增。如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号。

PUT可以新增也可以修改。PUT必须指定id,由于PUT需要指定id,不指定id会报错,所以一般用于修改。

image-20220928153240119

对POST请求来说,不指定id,发送多少次都是新增操作。

而对于PUT请求来说,需要指定id,发送多次,是更新操作。POST指定id的话,那也是更新操作。

image-20220928155221274

image-20220928155822559

3 查询文档

GET / customer/external/1

image-20220928160845829

老版是使用版本号做乐观锁,新版使用了序列号。

使用乐观锁更新的话,需要在保存语句后加上 ?if_seq_no=0&if_primary_term=1

4 更新文档

// 带_update的POST更新
POST / customer/external/1/_update

// 请求体
{
    "doc":{
        "name": "John Doe1"
    }
}

image-20220928162442793

带_update的POST更新,在更新之前会对比原来的数据,如果更新的数据与原来一样的话,就什么都不做,version、seq_no都不变。

// 不带_update的POST更新
POST / customer/external/1

// 请求体
{
	"name": "John Doe2"
}

image-20220928163124115

不带_update的POST更新,不会做对比,而是直接更新,version、seq_no都增加。

// 不带_update的PUT更新
PUT / customer/external/1

// 请求体
{
	"name": "John Doe3"
}

不带_update的PUT更新和 “不带_update的POST更新” 效果相同。

5 删除文档

DELETE / customer/external/1
DELETE / customer

image-20220928165322390

删除不存在的文档,result会显示“not_found”

6 批量操作

POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe"}
{"index":{"_id":"2"}}
{"name": "Jane Doe"}

两行为一个整体。

每一个整体都是独立操作的,不同于事务。无论前面的是否成功,后面的依然执行。

image-20220928172126911

image-20220928172121705

// 语法格式
{action:{metadata}}\n
{requestbody		}

{action:{metadata}}\n
{requestbody		}

7 导入样本测试数据

POST /bank/account/_bulk

测试数据:https://gitee.com/xlh_blog/common_content/blob/master/es测试数据.json#

进阶检索

1 SearchAPI

es支持两种基本方式检索:

  • 一个是通过 REST request URI 发送搜索参数(uri + 检索参数)
  • 另一个是通过使用 REST request body 来发送它们(uri + 请求体)

检索信息

  • 一切检索从_search开始
语句 含义
GET /bank/_search 检索bank下所有信息,包括type和docs
GET /bank/_search?q=*&sort=account_number:asc 请求参数方式检索

q=* :查询所有

sort=account_number:asc :安装account_number字段排序,升序排序

asc :升序排序

  • uri + 请求体进行检索
GET /bank/_search
{
    "query":{
        "match_all":{}
    },
    "sort":[
        {
            "account_number":"asc"
        }
    ]
}

image-20220930102718742

2 Query DSL

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/getting-started.html

基本语法格式

Elasticsearch提供了一个可以执行查询的Json风格的DSL(domain-specific language 领域特定语言)。这个被称为Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂,真正学好它的方法是从一些基础的示例开始的。

  • 一个查询语句的典型结构:
{
    QUERY_NAME:{
        ARGUMENT:VALUE,
        ARGUMENT:VALUE,
        ...
    }
}

image-20220930112704527

  • 如果是针对某个字段,那么它的结构如下:
{
    QUERY_NAME:{
        FIELD_NAME:{
            ARGUMENT:VALUE,
            ARGUMENT:VALUE,
            ...
        }
    }
}

image-20220930113118243

image-20220930113453600

返回部分字段

{
    "_source": ["field", ...]
}

image-20220930113734947

match匹配查询

这种匹配是全文检索。

  • 匹配数值类型是精确匹配

image-20220930114329023

  • 匹配字符串类型是模糊查询

若传了两个单词,则会 按空格分词 + 全文检索

image-20220930114542862

_score :得分

max_score :最高得分

全文检索会按照得分进行排序

match_phrase短语匹配

将需要匹配的值当成一个整体单词,不分词进行匹配

image-20220930115449192

multi_match多字段匹配

这个也是会进行 按空格分词 + 检索

image-20220930120146210

bool复合查询

bool用来做复合查询:

复合语句可以合并任何其他查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

bool中的条件是且的关系。

一个match中只能写一个条件,多个条件要多个match。

image-20220930142156340

满足should的能够提高相关性得分。must和must_not也能够提供相关性得分。

filter过滤

should、must、must_not都能够筛选,并且提供相关性得分。

filter也是过滤,功能类似于must,但是不提供相关性得分。

image-20220930144707996

term精确匹配

和match一样,匹配某个属性的值。全文检索用match,其他非text字段使用term更好。

image-20220930145901555

aggregation执行聚合

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL GROUP BY和SQL聚合函数。在Elasticsearch中,有执行搜索并返回hits(命中结果),并且同时返回聚合结果,把一个响应中的hits分隔开的能力。这是非常强大且有效的,你可以执行查询和多个聚合,并且在一次使用中得到各自的返回结果,使用一次简洁和简化的API来避免网络往返。

  • 搜索address中包含mill的所有人的年龄分布以及平均年龄,但是不显示这些人的详情。

image-20220930153240111

terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。

  • 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资。

image-20220930155301732

  • 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资。

多重聚合。

# 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资。
GET /bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "ageAgg": {
      "terms": {
        "field": "age",
        "size": 100
      },
      "aggs": {
        "genderAgg": {
          "terms": {
            "field": "gender.keyword",
            "size": 2
          },
          "aggs": {
            "balanceAgg": {
              "avg": {
                "field": "balance"
              }
            }
          }
        }
      }
    },
    "totalAvgAgg":{
      "avg": {
        "field": "balance"
      }
    }
  }
}

映射

1 Elasticsearch7中已经去掉了type概念

image-20220930161501711

2 创建索引并指定映射

作用:定义数据库中表的结构,通过mapping来控制索引存储数据的位置。不做映射直接创建索引的话,索引会帮你自动映射,但是映射的类型不一定跟你想的一样。

  • 定义Index下的字段名(Field Name)
  • 定义字段的类型,比如integer、text、keyword。(text会全表检索、keyword是精确检索)
  • 定义倒排索引相关的配置,比如documentId、记录position、打分等。

创建索引不建议再带上类型(type)了。

image-20220930163900597

3 在索引内添加一个新的映射字段

PUT /my_index/_mapping
{
  "properties" :{
    "employee-id":{
      "type": "keyword",
      "index": false
    }
  }
}

image-20220930165141279

4 修改映射&数据迁移

修改映射

对于已经存在的映射字段,我们不能更新。更新必须创建新的索引并进行数据迁移。

数据迁移

image-20220930170036592

image-20220930170129932

image-20220930170454338

// 数据迁移,固定写法。
POST _reindex
{
  "source": {
    "index": "bank"
  },
  "dest": {
    "index": "newbank"
  }
}
// 由于我们是用老版本(索引下带类型)迁移到新版本(无类型),所以需要加一个type
POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}

image-20220930170823094

想一个问题,为什么我们经常重启es,但是它里面的数据不丢失呢?

因为我们在创建容器的时候就将它的数据挂载到外部了,所以重启容器,数据不会丢失。

分词

一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。

例如,tokenizer 遇到空白字符时分割文本。它会将文本“Quick brown fox!”分割为[Quick, brown, fox!]。

tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于phrase短语和word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的start(起始)和end(结束)的character offsets(字符偏移量)(用于高亮显示搜索的内容)。Elasticsearch提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)。

standard分词器

image-20221008164940536

image-20221008165249770

标准分词器,但是它是支持英文的,不太支持中文。

所以我们需要一款能够识别中文的分词器,ik分词器。

ik分词器

安装ik分词器

下载链接:https://github.com/medcl/elasticsearch-analysis-ik/releases?page=7

要选择跟elasticsearch相同的版本

  1. 进入Linux虚拟机,进入和docker容器挂载到外面的elasticsearchplugins目录中,创建一个文件夹ik,并进入文件夹

    mkdir ik
    cd ik
    
  2. 下载ik分词器的压缩包

    wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
    
  3. 解压并删除压缩包

    unzip elasticsearch-analysis-ik-7.4.2.zip
    rm -f elasticsearch-analysis-ik-7.4.2.zip
    
  4. 重启容器(重启之前可以进入容器内部康康是否安装成功)

使用ik分词器

ik分词器提供了两个分词算法ik_smartik_max_word,其中 ik_smart 为最少切分,ik_max_word为最细粒度划分

image-20221008174001060

image-20221008173940133

从效果可以看出来,比上面的standard分词器要好很多。

自定义扩展词库

image-20221008174138311

ik分词器默认的词库并不支持一些新的词汇,比如“尚硅谷”,ik分词器的词库中是没有的。所以很多网络用语、新型词汇,ik分词器都不能识别,我们就需要自己来扩展它的词库。

安装nginx

  1. 随便创建并启动一个nginx实例,只是为了复制出配置

    docker pull nginx:1.10
    docker run -p 80:80 --name nginx -d <nginx容器id>
    
  2. 将容器内的配置文件拷贝到当前目录,并修改文件夹名称

    // 先创建一个文件夹 /mydata/nginx
    mkdir /mydata/nginx
    // 进入该文件夹
    cd /mydata/nginx
    // 拷贝目录
    docker container cp nginx:/etc/nginx .
    // 修改拷贝出来的文件夹名称为conf
    mv nginx conf
    
  3. 终止原容器,并删除原容器

    docker stop <nginx容器id>
    docker rm <nginx容器id>
    
  4. 创建新的nginx容器

    docker run -p 80:80 --name nginx \
    -v /mydata/nginx/html:/usr/share/nginx/html \
    -v /mydata/nginx/logs:/var/log/nginx \
    -v /mydata/nginx/conf:/etc/nginx \
    -d <nginx容器id>
    

此时尝试访问nginx:

image-20221009104533485

403是因为这个nginx没有设置index.html。所以我们到挂载的目录中新建一个index.html

image-20221009105621280

配置自定义词库

  1. nginxhtml目录下,新建一个es目录,在es目录中新建一个fenci.txt,里面写入自定义的词库,然后保存退出。这时候使用路径可以直接访问该文件。

    image-20221009111210155

  2. 修改配置文件,配置远程自定义字典

    image-20221009111642056

    image-20221009111754295

    远程字典的位置就是刚才能够直接访问的词库地址。

  3. 重启es。

    docker restart <es容器id>
    
  4. 测试

    image-20221009113253645

如果分词失败的很可能是编码问题。需要将虚拟机内部的编码改为utf-8的编码方式。

具体操作可参考:https://blog.csdn.net/dreaming317/article/details/120839655

springboot整合

操作库的选择

9200:HTTP

JestClient:非官方,更新慢

RestTemplate:模拟HTTP请求,es很多操作需要自己封装,麻烦

HttpClient:同上

Elasticsearch-Rest-Client:官方RestClient,封装了es操作,API层次分明,上手简单

整合

  1. 再单独创建一个检索服务

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6kZsgpmG-1665370313097)(https://img-beg.oss-cn-hangzhou.aliyuncs.com/img/创建检索服务.gif)]

  2. 引入依赖。我的elasticsearch的版本是7.4.2的,所以依赖的版本也要相同。

    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-high-level-client</artifactId>
        <version>7.4.2</version>
    </dependency>
    
  3. 打开maven可以看到,elasticsearch的版本却不是7.4.2,所以需要修改elasticsearch的版本

    image-20221009145313852

    image-20221009145424472

    SpringBoot已经对elasticsearch的版本做了管理。

  4. 直接在pom.xml中添加指定版本号

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

    image-20221009145807164

  5. 编写配置,给容器中注入一个RestHighLevelClient

    package com.example.gulimall.search.config;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestClientBuilder;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class GulimallElasticsearchConfig {
        @Bean
        public RestHighLevelClient esRestClient(){
            RestClientBuilder builder = null;
            builder = RestClient.builder(new HttpHost("192.168.121.138", 9200, "http"));
            RestHighLevelClient client = new RestHighLevelClient(builder);
            return client;
        }
    }
    
  6. 测试

    @SpringBootTest
    public class GulimallSearchApplicationTests {
    
        @Autowired
        private RestHighLevelClient client;
    
        @Test
        public void contextLoads() {
            System.out.println(client);
        }
    }
    

    image-20221009161720544

    注意pom.xml中的各种配置,特别是SpringBoot和SpringCloud的版本冲突问题。

测试保存数据

官网Java API文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-document-index.html

所有的RestHighLevelClient都可以使用RequestOptions来定义你的所有请求,并且其不会改变Elasticsearch执行请求的方式。所以我们需要定义一个RequestOptions

  1. 定义RequestOptions
@Configuration
public class GulimallElasticsearchConfig {

    public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
//        builder.addHeader("Authorization", "Bearer " + TOKEN);
//        builder.setHttpAsyncResponseConsumerFactory(
//                new HttpAsyncResponseConsumerFactory
//                        .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
        COMMON_OPTIONS = builder.build();
    }
}
  1. 测试
@SpringBootTest
public class GulimallSearchApplicationTests {

    @Autowired
    private RestHighLevelClient client;

    @Test
    public void indexData() throws IOException {
        // 一个参数:索引的名称,不存在会自动创建
        IndexRequest request = new IndexRequest("users");
        // 指定要插入数据的id,不指定则自动生成
        request.id("1");
        // 实例化一个user
        User user = new User();
        user.setName("李四");
        user.setAge(22);
        user.setGender("男");
        // 将user对象解析为json
        String userJson = JSON.toJSONString(user);
        // 构建文档(这个json就是文档)
        request.source(userJson, XContentType.JSON);
        // 同步执行,使用定义的RequestOptions
        IndexResponse indexResponse = client.index(request, GulimallElasticsearchConfig.COMMON_OPTIONS);
        System.out.println(indexResponse);
    }

    @Data
    static class User{
        private String name;
        private String gender;
        private int age;
    }
}

测试检索数据

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-search.html

// 搜索address中包含mill的所有人的年龄分布以及平均年龄
GET /bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "ageAggs": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg":{
      "avg": {
        "field": "age"
      }
    }
  }
}

使用Java来完成上方的检索

@Test
public void searchData() throws IOException{
    // 指定查询bank索引
    SearchRequest searchRequest = new SearchRequest("bank");
    SearchSourceBuilder builder = new SearchSourceBuilder();
    // 查询address中包含mill的所有人
    MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "mill");
    // 查询年龄分布。使用term聚合函数,函数名称为“ageAggs”;
    // terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
    TermsAggregationBuilder ageAggs = AggregationBuilders.terms("ageAggs");
    ageAggs.field("age");
    ageAggs.size(10);
    // 查询平均年龄。
    AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg");
    ageAvg.field("age");
    builder.query(matchQuery).aggregation(ageAggs).aggregation(ageAvg);
    // 构建文档
    searchRequest.source(builder);
    // 同步执行
    SearchResponse response = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
    System.out.println(response);
}

和MP的条件构造器挺相似的。

image-20221010100136174

获取解析信息

/**
     * 搜索address中包含mill的所有人的年龄分布以及平均年龄
     */
@Test
public void searchData() throws IOException {
    // 指定查询bank索引
    SearchRequest searchRequest = new SearchRequest("bank");
    SearchSourceBuilder builder = new SearchSourceBuilder();
    // 查询address中包含mill的所有人
    MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "mill");
    // 查询年龄分布。使用term聚合函数,函数名称为“ageAggs”;
    // terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
    TermsAggregationBuilder ageAggs = AggregationBuilders.terms("ageAggs");
    ageAggs.field("age");
    ageAggs.size(10);
    // 查询平均年龄。
    AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg");
    ageAvg.field("age");
    builder.query(matchQuery).aggregation(ageAggs).aggregation(ageAvg);
    // 构建文档
    searchRequest.source(builder);
    // 同步执行
    SearchResponse response = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
    //        System.out.println(response);
    SearchHit[] searchHits = response.getHits().getHits();
    for (SearchHit hit : searchHits) {
        // hit.getIndex();hit.getId();hit.getScore();
        // _source
        String sourceString = hit.getSourceAsString();
        Account account = JSON.parseObject(sourceString, Account.class);
        System.out.println("account=" + account);
    }
    // 获取聚合结果
    Aggregations aggregations = response.getAggregations();
    // lterms#ageAggs
    Terms termsAgeAvg = aggregations.get("ageAggs");
    for (Terms.Bucket bucket : termsAgeAvg.getBuckets()) {
        String key = bucket.getKeyAsString();
        long docCount = bucket.getDocCount();
        System.out.println("年龄:" + key + ",出现次数:" + docCount);
    }
	// avg#ageAvg
    Avg avgAgeAvg = aggregations.get("ageAvg");
    double value = avgAgeAvg.getValue();
    System.out.println("平均年龄:" + value);
}

image-20221010104734639

posted @ 2022-10-10 10:56  KledKled  阅读(90)  评论(0编辑  收藏  举报