谷粒商城–elasticsearch–高级篇笔记一
1.elasticsearch是什么?
Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch 中。Kibana 使您能够以交互方式探索、可视化和共享对数据的洞察,并管理和监控堆栈。Elasticsearch 是索引、搜索和分析发生的地方。
Elasticsearch 为所有类型的数据提供近乎实时的搜索和分析。无论您拥有结构化或非结构化文本、数值数据还是地理空间数据,Elasticsearch 都可以以支持快速搜索的方式高效地存储和索引它。您可以超越简单的数据检索和聚合信息来发现数据中的趋势和模式。随着您的数据和查询量的增长,Elasticsearch 的分布式特性使您的部署能够随之无缝增长。
虽然并非所有问题都是搜索问题,但 Elasticsearch 提供了在各种用例中处理数据的速度和灵活性:
- 向应用或网站添加搜索框
- 存储和分析日志、指标和安全事件数据
- 使用机器学习实时自动建模数据的行为
- 使用 Elasticsearch 作为存储引擎自动化业务工作流
- 使用 Elasticsearch 作为地理信息系统 (GIS) 管理、集成和分析空间信息
- 使用 Elasticsearch 作为生物信息学研究工具存储和处理遗传数据
我们不断对人们使用搜索的新颖方式感到惊讶。但是,无论您的用例是否与其中之一类似,或者您正在使用 Elasticsearch 来解决新问题,您在 Elasticsearch 中处理数据、文档和索引的方式都是相同的。
2.简介
全文搜索属于最常见的需求, 开源的 Elasticsearch 是目前全文搜索引擎的首选。它可以快速地储存、 搜索和分析海量数据。 维基百科、 Stack Overflow、 Github 都采用它Elastic 的底层是开源库 Lucene。 但是, 你没法直接用 Lucene, 必须自己写代码去调用它的
接口。 Elastic 是 Lucene 的封装, 提供了 REST API 的操作接口, 开箱即用。
REST API: 天然的跨平台。
官方文档
3.基本概念

3.1 Index(索引)
- 动词,相当于MySQL中的insert.
- 名词,相当于MySQL中的Database.
3.2 Type (类型)
3.2.1 概念
在Index(索引)中,可以定义一个或多个类型,每种类型的数据放一起.
类似于MySQL中数据库中可以定义一个或多个表(Table);
3.2.2 ElasticSearch7-去掉type概念
Elasticsearch 7. X URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.X 不再支持URL中的type参数。
原因
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed最终在Lucene中的处理方式是一样的。
- 两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在处理中出现冲突的情况,导致Lucene处理效率下降。
- 去掉type就是为了提高ES处理数据的效率。
3.2.3 Elasticsearch 版本升级问题(升级到8)
解决:将索引从多类型迁移到单类型,每种类型文档一个独立索引
3.3 Document(文档)
保存到某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是JSON格式的
一个Document就像是MySQL中某个表的一条记录.
3.4 倒排索引
倒排索引:由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。
索引存储示例
将整句拆分为单词,将单词的值与索引存储起来,就可以根据单词查询索引位置,然后根据检索条件的相关性得分进行排序

4.Docker 安装ES 与 kibana
4.1下载镜像文件
elasticsearch与kibana版本是同步的
docker pull elasticsearch:7.4.2 #存储和检索数据
docker pull kibana:7.4.2 #可视化检索数据
4.2 创建实例
4.2.1创建ElasticSearch实例
#先将es的数据与配置与需要映射的文件夹创建好
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
#配置es地址
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
#保证权限
chmod -R 777 /mydata/elasticsearch/
#创建并启动实例
docker run --name elasticsearch -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

**特别注意:
-e ES_JAVA_OPTS="-Xms64m -Xmx256m" \ 测试环境下, 设置 ES 的初始内存和最大内存, 否则导
致过大启动不了 ES **
4.2.2关于ElasticSearch的9200和9300端口区别
9200作为Http协议,主要用于外部通讯
9300作为Tcp协议,jar之间就是通过tcp协议通讯 .ES集群之间是通过9300进行通讯
测试是否创建成功
192.168.157.128:9200 虚拟机地址+9200

