分布式高级篇1 —— 全文检索

news2025/6/8 11:46:15

Elasticsearch

  • Elasticsearch
  • 简介
  • 一、基本概念
    • 1、index(索引)
    • 2、Type(类型)
    • 3、Document(文档)
    • 4、倒排索引
  • 二、Docker 安装 EL
    • 1、拉取镜像
    • 2、创建实例
  • 三、初步探索
    • 1、_cat
    • 2、索引一个文档(保存)
    • 3、查询文档
    • 3、更新文档
    • 4、删除文档&索引
    • 5、_bulk 批量 AP
    • 6、样本测试数据
  • 四、进阶检索
    • 1、 Search API
    • 2、Query DSL
      • (1)基本语法格式
      • (2)返回部分字段
      • (3)match 匹配查询
      • (4)match_phrase 短语匹配
      • (5)多字段匹配
      • (6)bool 复合查询
      • (7)filter 结果过滤
      • (8)term
      • 扩展:keyword
      • (9) aggregations(执行聚合)
    • 3、Mapping 映射
      • (1)新版本改变
      • (2)创建索引并指定映射
      • (3)添加新字段映射
      • (4)更新字段映射
      • (5)数据迁移
    • 4、分词
      • (1)安装 ik 分词
      • (2)自定义词库
  • 五、Elasticsearch-Rest-Client
    • 1、SpringBoot 整合 ES
    • 2、使用 SpringBoot 测试 ElasticSearch
      • (1)增加索引
      • (2)更新索引
      • (3)删除索引
      • (4)复杂查询
  • 六、拓展:Docker 安装 Nginx

视频来源: 【Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目】

简介

Elasticsearch 是什么? | Elastic

全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它

image-20230107192048534

Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的 接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。 REST

官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html

官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html

社区中文: https://es.xiaoleilu.com/index.html http://doc.codingdict.com/elasticsearch/0/

一、基本概念

1、index(索引)

动词,相当于 mysql 中的 insert

名词,相当于 mysql 中的 database

2、Type(类型)

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

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

3、Document(文档)

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

image-20230107194236454

4、倒排索引

Elasticsearch 检索速度快是因为使用了一种倒排索引的数据结构

保存的记录:

1-红海行动

2-探索红海行动

3-红海特别行动

4-红海记录篇

5-特工红海特别探索

Elasticsearch 会基于这些保存的数据额外维护一张表,这些表会将保存的数据拆分成单词,并记录命中的记录【如图所示】。

比如:红海行动 会拆分成 红海、行动 , 探索红海行动 被拆分成 探索、红海、行动

image-20230107194403167

当检索时

1)、红海特工行动?

会将检索的内容也拆分成单词,红海、特工、行动 ,并在表中查询每个单词所命中的记录。 并按照相关性得分排序,即每个文档跟查询的匹配程度

二、Docker 安装 EL

使用 su 切换到 root 用户登录

1、拉取镜像

docker pull elasticsearch:7.4.2  # 存储和检索数据 

docker pull kibana:7.4.2 # 可视化检索数据

2、创建实例

创建 EL

1、创建与elasticsearch挂载的目录

mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
mkdir -p /mydata/elasticsearch/plugins

2、修改权限,避免由于权限问题,elasticsearch报错

chmod -R 777 /mydata/elasticsearch/ 保证权限

3、0.0.0.0 是所有主机均可访问 elasticsearch 接口

echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

4、创建实例

9200:9200 提供给别的服务访问的接口

9300:9300 elasticsearch内部数据的访问

discovery.type=single-node 单节点模式启动

ES_JAVA_OPTS=“-Xms64m -Xmx512m” 设置 elasticsearch 启动所占用的内存。如果不设置会占用你所有的内存。导致OOM

docker run --name elasticsearch --restart=always -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-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	

5、访问 9200 端口

image-20230108162549529

创建 Kibana 实例

# 改成自己的 ip 地址
docker run --name kibana --restart=always -e ELASTICSEARCH_HOSTS=http://192.168.56.111:9200 -p 5601:5601 \
-d kibana:7.4.2

将 Kibana设置成中文

# 进入容器
docker exec -it Kibana容器id bash
cd config/
vi kibana.yml 
# 在配置文件中增加 
i18n.locale: "zh-CN"
#重启实例即可
docker restart 容器id

访问 IP地址:5601

image-20230108163414116

三、初步探索

1、_cat

只需要发送对应的请求即可。

image-20230108164107267

GET /_cat/nodes:查看所有节点 _

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

GET /_cat/master:查看主节点 _

GET /_cat/indices:查看所有索引 == show databases;

2、索引一个文档(保存)

PUT 方式

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

# 在 customer 索引下的 external 类型下保存 1 号数据
PUT customer/external/1
{ 
	"name": "John Doe"
}

(1)使用 PUT 请求带 id,第一次发送请求时增加操作,版本号为1.、第二次发送请求就是更新操作,版本号为2

image-20230108165216194

(2)PUT 方式必须携带ID,否则就会报错

image-20230108165353325

