ElasticSearch
ElasticSearch基本概念
Index索引、Type类型,类似于数据库中的数据库和表,我们说,ES的数据存储在某个索引的某个类型中(某个数据库的某个表中),Document文档(JSON格式),相当于是数据库中内容的存储方式
MySQL:数据库、表、数据
ElasticSearch:索引、类型、文档
概念:倒排索引
ElasticSearch的检索功能基于其倒排索引机制,该机制允许对检索的关键词进行拆分并判断其相关性得分,根据相关性得分再取得检索的结果排序,根据该排序返回具体的结果
ElasticSearch的安装
Docker安装ElasticSearch以及其可视化界面Kibana
拉取ES和Kibana,注意一定要注意ES的版本问题,不能直接拉取latest版本
进行ES的进一步安装,包括挂载等操作:
创建两个文件夹:mydata/elasticsearch/config 和 mydata/elasticsearch/data 用来挂载ES的配置和数据
进行一个写入操作:
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
创建并挂载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
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xms128m" \
-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:latest
-
run --name 是为容器起名并使其暴露两个端口,端口1是发送请求的端口,端口2是集群的通信端口
-
-e “discovery.type=single-node” \ 是令其以单节点方式启动
-
-e ES_JAVA_OPTS=“-Xms64m -Xms128m” \ 设置允许其占用的内存大小,64-128M,在实际上线中,其大概分配32G以上
-
后面的就是挂载和指定镜像
Docker安装ES的一些细节
注意在Docker安装ES时,可能会出现权限不足导致ES已启动就立刻自动关闭的情况,这时就需要我们修改挂载在外部系统的文件夹的权限:
执行:
chmod -R 777 /mydata/elasticsearch
之后在浏览器中发送:网址:9200就可以判断ES是否安装成功
安装ES的可视化界面KIBANA
PULL拉取kibana之后执行下面操作:
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://8.141.85.98:9200 -p 5601:5601 \-d kibana:7.4.2
- -e 指定的是对应的ES的地址,
- -p 是指kibana暴露出的端口号
- -d 指的是使用哪个镜像进行容器的创建
注意就算这里没有指定,我们也可以在kibana的配置文件中进行指定
ElasticSearch基本使用
ES的所有使用方式都以RestFul API的方式被封装为请求了,我们可以通过浏览器发送请求的方式来实现ES的使用
一些小功能
GET /_cat/nodes 查看节点
GET /_cat/health 查看节点健康状况
GET /_cat/master 查看主节点
GET /_cat/indices 查看所有索引
尝试:新增、修改数据
在customer索引下的external类型中保存1号数据的内容为:
{"name":"Jhon Doe"
}
注意一定要是PUT操作:
http://8.141.85.98:9200/customer/external/1
在发送的JSON中写入内容,注意新增和更新都是PUT操作,第一次是新增,后续再操作就是更新
但注意,如果我们在发送请求时不指定id,ES会为我们自动生成一个随机id,而我们的操作每次都是新增操作
注意PUT和POST有相同的效果,但PUT要求必须指定ID,POST在不指定ID的情况下会随机生成
查询数据
查询数据要发送get请求,举例查询:
返回的内容:
{"_index": "customer","_type": "external","_id": "1","_version": 1,"_seq_no": 0,"_primary_term": 1,"found": true,"_source": {"name": "Jhon Doe"}
}
此处要注意的是"_seq_no"字段,该字段是并发控制字段,使用乐观锁的方式进行并发控制:
我们可以在发送请求时携带并发判断字段:若版本未被其他业务修改,才进行修改,若已被其他业务修改,则自己不做任何操作:
POST:
http://8.141.85.98:9200/customer/external/1?if_seq_no=0&if_primary_term=1
若两个if后携带的字段都匹配的话,才会进行修改操作,这样规避了并发操作场景下结果的不可预测性
更新的几种操作
-
POST带_update:
http://8.141.85.98:9200/customer/external/1/_update
这种操作会对比原先的数据,若一致则什么也不做(版本号,并发控制字段都不改变),若不一致才进行修改,但注意我们带_update的修改要在修改的内容前加doc
{"doc":{"name":"Jhon Doe"} }
对于其他的更新操作都不会进行数据对比,直接进行更新覆盖,并修改版本号和并发编程字段
删除
以DELETE的方式发送和查询一样的请求就可以做到删除了
ES复杂查询
Bulk批量操作API
使用对应的操作进行批量的增删改操作:
发送POST请求:
http://192.168.0.1:9200/customer/external/_bulk
索引、类型、_bulk进行批量操作,这些批量操作的方式是:
每一次修改都是两个JSON数据,第一条是元数据,用来指定操作的具体数据以及操作的方式
POST /customer/external/_bulk
{"index":{"_id":1}}
{"name":"Jhon Doe"}
{"index":{"_id":2}}
{"name":"Tom Jackson"}
在Kibana中进行测试:
{"took" : 10, // 花费的时间:10ms"errors" : false, // 未发生错误"items" : [{"index" : {"_index" : "customer", // 操作的索引"_type" : "external", // 操作的类型"_id" : "1", // 操作的数据项id"_version" : 3, // 版本"result" : "updated", // 操作属性"_shards" : {"total" : 2,"successful" : 1,"failed" : 0},"_seq_no" : 2, // 并发控制版本号"_primary_term" : 1,"status" : 200 // 200代表修改、201代表新建}},{"index" : {"_index" : "customer","_type" : "external","_id" : "2","_version" : 1,"result" : "created","_shards" : {"total" : 2,"successful" : 1,"failed" : 0},"_seq_no" : 3,"_primary_term" : 1,"status" : 201}}]
}
复杂的批量操作
不在发送请求时指定索引和类型,在文档信息中进行具体的指定
POST _bulk
{"delete":{"_index":"website", "_type":"blog", "_id":"123"}}
{"create":{"_index":"website", "_type":"blog", "_id":"123"}}
{"title": "My first blog"}
{"index":{"_index":"website", "_type":"blog"}}
{"title": "My second blog"}
{"update":{"_index":"website", "_type":"blog", "_id":"123"}}
{"doc": {"title": "My updated Blog"}}
导入测试数据进行测试:(省略)
发送如下请求查看ES中数据的整体信息:
http://8.141.85.98:9200/_cat/indices
复杂查询
查询:查询所有数据的所有信息,并按照账户id排序:
GET bank/_search?q=*&sort=account_number:asc
查询出的结果:
{"took" : 30, "timed_out" : false,"_shards" : { // shards中存放的是分布式存储过程中的各个结点的操作信息"total" : 1,"successful" : 1,"skipped" : 0,"failed" : 0},"hits" : {"total" : {"value" : 1000,"relation" : "eq"},"max_score" : null, // 查询操作匹配的最大得分"hits":[] // 具体内容}
}
QueryDSL(ES专属查询语言)
基本操作
ES所提供的查询操作:请求发送之后,紧接着发送一个JSON格式的查询语言,用来规定查询条件:
GET bank/_search
{"query":{"match_all":{} // 不设置匹配条件,查询所有信息},"sort": [{"balance": {"order": "desc" // 以账户余额降序排序}}],"from": 5, // 从第五条数据开始"size": 6, // 展示6条数据"_source": ["balance", "firstname"]
}
设置查询条件的情况:
若我们的查询条件不为字符串,则代表我们所做的是精确查询,若为字符串,则是模糊查询:
精确查询
GET bank/_search
{"query": {"match":{"balance": 16418} }
}模糊查询
GET bank/_search
{"query": {"match":{"address": "Kings"} }
}
ES全文检索会对分词进行匹配,对每一个分词都在全文中进行检索,并以得分从高到低进行数据的返回
使用match_phrase进行不分词的整体匹配,这样就会和MySql中的模糊查询一样进行整体匹配了:
GET bank/_search
{"query": {"match_phrase":{"address": "mill road"} }
}
多字段查询
使用multi_match进行多字段的查询:
GET bank/_search
{"query": {"multi_match": {"query": "mill movico","fields": ["address", "city"]}}
}
对于每个字段,都进行查询,并且会进行分词查询
多字段拼接查询条件
使用bool进行查询条件的拼接:
GET bank/_search
{"query": {"bool": {"must": [{"match": {"gender": "M"}},{"match": {"address": "mill"}}],"must_not": [{"match": {"age": 18}}],"should": [{"match": {"lastname": "Wallace"}} ]}}
}
- must:必须匹配
- must_not:必须不匹配
- should:最好满足(应该)若满足会增加权重
区间:
"filter": {"range": {"age": {"gte": 18, "lte": 30}}
}
注意filter会只进行过滤,不会增加得分,而must、must_not、should都会增加相关性得分
-
term:term只适用于精确查询,尤其适合对于非文本类型的检索,注意对于文档类型的查询,一定不要使用term
-
match_phrase:模糊查询
-
"match": {"address.keyword": "xxxxxx" }
这个是精确查询
总结:对于非文本字段,都以term进行查询,对于文本字段,都以match进行查询
ES的数据分析功能(聚合)
示例
## 搜索address中包含mill的所有人的年龄分布(取出每个年龄的人有几个(term)并显示10条记录)以及平均年龄
GET bank/_search
{"query":{"match": {"address": "mill"}},"aggs": {"ageAgg": {"terms": {"field": "age", "size": 10}},"avgAgg": {"avg": {"field": "age"}}}
}
注意:在以文本字段为组进行分组时,我们必须在字段后添加.keyword
## 按照年龄聚合,并求这些年龄段人的平均薪资,并按照年龄分布对同一性别的人进行聚合,查他们的平均薪资
GET bank/_search
{"query": {"match_all": {}}, "aggs": {"ageAgg": {"terms": {"field": "age", "size": 100},"aggs": {"genderAgg": {"terms": {"field": "gender.keyword"},"aggs": {"balanceAgg": {"avg": {"field": "balance"}}}}}}}
}
映射
我们在向ES中存储数据时,ES会自动猜测我们的数据类型,如果是纯数字会被映射为Long类型,其他会被映射为text类型,而这种映射有时候不是我们想要的,我们在创建的时候就可以提前指定映射类型。
PUT /my_index
{"mappings": {"properties": {"age": {"type": "integer"},"email": {"type": "keyword"}, "name": {"type": "text"}}}
}
映射类型不为text的都不会被分词器进行检索,keyword只会被精确检索
添加一个属性只能新增:
PUT my_index/_mapping
{"properties": {"employee-id": {"type": "keyword", "index": false}}
}
通过下面请求来查看映射信息:
GET my_index/_mapping
映射的修改
映射是不可以进行直接修改的,我们必须进行数据迁移才能完成对ES中映射关系的修改:
新增一个对应的索引
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"}}}
}
将老索引的配置迁移给新索引:
POST _reindex
{"source": {"index": "bank", "type": "account"},"dest": {"index": "newbank"}
}
分词
ES将一句话分成若干个词,并按照分成的词进行全文检索,返回一个相关性排序的结果。
分词器
标准分词器尝试(中文):
POST _analyze
{"analyzer": "standard", "text": "分词器测试"
}
分词结果为:‘分’、‘词’、‘器’、‘测’、‘试’
其实很不符合中文语境
ik分词器
ik分词器是一个基于中文语境的ES分词器,我们在github上找到他并对应ES版本进行安装(7.4.2)
使用如下命令安装ik分词器对应的7.4.2版本
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
这样很简便,但非常慢,我们也可以直接下载下来再上传完成这个操作
之后解压就算安装好了ik分词器,我们进入容器的bin目录执行:elasticsearch-list
命令来查看分词器是否安装完成
之后我们就可以使用,ik分词器最常用的两种分词方式:
## 智能分词方式,
POST _analyze
{"analyzer": "ik_smart", "text": "我是中国人"
}
我、是、中国人## 最大分词方式
POST _analyze
{"analyzer": "ik_max_word", "text": "我是中国人"
}
我、是、中国人、中国、国人
但是,ik分词器虽然能够对中文分词有较为合理的分词基础,但对于一些比较具有时效性的分词,其还是不能进行很智能的分词,故我们还要进行词库拓展:
删除原先的ES容器,并重新安装,给它赋予更大的内存,但注意,ES不会丢失数据,因为数据已经进行过挂载(这里其实不用):
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
在Linux的mydata文件夹下创建一个文件夹:nginx用来存放nginx的相关配置信息:
直接创建一个nginx容器用来复制器配置信息(不需要拉取镜像,如果没有镜像会自动拉取):
docker run -p 80:80 --name nginx -d nginx:1.10
复制这个nginx的配置文件到我们新建的nginx文件夹中:
docker container cp nginx:/etc/nginx .
移除刚刚创建的nginx容器,将刚刚的文件夹重命名为conf,再将这个文件夹移动到新建一个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
这样nginx就创建好了,我们可以在html文件中创建一个index.html来显示首页
我们在html文件夹中创建一个es文件夹,用来存放定制化分词信息
创建一个fenci.txt的文件,存放以下内容(分词内容):
清河
南开
中移
在es的挂载的plugins文件夹中的conf文件夹中的IKAnalyzer.cfg.xml文件夹中进行修改:
更改远程那部分为自己的远程部分:http://xxxxxxxxxxxxxx/es/fenci.txt就可以了
SpringBoot整合ES
- JestClient: 非官方、更新慢
- RestTemplate、HttpClient等模拟HTTP请求发送的组件:模拟HTTP请求的发送,很多ES操作需要自己封装,操作较为繁琐
- 使用elasticsearch-rest-high-level-client依赖,这个依赖提供了便捷丰富的ES功能,这里也使用这种方式进行整合
在Java项目中建立一个新的包用来管理ES搜索服务
注意:只添加Web依赖,我选用SpringBoot 2.7.15版本
导入ElasticSearch依赖:
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.4.2</version></dependency>
同时注意:SpringBoot也集成了ES版本,这个版本很可能与我们自己选用的ES版本冲突,故我们必须对SpringBoot自动集成的ES版本进行修改:
<properties><java.version>1.8</java.version><elasticsearch.version>7.4.2</elasticsearch.version></properties>
在application.properties中配置注册中心:
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848spring.application.name=gulimail-search
创建配置类:config/GulimailElasticSearchConfig
@Configuration
public class GulimailElasticSearchConfig {
}
在启动类中添加启动注解:
@EnableDiscoveryClient
在配置类中添加Bean
@Beanpublic RestHighLevelClient esRestClient() {RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("8.141.85.94", 9200, "http")));return client;}
之后测试一下:看看能不能注入
@Autowiredprivate RestHighLevelClient client;
导入ES配置项信息
想配置类中导入ES的配置项信息,搜索ElasticSearch High Level 找 RequestOptions
这些配置项可以要求ES在被访问时必须携带某些信息才可以进行访问
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();}
简单测试
@Testpublic void indexData() throws IOException {IndexRequest indexRequest = new IndexRequest("users"); // 调用构造器时指定索引indexRequest.id("1"); // 指定所添加字段的idUser user = new User();user.setId(8L);user.setName("虎开发");user.setAge(20);String userString = JSON.toJSONString(user);indexRequest.source(userString, XContentType.JSON); // 将要保存的内容存在对应的IndexRequest对象中IndexResponse index = client.index(indexRequest, GulimailElasticSearchConfig.COMMON_OPTIONS);}
之后就可以在ES中查询到了
复杂检索功能(使用ES提供的API)
一个带有聚合的复杂检索示例
/*** 创建检索请求*/@Testpublic void searchData() throws IOException {// 1. 创建检索请求SearchRequest searchRequest = new SearchRequest();searchRequest.indices("bank"); // 指定要搜索的索引// 指定DSL检索条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();/*** sourceBuilder.query* sourceBuilder.from* sourceBuilder.size* sourceBuilder.aggregation** sourceBuilder.query()里面也可以拼matchAll方法用来查全部*/sourceBuilder.query(QueryBuilders.matchQuery("address", "mill")); // 匹配address字段中mill的值searchRequest.source(sourceBuilder); // 将查询条件传给searchRequest// 聚合(构建聚合条件)TermsAggregationBuilder terms = AggregationBuilders.terms("aggAgg").field("age").size(10);sourceBuilder.aggregation(terms); // 将聚合拼接到查询中AvgAggregationBuilder balanceAgg = AggregationBuilders.avg("balanceAgg").field("balance");sourceBuilder.aggregation(balanceAgg);SearchResponse searchResponse = client.search(searchRequest, GulimailElasticSearchConfig.COMMON_OPTIONS);SearchHits hits = searchResponse.getHits();// 获取大hitsSearchHit[] hitsObjects = hits.getHits();// 获取具体的数据// 之后遍历就行,这里不遍历了Aggregations aggregations = searchResponse.getAggregations();Terms term = aggregations.get("aggAgg"); // 以Term方式的聚合可以直接转换为Term类型for (Terms.Bucket bucket : term.getBuckets()) {String keyAsString = bucket.getKeyAsString();System.out.println("年龄:" + keyAsString + "==>" + bucket.getDocCount());}Avg balanceAgg1 = aggregations.get("balanceAgg");System.out.println("平均薪资:" + balanceAgg1.getValue());}