4.2.3创建Kibana实例
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.157.128:9200 -p 5601:5601 \
-d kibana:7.4.2
测试是否创建成功
192.168.157.128:5601

4.3设置es与Kibana在docker启动时启动
#设置es在docker开启的时候启动
docker update elasticsearch --restart=always
#设置Kibana在docker开启的时候启动
docker update kibana --restart=always
重启docker发现es与Kibana仍然可以使用
5.初步检索
5.1_cat
- GET /_cat/nodes: 查看所有节点

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

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

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

5.2索引(保存)一个文档
5.2.1 PUT方法(必须带id)
保存一个数据, 保存在哪个索引的哪个类型下, 指定用哪个唯一标识
#在 customer 索引下的 external 类型下保存 1 号数据
PUT customer/external/1
{
"name": "Zhang shan"
}

5.2.2POST方法(可以不带id)
#索引(保存)一个文档 (POST)
GET customer/external/2
{
"name":"Li Si"
}

5.2.3使用GET方法与使用PUT方法索引(保存)文档的区别
个人理解:
PUT与POST都可以新增与修改文档
PUT与POST修改文档都是指定id再索引一次
POST新增可以不带id(自动生成),也可以自定义id
PUT新增只能自定义id,不能自动生成(PUT本就是设定用来修改的)
PUT 和 POST 都可以对文档进行新增,修改
POST 新增。 如果不指定 id, 会自动生成 id。 指定 id 就会修改这个数据, 并新增版本号.
- POST新增带id:如5.2.2
- POST新增不带id :id自动生成

- POST 修改

PUT 可以新增可以修改。PUT 必须指定 id; 由于 PUT 需要指定 id, 我们一般都用来做修改 操作, 不指定 id 会报错。
- PUT新增必须带id,不带id会报错

5.3查询指定id文档
#查询文档指定id
GET /customer/external/1

#查询结果
{
"_index" : "customer", //在哪个索引
"_type" : "external", //在哪个类型
"_id" : "1", //记录 id
"_version" : 2, //版本号
"_seq_no" : 10, //并发控制字段, 每次更新就会+1, 用来做乐观锁
"_primary_term" : 1, //同上, 主分片重新分配, 如重启, 就会变化
"found" : true,
"_source" : { //真正的内容
"name" : "Zhang shan2"
}
}
5.4更新文档
5.4.1 POST更新方式一
POST customer/external/1/_update
{
"doc": {
"name": "John Doew"
}
}
5.4.2 POST更新方式二
#之前就是这种写法
POST customer/external/1
{
"name": "John Doe2"
}
5.4.3 PUT更新
PUT customer/external/1
{
"name": "John Doe3"
}
5.4.3 更新同时增加属性
#更新同时增加属性
POST customer/external/1/_update
{
"doc": {
"name": "Jane Doe",
"age": 20
}
}
5.4.4 三种更新方式的特点
POST方式一与方式二的区别是否带update
- 带_update的POST更新:会对比源文档数据, 如果相同不会有什么操作, 文档 version 不增加
(会对比源文档数据, 如果相同不会有什么操作, 文档 version 不增加)
- 不带_update的POST:总会将数据重新保存并增加 version 版本
POST使用场景
对于大并发更新, 不带 update;
对于大并发查询偶尔更新, 带 update; 对比更新, 重新计算分配规则。
- PUT 操作总会将数据重新保存并增加 version 版本;
5.5删除指定id文档&索引
5.5.1删除指定id文档
#删除文档
DELETE customer/external/1

5.5.2删除索引
#删除索引
DELETE customer

5.6 bulk 批量 API
在单个 API 调用中执行多个索引或删除操作。这减少了开销并且可以大大提高索引速度。
5.6.1 语法格式
{ action: { metadata }} //action: 操作; metadata:对哪一个数据进行操作
{ request body } //操作的内容
{ action: { metadata }}
{ request body }
两个一组
#批量操作
POST customer/external/_bulk //对customer索引下的external类型进行批量操作
{"index":{"_id":"1"}} //索引(添加)一个文档,指定id=1
{"name": "John Doe" } //id=1的属性值
{"index":{"_id":"2"}} //索引(添加)一个文档,指定id=2
{"name": "Jane Doe" } //id=2属性值