POST 方式

POST 请求也可以新增数据

# 在 customer 索引下的 external 类型下保存 1 号数据
POST customer/external/1
{ 
	"name": "John Doe"
}

(1)POST 请求携带 id ,当第一次发送POST请求是新增操作,当第二次发送POST请求是更新操作,并且版本号+1

image-20230108165650290

第二次携带 id 发送POST 请求

image-20230108165715367

(2)POST方式可以不携带 id ,el 会自动随机生成一个不重复的 id,也就是说,如果不携带ID发送 POST 请求,每一个都是新增

image-20230108165834962

3、查询文档

请求

GET /customer/external/1

返回结果

{ 
    "_index": "customer", //在哪个索引
    "_type": "external", //在哪个类型
    "_id": "1", //记录 id
    "_version": 2, //版本号
    "_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁
    "_primary_term": 1, //同上,主分片重新分配,如重启,就会变化
    "found": true, "_source": { //真正的内容
    	"name": "John Doe"
	}
}

使用 _seq_no + _primary_term 用来做乐观锁 :?if_seq_no=0&if_primary_term=1

假设请求A发送修改请求:

PUT http://192.168.56.111:9200/customer/external/1?if_seq_no=1&if_primary_term=1

返回结果:此时 _seq_no 被修改成了5

{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 3,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 5,
    "_primary_term": 1
}

请求B 同样发送修改请求

请求B认为没有别的请求修改数据,认为 _seq_no 仍然为 1

PUT http://192.168.56.111:9200/customer/external/1?if_seq_no=1&if__primary_term=1

返回结果:报错reason就是: 需要 seqNo [1] ,但是 seqNo [5]

{
    "error": {
        "root_cause": [
            {
                "type": "version_conflict_engine_exception",
                "reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [5] and primary term [1]",
                "index_uuid": "LDc8rC6ERbm0KN5XfSLmuQ",
                "shard": "0",
                "index": "customer"
            }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, required seqNo [1], primary term [1]. current document has seqNo [5] and primary term [1]",
        "index_uuid": "LDc8rC6ERbm0KN5XfSLmuQ",
        "shard": "0",
        "index": "customer"
    },
    "status": 409
}

3、更新文档

第一种方式: POST请求带_update

POST customer/external/1/_update
{ 
	"doc":{ 
		"name": "John Doew"
	}
}

第二种方式: POST请求不带_update

POST customer/external/1
{ 
    "name": "John Doe2"
}

第三种方式: PUT请求不带_update

PUT customer/external/1
{ 
    "name": "John Doe"
}

三者区别:

POST请求带_update 会对比源文档数据,如果一样,就不进行任何操作。 包括 version、 _seq_no 也不会变化

不带 _update ,每发送一次请求,version、 _seq_no 都会变化

{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 12,
    "result": "noop", // 结果是no operation,没有进行任何操作
    "_shards": {
        "total": 0,
        "successful": 0,
        "failed": 0
    },
    "_seq_no": 14,
    "_primary_term": 1
}

更新时增加属性:

POST customer/external/1/_update
{ 
	"doc": { "name": "Jane Doe", "age": 20 }
}

POST/PUT 不带 _update 也可以

POST customer/external/1
{ 
	"name": "Jane Doe", "age": 20
}


PUT customer/external/1/
{ 
	"name": "Jane Doe", "age": 20
}

4、删除文档&索引

删除文档

DELETE customer/external/1

返回结果

{
    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 13,
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 15,
    "_primary_term": 1
}

删除索引

DELETE customer

返回结果

{
    "acknowledged": true
}

5、_bulk 批量 AP

语法格式:

action: 动作,可以是 delete-删除,index-新增,update-修改

metadata :操作的数据,比如

  • {“_id”:“1”} 操作 id 为1 的数据
  • {“_index”: “user”, " _type": “man”} 操作 索引为 user,类型为 man 下的所有数据

request body :请求体

{ action: { metadata }}
{ request body }

{ action: { metadata }}
{ request body }

案例演示1

// _bulk 表示批量操作
POST /customer/external/_bulk
//  index 新增,id=1
{"index":{"_id":"1"}}
//  请求体,新增的内容
{"name": "John Doe"}

{"index":{"_id":"2"}}
{"name": "Jane Doe"}

返回结果

bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败, 它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送 的顺序相同),所以可以检查是否一个指定的动作是不是失败了。

