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 都采用它
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 里面的内容;
4、倒排索引
Elasticsearch 检索速度快是因为使用了一种倒排索引的数据结构
保存的记录:
1-红海行动
2-探索红海行动
3-红海特别行动
4-红海记录篇
5-特工红海特别探索
Elasticsearch 会基于这些保存的数据额外维护一张表,这些表会将保存的数据拆分成单词,并记录命中的记录【如图所示】。
比如:红海行动 会拆分成 红海、行动
, 探索红海行动 被拆分成 探索、红海、行动
当检索时:
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 端口
创建 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
三、初步探索
1、_cat
只需要发送对应的请求即可。
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
(2)PUT 方式必须携带ID,否则就会报错
POST 方式
POST 请求也可以新增数据
# 在 customer 索引下的 external 类型下保存 1 号数据
POST customer/external/1
{
"name": "John Doe"
}
(1)POST 请求携带 id ,当第一次发送POST请求是新增操作,当第二次发送POST请求是更新操作,并且版本号+1
第二次携带 id 发送POST 请求
(2)POST方式可以不携带 id ,el 会自动随机生成一个不重复的 id,也就是说,如果不携带ID发送 POST 请求,每一个都是新增
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
返回结果
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 条数据。
使用 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
使用 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 不显示查询记录的详情
返回结果
案例二:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
在 aggAge 聚合里面嵌套 balanceAgg 聚合:先查看年龄分步情况,基于以上的结果,计算平均薪资
# **案例二**:按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"aggAge": {
"terms": {
"field": "age",
"size": 10
},
"aggs": {
"balanceAgg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
案例三:查出所有年龄分布,并且这些年龄段中 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
}
3、Mapping 映射
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和 索引的。
比如:
- 哪些字段是 text类型,哪些字段是 boolean 类型
ES 中的映射类型有:
(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
}
想要更新映射,唯一的方法就是创建新的映射关系,然后将数据迁移过去。
除了支持的映射参数外,您不能更改现有字段的映射或字段类型。更改现有字段可能会使已编入索引的数据失效。如果需要更改字段的映射,请使用正确的映射创建一个新索引,然后将数据重新索引到该索引中。
(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": "我爱中国"
}
分词结果:按照我们中国人的逻辑 正常是 [我 , 爱 ,中国]
(1)安装 ik 分词
查看对应版本安装: Releases · medcl/elasticsearch-analysis-ik (github.com)
由于我们在启动 ES 时,将 plugins 与我们主机目录进行了挂载,只需要通过 XFTP 将压缩包 传入 主机的 /mydata/elasticsearch/plugins 目录进行解压即可。
使用命令解压缩:
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": "尚硅谷电商项目"
}
但是分词结果:
我们可以利用 Nginx 做转发,将分词的查询请求发送给 Nginx
1、首先在 /mydata/nginx/html/es
目录下创建 fenci.txt 用来做我们的词库,我写的词语:
2、Nginx 默认是从 html 目录下访问 index.html 页面,我们就可以指定词库的路径 192.168.56.111/es/fenci.txt
,从页面上也是可以访问到的
3、修改 /mydata/elasticsearch/plugins/ik/config
下的 IKAnalyzer.cfg.xml
文件,配置我们自定义的词库访问地址
4、重启 ES 容器
5、测试效果
POST _analyze
{
"analyzer": "ik_smart",
"text": "尚硅谷电商项目"
}
分析达到我们想要的效果了
五、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
<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)查看结果
(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
}
对应图中:
要访问返回的文档,我们需要首先获取响应中包含的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();
对应图中:
获取查询出来的文档记录
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// do something with the SearchHit
}
返回的聚合信息:
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();
案例一: 在 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 .
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