5.6.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"} }

bulk API 以此按顺序执行所有的 action(动作) 。 如果一个单个的动作因任何原因而失败,它将继续处理它后面剩余的动作。 当 bulk API 返回时, 它将提供每个动作的状态(与发送的顺序相同) , 所以您可以检查是否一个指定的动作是不是失败了。
5.6.3 样本测试数据
原测试数据地址已经挂了
新测试数据地址:https://gitee.com/zhourui815/gulimall/blob/master/doc/es%E6%B5%8B%E8%AF%95%E6%95%B0%E6%8D%AE.json
POST bank/account/_bulk
测试数据

6.进阶检索
6.1 SearchAPI
ES 支持两种基本方式检索 :
- 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
- 另一个是通过使用 REST request body 来发送它们(uri+请求体)
6.1.1检索信息
**一切检索从_search 开始 **
#检索 bank 下所有信息, 包括 type 和 docs
GET bank/_search

6.1.1.1请求参数方式检索
#请求参数方式检索
q=* 代表要查询的字段类似于select *
sort=account_number:asc 代表按照account_number字段排序,升序
GET bank/_search?q=*&sort=account_number:asc

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

6.2 Query DSL
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL( domain-specific language 领域特定语言) 。 这个被称为 Query DSL。 该查询语言非常全面, 并且刚开始的时候感觉有点复杂,真正学好它的方法是从一些基础的示例开始的。
6.2.1 基本语法格式
6.2.1.1 查询结构
#一个查询语句 的典型结构
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
#如果是针对某个字段, 那么它的结构如下:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
6.2.1.2查询示例
查询back索引并按照account_number字段降序排序,分页大小为5
#基本查询示例
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
- query 定义如何查询,
- match_all 查询类型【代表查询所有的所有】 , es 中可以在 query 中组合非常多的查 询类型完成复杂查询
- 除了 query 参数之外, 我们也可以传递其它的参数以改变查询结果。 如 sort, size
- from+size 限定, 完成分页功能
- sort 排序, 多字段排序, 会在前序字段相等时后续字段内部排序, 否则以前序为准

6.2.2 match匹配
#match 基本类型(非字符串类型),精确匹配
GET bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
match 返回 account_number=20 的

#match 字符串类型,全文检索
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
}
最终查询出 address 中包含 mill 单词的所有记录 match 当搜索字符串类型的时候, 会进行全文检索, 并且每条记录有相关性得分。

#match 字符串, 多个单词( 分词+全文检索)
GET bank/_search
{
"query": {
"match": {
"address": "mill road"
}
}
}
最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录, 并给出相关性得分

6.2.3 match_phrase 短语匹配
**短语匹配:将需要匹配的值当成一个整体单词( 不分词) 进行检索 **
#match_phrase 短语匹配
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}
查出 address 中包含 mill road 的所有记录, 并给出相关性得分

6.2.4 multi_match 多字段匹配
#multi_match 多字段匹配
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["address","state"]
}
}
}
查询 state 或者 address 包含 mill

6.2.5 bool 复合查询
bool 用来做复合查询:
复合语句可以合并 任何 其它查询语句, 包括复合语句, 了解这一点是很重要的。 这就意味着, 复合语句之间可以互相嵌套, 可以表达非常复杂的逻辑。
6.2.5.1 must
必须达到 must 列举的所有条件
#must 必须达到 must 列举的所有条件
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "Mill"
}
},
{
"match": {
"gender": "M"
}
}
]
}
}
}

6.2.5.2 should
应该达到 should 列举的条件, 如果达到会增加相关文档的评分, 并不会改变查询的结果。 如果 query 中只有 should 且只有一种匹配规则, 那么 should 的条件就会被作为默认匹配条件而去改变查询结果
# 应该达到 should列举的条件,如果达到会增
#加相关文档的评分,并不会改变查询的结果。
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
],
"should": [
{
"match": {
"address": "lane"
}
}
]
}
}
}