{
  "took" : 120, //花费时间
  "errors" : false, // 是否有异常
  "items" : [
    {
      "index" : {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "1",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "customer",
        "_type" : "external",
        "_id" : "2",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

案例演示2

第一个删除操作,第二个新增操作,第三个新增操作,第四个修改操作

POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123"}}
{ "doc" : {"title" : "My updated blog post"} }

6、样本测试数据

准备了一份顾客银行账户信息的虚构的 JSON 文档样本。每个文档都有下列的 schema (模式):

{ 
    "account_number": 0, 
    "balance": 16623, 
    "firstname": "Bradshaw", 
    "lastname": "Mckenzie", 
    "age": 29, 
    "gender": "F", 
    "address": "244 Columbus Place", 
    "employer": "Euron", 
    "email": "bradshawmckenzie@euron.com", 
    "city": "Hobucken", 
    "state": "CO"
}

测试数据:yangzhaoguang/ES-: ES测试数据 (github.com)

四、进阶检索

1、 Search API

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

  • 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)

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

(1)uri + 检索参数

// * 查询所有,sort 排序
GET bank/_search?q=*&sort=account_number:asc

返回结果

image-20230108202403087

took Elasticsearch 执行搜索的时间(毫秒)

time_out 告诉我们搜索是否超时

_shards 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片

hits 搜索结果 hits.total - 搜索结果

hits.hits - 实际的搜索结果数组(默认为前 10 的文档)

sort 结果的排序 key(键)(没有则按 score 排序)

score 和 max_score 相关性得分和最高得分(全文检索用)

(2)uri + 请求体

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "account_number": {
        "order": "desc"
      },
      "balance": {
        "order": "asc"
      }
    }
  ]
}

在请求体中 封装查询的条件,使用 Query DSL 语句

2、Query DSL

(1)基本语法格式

Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特 定语言)。这个被称为 Query DSL。

结构语法

{
    QUERY_NAME: {
    	ARGUMENT: VALUE, 
        ARGUMENT: VALUE,... 
    }
}

query 定义如何查询, match_all 查询类型【代表查询所有的所有】

sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准

“from”: 10,
“size”: 5

从第十条记录开始,查询五条数据。使用 from + size 可以达到分页效果。

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "balance": {
        "order": "desc"
      }
    }
  ],
  "from": 10,
  "size": 5
  
}

(2)返回部分字段

_source 可指定返回的字段

GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "balance": {
        "order": "desc"
      }
    }
  ],
  "from": 10,
  "size": 5,
  "_source": ["account_number","balance"]
}

(3)match 匹配查询

match 精确匹配

如果字段值是数字类型的,就是精确匹配。匹配 account_number = 2 的记录

# match 精确匹配
GET bank/_search
{
  "query": {
    "match": {
      "account_number": 2
    }
  }
}

match 模糊匹配

字段值是 字符串类型的,就是模糊匹配。值得注意的是,El 会将 Laurel Avenue 进行分词,将所有包含 Laurel 或者 Avenue 或者 Laurel Avenue 都查出来。

# match 模糊匹配
GET bank/_search
{
  "query": {
    "match": {
      "address": "Laurel Avenue"
    }
  }
}

(4)match_phrase 短语匹配

不会将查询条件分词 ,查询 address 包含 Laurel Avenue 短语 的记录

# match_phrase 短语匹配,不会分词
GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "Laurel Avenue"
    }
  }
}

(5)多字段匹配

query : 查询条件

fields 查询字段

multi_match 也会分词查询,address 和 city 里只要包含 Laurel、Belvoir、Laurel Belvoir 都能查询出来。

# multi_match 多字段匹配
GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "Laurel Belvoir",
      "fields": ["address","city"]
    }
  }
}

(6)bool 复合查询

bool 用来做复合查询 :

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

must: 必须符合的条件

查询的数据必须符合:gender为F,age为35 的记录

# bool 复合查询
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "F"
          }
        },
        {
          "match": {
            "age": "35"
          }
        }
      ]
    }
  }
}

must_not 必须不是指定的条件

查询的数据必须符合:gender为F,age为35 并且 city 不为 Ironton 的记录

# bool 复合查询
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "F"
          }
        },
        {
          "match": {
            "age": "35"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "city": "Ironton"
          }
        }
      ]
    }
  }
}

should 查询的数据当中应当包含此条件,如果包含此条件会提高相关性得分,不包含也是可以的。

# bool 复合查询
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "gender": "F"
          }
        },
        {
          "match": {
            "age": "35"
          }
        }
      ],
      "must_not": [
        {
          "match": {
            "city": "Ironton"
          }
        }
      ],
      "should": [
        {
          "match": {
            "city": "Darrtown"
          }
        }
      ]
    }
  }
}

(7)filter 结果过滤

过滤掉不符合条件的数据,和 must一样,但是 filter 不会计算相关性得分

查询出符合 10<=age<=30 的记录

# filter 不会计算相关性得分
GET bank/_search
{
  "query": {
    "bool": {
      "filter": {
        "range": {
          "age": {
            "gte": 10,
            "lte": 30
          }
        }
      }
    }
  }
}

(8)term

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

使用 match

GET bank/_search
{
  "query": {
    "match": {
      "address": "880 Holmes Lane"
    }
  }
}

返回结果 一共有 16 条数据。

image-20230109215731053

使用 term

GET bank/_search
{
  "query": {
    "term": {
      "address": "880 Holmes Lane"
    }
  }
}

返回结果 0 条数据

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

官方是这样说的:

避免使用 term 对文本字段查询

默认情况下,Elasticsearch会作为分析的一部分更改文本字段的值。这会使查找文本字段值的精确匹配变得困难。