6.2.5.3 must_not
必须不是指定的情况
#must_not 必须不是指定的情况
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
],
"should": [
{
"match": {
"address": "lane"
}
}
],
"must_not": [
{"match": {
"FIELD": "TEXT"
}}
]
}
}
}
6.2.5.2中查询的"email" : “winnieholland@neteria.com”,的记录不见了
address 包含 mill, 并且 gender 是 M, 如果 address 里面有 lane 最好不过, 但是 email 必 须不包含 baluba.com

6.2.5.4 filter 结果过滤
并不是所有的查询都需要产生分数, 特别是那些仅用于 “filtering”(过滤) 的文档。 **为了不计算分数 **Elasticsearch 会自动检查场景并且优化查询的执行。

6.2.5.4 总结
| 事件 | 描述 |
|---|---|
| must | 子句(查询)必须出现在匹配的文档中,并将有助于得分。 |
| filter | 子句(查询))必须出现在匹配的文档中。然而不像 must此查询的分数将被忽略。 |
| should | 子句(查询)应出现在匹配文档中。在布尔查询中不包含must或fiter子句,一个或多个should子句必须有相匹配的文件。匹配 should条件的最小数目可通过设置minimum_should_match参数。 |
| must_not | 子句(查询)不能出现在匹配的文档中。 |
6.2.6 term 非 text 字段检索
和 match 一样。 匹配某个属性的值。 全文检索字段用 match, 其他非 text 字段匹配用 term。
#term 其他非 text 字段检索
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"account_number": {
"value": "970"
}
}
},
{
"match": {
"address": "Mill"
}
}
]
}
}
}

6.2.7 aggregations 聚合检索
聚合提供了从数据中分组和提取数据的能力。 最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。 在 Elasticsearch 中, 您有执行搜索返回 hits( 命中结果) , 并且同时返回聚合结果, 把一个响应中的所有 hits( 命中结果) 分隔开的能力。 这是非常强大且有效的,您可以执行查询和多个聚合, 并且在一次使用中得到各自的( 任何一个的) 返回结果, 使用一次简洁和简化的 API 来避免网络往返。
6.2.7.1 年龄分布 并求出年龄的平均值
搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄, 但不显示这些人的详情
#aggregations 执行聚合
#年龄分布及平均值 聚合
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Mill"
}}
]
}
},
"aggs": {
"group_by_state": {
"terms": {
"field": "age",
"size": 10
}
},
"avgAge": {
"avg": {
"field": "age"
}
}
}
}
size: 0 不显示搜索数据
aggs: 执行聚合。 聚合语法如下
“aggs”: {
“aggs_name 这次聚合的名字, 方便展示在结果集中”: {
"AGG_TYPE 聚合的类型(
avg,term,terms) ": {} }
},

6.2.7.2 求出年龄分布,并在每个年龄求平均薪资
#年龄分布内的平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"agg_avg": {
"terms": {
"field": "age",
"size": 10
}
,
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
注意与6.2.7.1区分,6.2.7.1是分为两个聚合
此示例是在一次聚合的基础上再次聚合

6.2.7.3 年龄分布,性别分布的基础上求平均值
查出所有年龄分布, 并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
#查出所有年龄分布, 并且这些年龄段中 M
#的平均薪资和 F 的平均薪资以及这个年龄
#段的总体平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"age_state": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"sex_agg": {
"terms": {
"field": "gender.keyword",
"size": 10
},
"aggs": {
"banlances_avg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
},
"size": 0
}

6.3 Mapping
6.3.1 字段类型