对于查询文本字段,使用 match

image-20230109220204995

使用 term 查找非文本字段:

# 使用 term 查询非文本字段(精确查找)
GET bank/_search
{
  "query": {
    "term": {
      "age": "28"
    }
  }
}

因此以后规定使用 term 查询非文本字段,使用 match 查询文本字段。

扩展:keyword

每一个字段都有 keyword 属性,用来实现精确查找。

查询 address 为 880 Holmes Lane 的记录

# keywords
GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "880 Holmes Lane"
    }
  }
}

他 与 match_phrase 的区别?

match_phrase 指明查询条件是一个短语,是一个包含关系。而 keyword 指明查询条件是一个 equals 关系。

# keywords
GET bank/_search
{
  "query": {
    "match": {
      "address.keyword": "880 Holmes"
    }
  }
}

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address.keyword": "880 Holmes"
    }
  }
}

使用 keyword : 会查询出 address 与 880 Holmes 相等的记录

使用 match_phrase: 会查询出 address 包含 880 Holmes 短语的记录

(9) aggregations(执行聚合)

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

语法格式

文档:Aggregations | Elasticsearch Guide 7.4] | Elastic

"aggregations" : {
    "<aggregation_name>" : {
        "<aggregation_type>" : {
            <aggregation_body>
        }
        [,"meta" : {  [<meta_data_body>] } ]?
        [,"aggregations" : { [<sub_aggregation>]+ } ]?
    }
    [,"<aggregation_name_2>" : { ... } ]*
}

aggregation_name :为本次聚合设置一个名字(自定义)

aggregation_type : 聚合的类型(官方提供的聚合类型)

aggregation_body : 实现聚合

aggregation_name_2 : 可以设置多个聚合

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

GET bank/_search
{
  "query": {
    "match": {
      "address": "mill"
    }
  },
  "aggs": {
    "aggAge": {
      "terms": {
        "field": "age",
        "size": 10
      }
    },
    "ageAvg": {
      "avg": {
        "field": "age"
      }
    }
  },
  "size": 0
}
第一个聚合 aggAge 统计 age 的分布情况,并显示前10条记录
第二个聚合 ageAvg 统计 age 的平均年龄
"size": 0 不显示查询记录的详情

返回结果

image-20230109223435365

案例二:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资

在 aggAge 聚合里面嵌套 balanceAgg 聚合:先查看年龄分步情况,基于以上的结果,计算平均薪资

# **案例二**:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "aggs": {
    "aggAge": {
      "terms": {
        "field": "age",
        "size": 10
      },
      "aggs": {
        "balanceAgg": {
          "avg": {
            "field": "balance"
          }
        }
      }
    }
  },
  "size": 0
}

image-20230109224744972

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

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

image-20230109225434226

3、Mapping 映射

Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和 索引的。

比如:

  • 哪些字段是 text类型,哪些字段是 boolean 类型

ES 中的映射类型有:

image-20230110153321733

image-20230110153331139

(1)新版本改变

Es7 及以上移除了 type 的概念。

关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用, 但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。

  • 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed, 你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段 名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
  • 去掉 type 就是为了提高 ES 处理数据的效率。

(2)创建索引并指定映射

# 创建索引并指定映射
PUT /my_index
{
  "mappings": {
    "properties": {
      "age": {"type": "integer"},
      "address": {"type": "text"}
    }
  }
}

(3)添加新字段映射

“index”: false 表示不会被索引到

# 增加新的映射关系
PUT /my_index/_mapping
{
  "properties": {
    "email": {
      "type": "keyword",
      "index": false
    }
  }
}

(4)更新字段映射

对于已经存在的映射是无法更新的。否则就会报错

{
  "error": {
    "root_cause": [
      {
        "type": "resource_already_exists_exception",
        "reason": "index [my_index/c7ZZpqz9RzGdKJS_FTE3qA] already exists",
        "index_uuid": "c7ZZpqz9RzGdKJS_FTE3qA",
        "index": "my_index"
      }
    ],
    "type": "resource_already_exists_exception",
    "reason": "index [my_index/c7ZZpqz9RzGdKJS_FTE3qA] already exists",
    "index_uuid": "c7ZZpqz9RzGdKJS_FTE3qA",
    "index": "my_index"
  },
  "status": 400
}

想要更新映射,唯一的方法就是创建新的映射关系,然后将数据迁移过去。

除了支持的映射参数外,您不能更改现有字段的映射或字段类型。更改现有字段可能会使已编入索引的数据失效。如果需要更改字段的映射,请使用正确的映射创建一个新索引,然后将数据重新索引到该索引中。

image-20230110161850924

(5)数据迁移

基本语法:

POST _reindex
{ 
"source": { 
	"index": "twitter"
	},
"dest": {
	"index": "new_twitter"
	}
}

如果是 ES6之前指定了 Type

POST _reindex
{ 
"source": { 
	"index": "twitter",
	"type" : "type"
	},
"dest": {
	"index": "new_twitter"
	}
}

1、创建新的映射