6.3.2 映射
Mapping(映射)
Mapping 是用来定义一个文档( document) , 以及它所包含的属性( field) 是如何存储和
索引的。 比如, 使用 mapping 来定义:
- 哪些字符串属性应该被看做全文本属性(full text fields) 。
- 哪些属性包含数字, 日期或者地理位置。
- 文档中的所有属性是否都能被索引(_all 配置) 。
- 日期的格式。
- 自定义映射规则来执行动态添加属性。
6.3.2.1 查看mapping信息
#查看mapping信息
GET bank/_mapping
我们在创建索引是没有指定类型,为什么会查询出来呢?
答:es会根据数据自动猜测的映射类型

6.3.3 新版本改变
Es7 及以上移除了 type 的概念。
关系型数据库中两个数据表示是独立的, 即使他们里面有相同名称的列也不影响使用,但 ES 中不是这样的。 elasticsearch 是基于 Lucene 开发的搜索引擎, 而 ES 中不同 type下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
两个不同 type 下的两个 user_name, 在 ES 同一个索引下其实被认为是同一个 filed,你必须在两个不同的 type 中定义相同的 filed 映射。 否则, 不同 type 中的相同字段名称就会在处理中出现冲突的情况, 导致 Lucene 处理效率下降。
去掉 type 就是为了提高 ES 处理数据的效率。
Elasticsearch 7.x
URL 中的 type 参数为可选。 比如, 索引一个文档不再要求提供文档类型。
Elasticsearch 8.x
不再支持 URL 中的 type 参数。
解决:
1) 、 将索引从多类型迁移到单类型, 每种类型文档一个独立索引
2) 、 将已存在的索引下的类型数据, 全部迁移到指定位置即可。 详见数据迁移
6.3.3.1 创建映射
#创建索引并指定映射
PUT my-index
{
"mappings": {
"properties": {
"age":{
"type": "integer"
},
"emali":{
"type": "keyword"
},
"name":{
"type": "text"
}
}
}
}

6.3.3.2 添加新的字段映射
#添加新的字段映射
PUT my-index/_mapping
{
"properties": {
"employee-id": {
"type": "text",
"index": false
}
}
}

6.3.3.3 更新映射
对于已经存在的映射字段, 我们不能更新。 更新必须创建新的索引进行数据迁移
6.3.3.4 数据迁移
6.3.3.4.1查询出想要修改的映射类型
GET bank/_mapping
#创建一个新的索引
将一下的属性复制出来

6.3.3.4.2 新增一个新的索引
将6.3.3.4.1中复制的属性粘贴至属性中,先不要执行
#创建一个新的索引 PUT newbank { "properties": { "account_number" : { "type" : "long" }, "address" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "age" : { "type" : "long" }, "balance" : { "type" : "long" }, "city" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "email" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "employer" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "firstname" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "gender" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "lastname" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "state" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } } } }

- 将各个字段的映射类型按照自己的需求修改 保存
#创建一个新的索引
PUT /newbank
{
"mappings": {
"properties": {
"account_number": {
"type": "long"
},
"address": {
"type": "text"
},
"age": {
"type": "integer"
},
"balance": {
"type": "long"
},
"city": {
"type": "keyword"
},
"email": {
"type": "keyword"
},
"employer": {
"type": "keyword"
},
"firstname": {
"type": "text"
},
"gender": {
"type": "keyword"
},
"lastname": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"state": {
"type": "keyword"
}
}
}
}

6.3.3.4.3 数据迁移
先创建出 newbank 的正确映射。 然后使用如下方式进行数据迁移
固定语法
POST _reindex
{
“source”: {
“index”: “twitter”
},
“dest”: {
“index”: “new_twitter”
}
}
将 旧索引的 type 下的数据进行迁移
#数据迁移
POST _reindex
{
"source": {
"index": "bank"
},
"dest": {
"index": "newbank"
}
}
迁移成功

6.4 分词
一个 tokenizer( 分词器) 接收一个字符流, 将之分割为独立的 tokens( 词元, 通常是独立的单词) , 然后输出 tokens 流。
例如, whitespace tokenizer 遇到空白字符时分割文本。 它会将文本 “Quick brown fox!” 分割为 [Quick, brown, fox!]。
该 tokenizer(分词器) 还负责记录各个 term(词条) 的顺序或 position 位置(用于 phrase 短语和 word proximity 词近邻查询) , 以及 term(词条) 所代表的原始 word(单词) 的 start(起始) 和 end(结束) 的 character offsets(字符偏移量) (用于高亮显示搜索的内容) 。
Elasticsearch 提供了很多内置的分词器, 可以用来构建 custom analyzers(自定义分词器) 。
6.4.1安装ik分词器
没安装ik分词器之前的效果

**注意: 不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装 **
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2 对应 es 版本安装 (此处选用的7.4.2版本)
- 由于之前映射了plugins目录,所以在/mydata/elasticsearch/plugins/下载elasticsearch-analysis-ik-7.4.2.zip
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
- 解压下载的文件
unzip elasticsearch-analysis-ik-7.4.2.zip
- 删除zip文件
rm –rf *.zip
- 将elasticsearch文件夹下的所有文件 移动至ik目录下(自建目录)
mv elasticsearch/ ik
- 确认是否安装好了分词器
#进入容器内部
docker exec -it 容器 id /bin/bash
#即可列出系统的分词器
cd ../bin
elasticsearch plugin list

- 发现ik后重启容器
docker restart elasticsearch
6.4.2 测试分词器
6.4.2.1使用默认分词器
#使用默认分词器
POST _analyze
{
"text": "我是中国人"
}

6.4.2.2 使用ik_smart 分词器
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}

6.4.2.3 使用ik_max_word 分词器
#使用ik_max_word 分词器
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}

6.4.2.4总结
能够看出不同的分词器, 分词有明显的区别, 所以以后定义一个索引不能再使用默认的 mapping 了, 要手工建立 mapping, 因为要选择分词器。
6.4.4.自定义词库
6.4.4.1 未自定义词库

6.4.4.2 自定义词库测试
6.4.4.2.1 创建词库
在搭建好nginx的基础上,搭建nginx在8.x章节
cd /mydata/nginx/html/
#创建自定义词库
vim fenci.txt
添加新词

6.4.4.2.1 自定义词库
修改分词器的配置文件
vim /mydata/elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
指定自定义词库地址


重启es
docker restart elasticsearch
测试结果

7.Elasticsearch-Rest-Client
7.1 为什么选择Elasticsearch-Rest-Client
9300: TCP
- spring-data-elasticsearch:transport-api.jar;
- springboot 版本不同, transport-api.jar 不同, 不能适配 es 版本
- 7.x 已经不建议使用, 8 以后就要废弃
9200: HTTP
- JestClient: 非官方, 更新慢
- RestTemplate: 模拟发 HTTP 请求, ES 很多操作需要自己封装, 麻烦
- HttpClient: 同上
- Elasticsearch-Rest-Client: 官方 RestClient, 封装了 ES 操作, API 层次分明, 上手简单
最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
为什么选择用高阶?
低阶与高阶的区别就像是jdbc与mybatis的区别

7.2 SpringBoot整合
7.2.1 新增模块gulimall-search

启动类
package site.zhourui.gilimall.search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallSearchApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSearchApplication.class, args);
}
}
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>site.zhourui.gulimall</groupId>
<artifactId>gulimall-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-search</name>
<description>ElasticSearch检索服务</description>
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.zhourui.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
7.2.2 新增配置类
gulimall-search/src/main/java/site/zhourui/gilimall/search/config/GulimallElasticSearchConfig.java
package site.zhourui.gilimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zr
* @date 2021/10/25 14:27
*/
@Configuration
public class GulimallElasticSearchConfig {
//全局通用设置项,单实例singleton,构建授权请求头,异步等信息
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();
}
@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.157.128", 9200, "http")));
return client;
}
}
7.2.3 测试
gulimall-search/src/test/java/site/zhourui/gilimall/search/GulimallSearchApplicationTests.java
package site.zhourui.gilimall.search;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
class GulimallSearchApplicationTests {
@Autowired
RestHighLevelClient client;
@Test
void contextLoads() {
System.out.println(client);
}
}
测试结果