# 创建新的映射
PUT /new_bank
{
  "mappings": {
    "properties": {
      "address": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "balance": {
        "type": "long"
      },
      "city": {
        "type": "keyword"
      },
      "email": {
        "type": "keyword"
      },
      "employer": {
        "type": "keyword"
      },
      "firstname": {
        "type": "keyword"
      },
      "gender": {
        "type": "keyword"
      },
      "lastname": {
        "type": "keyword"
      },
      "state": {
        "type": "keyword"
      }
    }
  }
}

2、迁移数据

# 数据迁移
POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "new_bank"
  }
}

4、分词

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

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

ES 提供的分词器都是只支持英文分词,如果是中文就有些不尽人意了。

# 使用分词器
POST _analyze
{
  "analyzer": "standard",
  "text": "我爱中国"
}

分词结果:按照我们中国人的逻辑 正常是 [我 , 爱 ,中国]

image-20230110164113803

(1)安装 ik 分词

查看对应版本安装: Releases · medcl/elasticsearch-analysis-ik (github.com)

由于我们在启动 ES 时,将 plugins 与我们主机目录进行了挂载,只需要通过 XFTP 将压缩包 传入 主机的 /mydata/elasticsearch/plugins 目录进行解压即可。

image-20230110170335846

使用命令解压缩:

unzip 压缩包名称

我们可以进入到 ES 容器内部查看插件是否安装:

docker exec -it 容器id /bin/bash 
# 切换到 bin 目录下
# 查看安装的 插件
elasticsearch-plugin list

安装好后,重启 ES 容器

docker restart 容器id

使用 ik 分词器

# 使用 ik 分词器
POST _analyze
{
  "analyzer": "ik_smart",
  "text": "我爱中国"
}
POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "我爱中国"
}

(2)自定义词库

虽然使用了 ik 分词器,但是一些词语的分词还是达不到我们想要的效果,这是因为 ES 中的默认词库中没有我们想要的单词,因此我们需要自定义一个词库,每次分词都先在自定义词库中查询。

比如:期待的分词效果就是 [尚硅谷, 电商, 项目]

POST _analyze
{
  "analyzer": "ik_smart",
  "text": "尚硅谷电商项目"
}

但是分词结果:

image-20230110174954773


我们可以利用 Nginx 做转发,将分词的查询请求发送给 Nginx

1、首先在 /mydata/nginx/html/es 目录下创建 fenci.txt 用来做我们的词库,我写的词语:

image-20230110175440870

2、Nginx 默认是从 html 目录下访问 index.html 页面,我们就可以指定词库的路径 192.168.56.111/es/fenci.txt,从页面上也是可以访问到的

image-20230110175601376

3、修改 /mydata/elasticsearch/plugins/ik/config 下的 IKAnalyzer.cfg.xml 文件,配置我们自定义的词库访问地址

image-20230110175711532

4、重启 ES 容器

5、测试效果

POST _analyze
{
  "analyzer": "ik_smart",
  "text": "尚硅谷电商项目"
}

分析达到我们想要的效果了

image-20230110180215177

五、Elasticsearch-Rest-Client

1、9300:TCP

  • spring-data-elasticsearch:transport-api.jar;
  • springboot 版本不同, transport-api.jar 不同,不能适配 es 版本
  • 7.x 已经不建议使用,8 以后就要废弃

2、9200:HTTP

  • JestClient:非官方,更新慢
  • RestTemplate:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
  • HttpClient:同上

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

最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client) https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

1、SpringBoot 整合 ES

(1)导入依赖

要在 properties 指明 elasticSearch的全局版本号。因为在 SpringBoot 中的默认版本号 是 6.4.3

image-20230110184443590

      
    <properties>
        <java.version>8</java.version>
        <elasticsearch.version>7.4.2</elasticsearch.version>
    </properties>
	<!--elasticSearch-->
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>${elasticsearch.version}</version>
        </dependency>

(2)编写配置

@Configuration
public class GulimallElasticSearchConfig {
    @Bean
    public RestHighLevelClient esClient() {
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("192.168.56.111", 9200, "http")));

        return  client;
    }
}

2、使用 SpringBoot 测试 ElasticSearch

ElasticSearch 的所有 API 使用方法都能在 官方文档中找到:Update API | Java REST Client 7.4] | Elastic

(1)增加索引

​ (1)创建共享的请求部分 放在 配置类中

    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();
    }

(2)测试新增索引

  @Test
    public void addIndex() throws IOException {
        // 参数为索引名
        IndexRequest request = new IndexRequest("users");
        // 设置id
        request.id("1");
        // 第一种封装数据方式
        // request.source("name","张三","age","17","address","河北");
        // 第二种封装数据方式
        User user = new User();
        user.setName("张三");
        user.setAge(22);
        user.setAddress("二仙桥");
        // 将对象转换为 json
        String toJSONString = JSON.toJSONString(user);
        request.source(toJSONString, XContentType.JSON);
        // 发送请求新增索引
        IndexResponse response = restHighLevelClient.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);
        System.out.println(response);
    }


    @Data
    class User {
        private String name;
        private Integer age;
        private String address;
    }

(3)查看结果

image-20230110193238918

(2)更新索引

    /*
    * 更新索引
    * */
    @Test
    public void test() throws IOException {
        // index,id
        UpdateRequest updateRequest = new UpdateRequest("users","1");
        User user = new User();
        user.setName("李四");
        String jsonString = JSON.toJSONString(user);
        updateRequest.doc(jsonString,XContentType.JSON);
        UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        System.out.println(updateResponse);
    }

(3)删除索引

    @Test
    public void delIndex() throws IOException {
        DeleteRequest deleteRequest = new DeleteRequest("users","1");
        DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        System.out.println(deleteRequest);
    }

(4)复杂查询

1、构建普通的查询条件

// 创建查询请求
SearchRequest searchRequest = new SearchRequest(); 
// 构建DSL检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
searchSourceBuilder.query(QueryBuilders.matchAllQuery()); 
searchRequest.source(searchSourceBuilder); 

2、聚合条件

“构建聚合”提供了所有可用聚合及其对应的AggregationBuilder对象和AggregationBuilders帮助方法的列表。

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_company")
        .field("company.keyword");
aggregation.subAggregation(AggregationBuilders.avg("average_age")
        .field("age"));

searchSourceBuilder.aggregation(aggregation);

3、返回信息

通过执行搜索返回的SearchResponse提供了有关搜索执行本身的详细信息以及对返回文档的访问。首先,有关于请求执行本身的有用信息,如HTTP状态代码、执行时间或请求是否提前终止或超时

RestStatus status = searchResponse.status();
TimeValue took = searchResponse.getTook();
Boolean terminatedEarly = searchResponse.isTerminatedEarly();
boolean timedOut = searchResponse.isTimedOut();

获取分片信息

int totalShards = searchResponse.getTotalShards();
int successfulShards = searchResponse.getSuccessfulShards();
int failedShards = searchResponse.getFailedShards();
for (ShardSearchFailure failure : searchResponse.getShardFailures()) {
    // failures should be handled here
}

对应图中:

image-20230110224744225

要访问返回的文档,我们需要首先获取响应中包含的SearchHits:

SearchHits hits = searchResponse.getHits();

SearchHits提供所有点击的全局信息,如点击总数或最高得分:

TotalHits totalHits = hits.getTotalHits();
// the total number of hits, must be interpreted in the context of totalHits.relation
long numHits = totalHits.value;
TotalHits.Relation relation = totalHits.relation;
float maxScore = hits.getMaxScore();

对应图中:

image-20230110224511543

获取查询出来的文档记录

SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
    // do something with the SearchHit
}

image-20230110224848905

返回的聚合信息:

Aggregations aggregations = searchResponse.getAggregations();
// 通过聚合名获取聚合信息
Terms byCompanyAggregation = aggregations.get("ageAgg"); 
// 获取key
Bucket elasticBucket = byCompanyAggregation.getBucketByKey("key"); 

Avg averageAge = elasticBucket.getAggregations().get("BalanceAgg"); 
// 获取 value
double avg = averageAge.getValue();

image-20230110225112587

案例一: 在 bank 索引中,按照年龄聚合,并且请求这些年龄段的这些人的平均薪资

  @Test
    public void search() throws IOException {
        // 案例: 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
        SearchRequest searchRequest = new SearchRequest("bank");
        // 1、sourceBuilder 构建DSL检索条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        // query中的match
        // sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));

        // query 中的 match_phrase
        // sourceBuilder.query(QueryBuilders.matchPhraseQuery("address","Holmes Lane "));

        // 2、sourceBuilder 同样可以构建分页信息
        // sourceBuilder.from(10);
        // sourceBuilder.size(20);

        // query中的match_all
        sourceBuilder.query(QueryBuilders.matchAllQuery());


        // 3、sourceBuilder封装聚合条件
        // subAggregation 可嵌套聚合
        TermsAggregationBuilder aggregation = AggregationBuilders.terms("ageAgg").field("age").size(10)
                .subAggregation(AggregationBuilders.avg("BalanceAgg").field("balance"));
        sourceBuilder.aggregation(aggregation);

        searchRequest.source(sourceBuilder);
        System.out.println("查询条件: " + sourceBuilder);

        // 执行检索
        SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        System.out.println("响应结果: " +response);

        // 4、分析结果
        SearchHits hits = response.getHits();
        SearchHit[] searchHits = hits.getHits();

        for (SearchHit searchHit : searchHits) {
            // 查询结果
            String sourceAsString = searchHit.getSourceAsString();
            // 通过JSON转换工具,将json——》实体类
            AccountBean accountBean = JSON.parseObject(sourceAsString, AccountBean.class);
            System.out.println("Account对象: " + accountBean);
        }
        // 5、查看聚合信息
        Aggregations aggregations = response.getAggregations();
        // 获取指定的聚合
        Terms ageAgg = aggregations.get("ageAgg");
        for (Terms.Bucket bucket : ageAgg.getBuckets()) {
            System.out.println("年龄: " + bucket.getKeyAsString());
            System.out.println("人数: " + bucket.getDocCount());
            // 获取嵌套的聚合
            Avg balanceAgg = bucket.getAggregations().get("BalanceAgg");
            System.out.println("平均薪资: " + balanceAgg.getValue());
        }
    }