7.3 使用
7.3.1 索引(新增) 数据
gulimall-search/src/test/java/site/zhourui/gilimall/search/GulimallSearchApplicationTests.java
package site.zhourui.gilimall.search;
import com.alibaba.fastjson.JSON;
import lombok.Data;
import org.apache.catalina.User;
import org.apache.ibatis.ognl.JavaSource;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import site.zhourui.gilimall.search.config.GulimallElasticSearchConfig;
import java.io.IOException;
@SpringBootTest
@RunWith(SpringRunner.class)
class GulimallSearchApplicationTests {
@Autowired
RestHighLevelClient client;
/**
* 测试索引到es
*/
@Test
void index() throws IOException {
IndexRequest request = new IndexRequest("users");//索引名
request.id("1");//文档id
User user = new User();
user.setUserName("张三");
user.setAge(18);
user.setGender("男");
String jsonString = JSON.toJSONString(user);
request.source(jsonString,XContentType.JSON);//要保存的内容
//执行操作
IndexResponse index = client.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应数据
System.out.println(index);
}
@Data
class User{
private String userName;
private Integer age;
private String gender;
}
@Test
void contextLoads() {
System.out.println(client);
}
}

成功索引
7.3.2 获取数据
/**
* 测试查询es
* @throws IOException
*/
@Test
void search() throws IOException {
//创建索引请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//构造检索条件
// searchSourceBuilder.query();
// searchSourceBuilder.from();
// searchSourceBuilder.size();
// searchSourceBuilder.aggregation();
searchRequest.source(searchSourceBuilder);
//执行检索
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//分析结果 searchResponse
System.out.println(searchResponse);
}
searchResponse查询结果
与
#match 字符串类型全文检索 GET bank/_search { "query": { "match": { "address": "mill" } } }查询出的结果一致
{
"took": 15,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 5.4032025,
"hits": [{
"_index": "bank",
"_type": "account",
"_id": "970",
"_score": 5.4032025,
"_source": {
"account_number": 970,
"balance": 19648,
"firstname": "Forbes",
"lastname": "Wallace",
"age": 28,
"gender": "M",
"address": "990 Mill Road",
"employer": "Pheast",
"email": "forbeswallace@pheast.com",
"city": "Lopezo",
"state": "AK"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "136",
"_score": 5.4032025,
"_source": {
"account_number": 136,
"balance": 45801,
"firstname": "Winnie",
"lastname": "Holland",
"age": 38,
"gender": "M",
"address": "198 Mill Lane",
"employer": "Neteria",
"email": "winnieholland@neteria.com",
"city": "Urie",
"state": "IL"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "345",
"_score": 5.4032025,
"_source": {
"account_number": 345,
"balance": 9812,
"firstname": "Parker",
"lastname": "Hines",
"age": 38,
"gender": "M",
"address": "715 Mill Avenue",
"employer": "Baluba",
"email": "parkerhines@baluba.com",
"city": "Blackgum",
"state": "KY"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "472",
"_score": 5.4032025,
"_source": {
"account_number": 472,
"balance": 25571,
"firstname": "Lee",
"lastname": "Long",
"age": 32,
"gender": "F",
"address": "288 Mill Street",
"employer": "Comverges",
"email": "leelong@comverges.com",
"city": "Movico",
"state": "MT"
}
}]
}
}
7.3.3 聚合查询
7.3.3.1 年龄分布
/**
* 测试聚合查询es
* @throws IOException
*/
@Test
void aggSearch1() throws IOException {
//创建索引请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//聚合条件
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
//在检索条件中增加聚合条件
searchSourceBuilder.aggregation(ageAgg);
//构造检索条件
// searchSourceBuilder.query();
// searchSourceBuilder.from();
// searchSourceBuilder.size();
// searchSourceBuilder.aggregation();
searchRequest.source(searchSourceBuilder);
//执行检索
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//分析结果 searchResponse
System.out.println(searchResponse);
}
查询结果
与
#aggregations 执行聚合 #年龄分布及平均值 聚合 GET bank/_search { "query": { "bool": { "must": [ {"match": { "address": "Mill" }} ] } }, "aggs": { "group_by_state": { "terms": { "field": "age", "size": 10 } } }, "size": 0 }查询出的结果一致
{
"took": 19,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 5.4032025,
"hits": [{
"_index": "bank",
"_type": "account",
"_id": "970",
"_score": 5.4032025,
"_source": {
"account_number": 970,
"balance": 19648,
"firstname": "Forbes",
"lastname": "Wallace",
"age": 28,
"gender": "M",
"address": "990 Mill Road",
"employer": "Pheast",
"email": "forbeswallace@pheast.com",
"city": "Lopezo",
"state": "AK"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "136",
"_score": 5.4032025,
"_source": {
"account_number": 136,
"balance": 45801,
"firstname": "Winnie",
"lastname": "Holland",
"age": 38,
"gender": "M",
"address": "198 Mill Lane",
"employer": "Neteria",
"email": "winnieholland@neteria.com",
"city": "Urie",
"state": "IL"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "345",
"_score": 5.4032025,
"_source": {
"account_number": 345,
"balance": 9812,
"firstname": "Parker",
"lastname": "Hines",
"age": 38,
"gender": "M",
"address": "715 Mill Avenue",
"employer": "Baluba",
"email": "parkerhines@baluba.com",
"city": "Blackgum",
"state": "KY"
}
}, {
"_index": "bank",
"_type": "account",
"_id": "472",
"_score": 5.4032025,
"_source": {
"account_number": 472,
"balance": 25571,
"firstname": "Lee",
"lastname": "Long",
"age": 32,
"gender": "F",
"address": "288 Mill Street",
"employer": "Comverges",
"email": "leelong@comverges.com",
"city": "Movico",
"state": "MT"
}
}]
},
"aggregations": {
"lterms#ageAgg": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": 38,
"doc_count": 2
}, {
"key": 28,
"doc_count": 1
}, {
"key": 32,
"doc_count": 1
}]
}
}
}
7.3.4获取查询结果(转为对象)
具体结果在下面那个hits里