案例二 :搜索 bank 索引中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。

  /*
    * 案例
    * */
    @Test
    public void caseTwo() throws IOException {
     // 搜索 bank 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
        SearchRequest searchRequest = new SearchRequest("bank");
        // 构建DSL 查询条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
        sourceBuilder.size(0);
        // 构建聚合
        TermsAggregationBuilder aggregation1 = AggregationBuilders.terms("ageAgg").field("age");
        AvgAggregationBuilder aggregation2 = AggregationBuilders.avg("avgAgg").field("age");

        sourceBuilder.aggregation(aggregation1);
        sourceBuilder.aggregation(aggregation2);
        searchRequest.source(sourceBuilder);

        System.out.println("条件: " + sourceBuilder);
        // 发送请求
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
        // 获取聚合结果
        Terms ageAgg = searchResponse.getAggregations().get("ageAgg");
        for (Terms.Bucket bucket : ageAgg.getBuckets()) {
            System.out.println("年龄: " + bucket.getKey());
            System.out.println("人数: " + bucket.getDocCount());
        }

        Avg avgAgg = searchResponse.getAggregations().get("avgAgg");
        System.out.println("平均年龄: " + avgAgg.getValue());
    }

六、拓展:Docker 安装 Nginx

1、先随便启动一个 Nginx 复制出里面的配置文件

docker run -p 80:80 --name nginx -d nginx:1.10

2、创建 /mydata/nginx/conf 目录, 并切换到此目录下,将 Nginx 配置文件都拷贝文件夹里

docker container cp nginx:/etc/nginx .

image-20230110173958943

3、删除容器,并重新启动 Nginx 容器

docker run -p 80:80 --name nginx --restart=always \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10

4、访问 虚拟机 IP 地址即可访问 Nginx

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/334474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安全测试的最常用方法你知道多少呢?

安全性测试(Security Testing)是指有关验证应用程序的安全等级和识别潜在安全性缺陷的过程&#xff0c;其主要目的是查找软件自身程序设计中存在的安全隐患&#xff0c;并检查应用程序对非法侵入的防范能力&#xff0c;安全指标不同&#xff0c;测试策略也不同。 但安全是相对的…

【ESP32+freeRTOS学习笔记-(七)中断管理】

目录1、概述2、在ISR中使用FreeRTOS中专用的API2.1 独立的用于ISR中的API2.2 关于xHigherPriorityTaskWoken 参数的初步理解3、延迟中断处理的方法-将中断中的处理推迟到任务中去4 方法一&#xff1a;用二进制信号量来同步ISR与”延时处理的任务“4.1 二进制信号量4.2 函数用法…

高中生用台灯哪种好?2023最好的台灯品牌排行榜

高中生的学习时长是最长的&#xff0c;所以导致现在许多高中生都戴上了眼镜&#xff0c;主要是因为长时间对着书本&#xff0c;没有合理的让眼睛休息&#xff0c;导致眼疲劳&#xff0c;而选择护眼台灯是最好的&#xff0c;台灯内置的护眼技术是非常实用的&#xff0c;可以改善…

SIP协议的一键对讲终端

SIP对讲终端是一款采用了ARMDSP架构&#xff0c;接收网络音频流&#xff0c;实时解码播放&#xff1b;配置了麦克风输入和扬声器输出&#xff0c;作为网络数字广播的播放终端。主要用于银行、部门机构、酒店等场所的网络广播、网络对讲。本产品配置了麦克风和3W扬声器&#xff…

psudohash:一款基于变异机制的密码列表生成工具

关于psudohash psudohash是一款功能强大的密码列表生成工具&#xff0c;该工具基于关键词变异技术实现其功能&#xff0c;并且能够根据常用密码创建模式来生成字典文件。 psudohash可以用于密码爆破任务中&#xff0c;以帮助广大研究人员测试密码的安全性。该工具能够模仿人类…

Spring Security 源码解读:权限控制

本文样例代码地址&#xff1a; spring-security-oauth2.0-client-sample。 关于此章&#xff0c;官网介绍&#xff1a;Authorization 本文使用Spring Boot 2.7.4版本&#xff0c;对应Spring Security 5.7.3版本。 Introduction 认证过程中会一并获得用户权限&#xff0c;Au…

vue-router 源码解析(二)-创建路由匹配对象

文章目录基本使用导语createRouterMatcher 创建匹配路由记录addRoute 递归添加matchercreateRouteRecordMatcher 创建matchertokenizePath 解析pathtokensToParser 记录打分insertMatcher 将matcher排序总结基本使用 const routes [{path:"/",component: Demo2,nam…

爷青回!如果当年大学数据库实训选择了这款SQL工具,结局可能不一样

SQL语言逐渐成为职场人士必备的能力。很多人一直走上职场才了解什么是SQL&#xff0c;而更多人在大学就已经开始学习。 这些人一定对类似《数据库原理与应用》的课程不陌生。还记得你们是怎么熬过这门课的吗&#xff1f; 为什么说“熬”呢&#xff1f;实话说&#xff0c;数据库…

[DiceCTF 2023] rSabin

一点点学习别人的WP&#xff0c;这回看到一个大姥(r3kapig)的帖子&#xff0c;DiceCTF第二名&#xff0c;不过有好多东西一时还理解不了&#xff0c;得慢慢来。题目这个题有3个功能&#xff1a;rsa加密功能&#xff0c;p,q,N未知&#xff0c;e17低加密指数解密&#xff0c;不过…

如何通过极狐GitLab 平滑落地 Java 增量代码规范?

本文来自&#xff1a; 杨周 极狐GitLab 高级解决方案架构师 代码越写越规范是优秀开发者的成长之路&#xff0c;但很多人对老项目感到有心无力&#xff0c;因为太不规范了&#xff0c;所有人停下来一起修复也要花费很长时间&#xff0c;而且一次改动太多难以确保可靠性&#xf…

达梦8的dblink

简介&#xff1a;外部链接对象&#xff08;LINK&#xff09;是 DM 中的一种特殊的数据库实体对象&#xff0c;它记录了远程数据库的连接和路径信息&#xff0c;用于建立与远程数据的联系。通过多台数据库主库间的相互通讯&#xff0c;用户可以透明地操作远程数据库的数据&#…

我的网站上线了!

最近有段时间没有写原创文章了&#xff0c;恰好这两天正在翻阅历史文章的时候&#xff0c;发现文章中的图片竟然裂了&#xff1f;顿时冒了一身冷汗&#xff0c;因为每逢遇到这种情况&#xff0c;动辄需要花费一周的时间迁移图片。。。。。。 当我直接访问图片 url 的时候&#…

直播预告 | 数据库自治平台 KAP 监控告警架构及实例演示

线上沙龙-技术流第 25 期营业啦02月15日&#xff08;周三&#xff09;19:30KaiwuDB - B站直播间企业级数据集群往往有成百上千的各类型运算或应用同时运行&#xff0c;为保障系统的稳定可靠性&#xff0c;势必需要克服庞大数据量、复杂运算逻辑、相互关联大数据组件等重难点&am…

4年测试经验去面试10分钟就被pass,测试现在要求这么高了?

年过完了&#xff0c;大家都开始上班了&#xff0c;各位小伙伴多多注意身体&#xff0c;但是学习也别落下等 做为一名优秀的程序员&#xff0c;技术面试都是不可避免的一个环节&#xff0c;通常技术面试官都会经过本身的方式去考察程序员的技术功底与基础理论知识。 若是你参…

python基础之PyCharm介绍

课程&#xff1a;PyCharm 课程目标 PyCharm的作用下载安装PyCharmPyCharm的基本使用PyCharm的基本设置 一. PyCharm的作用 PyCharm是一种Python IDE&#xff08;集成开发环境&#xff09;&#xff0c;带有一整套可以帮助用户在使用Python语言开发时提高其效率的工具&#x…

《Spring揭秘》记录

IOC部分 IOC不等于IOC容器&#xff0c;即使不使用spring&#xff0c;我们也可以使用IOC&#xff0c;只不过spring提供了IOC容器实现。Spring的IoC容器的功能就包含一个提供依赖注入服务的IoC Service Provider。它提供两方面的支持&#xff0c;业务对象的构建管理和业务对象间的…

GAMES202 PCSS软阴影算法细节解析

在LearnOpenGL框架的基础上实现了一遍GAMES202的PCFPCSS软阴影&#xff0c;之前学习GAMES202时一些没弄清楚的问题顺便搞清楚了。 注&#xff1a;本文中代码和shader均在笔者自学LearnOpenGL的框架中实现&#xff0c;因此有一些细节可能和GAMES202作业框架不一致&#xff0c;且…

【前端CSS面试题】2023前端最新版css模块,高频15问

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;博主收集的CSS面试题 目录 一、CSS必备面试题 1.CSS3新特性 2.CSS实现元素两个盒子垂…

开发技术-Java switch case 的简单用法

文章目录1. integral-selector2. case3. break4. default5. 总结最近开发写 switch 发现有的技术点还没有掌握&#xff0c;在此做个记录。ON JAVA 中文版中&#xff0c;关于 switch 的描述为&#xff1a; switch 有时也被划归为一种选择语句。根据整数表达式的值&#xff0c;s…

Vue路由 —— vue-router

在上一篇内容讲到关于单页面组件的内容&#xff0c;同时也附上补充讲了关于单页面&#xff08;SPA&#xff09;和多页面&#xff08;MPA&#xff09;之间的优缺点&#xff0c;在本篇目当中就要来讲这个路由&#xff08;vue-router&#xff09;&#xff0c;通过路由来实现页面的…