利用json生成javabean,并使用lombok
/**
* Auto-generated: 2021-10-25 16:57:49
*
* @author bejson.com (i@bejson.com)
* @website http://www.bejson.com/java2pojo/
*/
@Data
@ToString
public static class Accout {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
/**
* 测试聚合查询es
* @throws IOException
*/
@Test
void aggSearch1() throws IOException {
//创建索引请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//聚合条件
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
//在检索条件中增加聚合条件
searchSourceBuilder.aggregation(ageAgg);
//构造检索条件
// searchSourceBuilder.query();
// searchSourceBuilder.from();
// searchSourceBuilder.size();
// searchSourceBuilder.aggregation();
searchRequest.source(searchSourceBuilder);
//执行检索
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//分析结果 searchResponse
System.out.println(searchResponse);
//获取所有查询结果
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit searchHit : searchHits) {
// searchHit.getId();
// searchHit.getIndex();
// searchHit.getType();
//转为json字符串
String sourceAsString = searchHit.getSourceAsString();
Accout accout = JSON.parseObject(sourceAsString, Accout.class);
System.out.println(accout);
}
}
查询结果

8.安装nginx
8.1 随便启动一个 nginx 实例, 只是为了复制出配置
#创建一个空文件夹
cd /mydata/
mkdir nginx
#启用nginx实例
#没有nginx镜像会自动下载并启动
docker run -p 80:80 --name nginx -d nginx:1.10
8.2 将容器内的配置文件拷贝到当前目录
#当前目录为mydata
docker container cp nginx:/etc/nginx .

8.3修改文件名称
#修改文件名
mv nginx conf
#新建一个nginx文件夹
mkdir nginx
#将config文件夹复制到nginx下
mv conf nginx/

8.4 删除原容器:
#停止原容器
docker stop nginx
#删除原容器
docker rm 容器id

8.5 创建新的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:1.10
访问:192.168.157.128虚拟机地址
搭建成功了,只是没有访问文件

新建一个hello word文件
vim index.html


测试通过

