springcloud微服务技术栈
- 声明:本文部分图片源于网络,仅仅作为本人复习学习所用。
文章目录
- springcloud微服务技术栈
- 5、分布式搜索
- 1、elasticsearch基础(ES)
- 2、es快速开始
- 3、elastic 索引库操作
- 4、elastic 文档的CRUD
- 5、RestClient 操作ES索引库
- 6、RestClient操作ES文档
- 7、ES的搜索功能
- 8、深入elasticsearch
5、分布式搜索
1、elasticsearch基础(ES)
- 随着数据量的增长,简单的MySql已经无法满足复杂的业务需求,后续就出现了elasticsearch。
- es是一款开源的分布式搜索引擎,用于搜索、分析、日志统计、系统监控等。支持REstfu风格。
- elastic stack(ELK):实际上是一个以elasticsearch为核心的技术栈,一般包括数据收集(beats、logsash),数据视图展示(kibana),核心引擎(elasticsreach)。
- Lucene:是apache的开源搜索引擎类库,提供搜索引擎api,es实际上是在该api上进行的二次开发。
- 正向索引和倒排索引
- 1)正向索引类似于mysql这种数据库为表建立索引的过程
- 2)es使用倒排索引,倒排索引是对比与正向索引的。例如,在正向索引中我们向表中我们想用一个关键词模糊查询,mysql会从表中一行一行的数据找,满足条件的保存下来。而使用倒排索引,它将mysql表中每一行当作一个文档,然后对该行中的某个字段进行拆分成关键词并把这些关键词重新建个表,关键词是唯一的,关键词对应存放的是文档的主键。对比:正向索引根据文档找词,倒排索引根据词找文档。
- 文档:相当于mysql表中的一条数据,在es中是使用json存储数据。
- 索引:相同类型文档的集合。
- es与mysql的对比
- 应用场景:
2、es快速开始
- 1)部署单点es:
- 1)创建网络
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
- 2)加载镜像
这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。
使用本地提供了镜像的tar包:
其上传到虚拟机中,然后运行命令加载即可:
# 导入数据
docker load -i es.tar
同理还有kibana
的tar包也需要这样做。
- 3)运行
运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v /home/docker/elasticsearch/es-data:/usr/share/elasticsearch/data \
-v /home/docker/elasticsearch/es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
#-v /home/docker/elasticsearch/es-config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
命令解释:
-
-e "cluster.name=es-docker-cluster"
:设置集群名称 -
-e "http.host=0.0.0.0"
:监听的地址,可以外网访问 -
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小 -
-e "discovery.type=single-node"
:非集群模式 -
-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录 -
-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录 -
-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录 -
--privileged
:授予逻辑卷访问权 -
--network es-net
:加入一个名为es-net的网络中 -
-p 9200:9200
:端口映射配置
在浏览器中输入:http://192.168.56.10:9200 即可看到elasticsearch的响应结果。
- 踩坑:
- 挂载目录时报错,原因:Caused by: java.nio.file.AccessDeniedException: /usr/share/elastic
es-data与es-plugins权限不够,使用chmod修改即可:chmod 777 /home/docker/elasticsearch/**
- 2)部署Kibana:
- kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
1)部署:
运行docker命令,部署kibana(由于kibana容器与es容器在同一个网络下所以可以直接使用服务名代替地址,且kibana和es版本要保持一致)
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
-
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中 -
-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch -
-p 5601:5601
:端口映射配置
kibana启动一般比较慢,需要多等待一会,可以通过命令:
docker logs -f kibana
查看运行日志是否运行成功。
此时,在浏览器输入地址访问:http://192.168.56.10:5601,即可看到结果:
- 3)安装IK分词器
- 由于默认提供在分词器对中文分词支持较差,所以使用IK分词器插件对中文分词。
- 1)在线安装ik插件(较慢)
# 进入容器内部
docker exec -it es /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch
- 2)离线安装ik插件(推荐),将IK安装包放入es-plugins目录下即可
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录或者你使用了数据卷(es-plugins)可以通过下面命令查看:
docker volume inspect es-plugins
在上文中plugins目录被挂载到了:/home/docker/elasticsearch/es-plugins
这个目录中。
解压缩分词器安装包:
下面找到资料中ik
上传到es容器的插件数据卷中:
也就是 /home/docker/elasticsearch/es-plugins
。
- 3)重启容器
# 3、重启容器
docker restart es
# 查看es日志
docker logs -f es
- 4)测试:
IK分词器包含两种模式:
ik_smart
:最少切分ik_max_word
:最细切分
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "我想先买手机再买电脑"
}
结果:
- 扩展词词典:
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。
所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
- 1)打开IK分词器config目录:
- 在IKAnalyzer.cfg.xml配置文件内容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
<entry key="ext_dict">ext.dic</entry>
</properties>
- 3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
姬霓太美
- 4)重启elasticsearch
docker restart es
# 查看 日志
docker logs -f elasticsearch
- 日志中已经成功加载ext.dic配置文件
- 5)测试效果:
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
- 停用词词典:
- 在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
- IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
- 1)IKAnalyzer.cfg.xml配置文件内容添加:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典-->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典-->
<entry key="ext_stopwords">stopword.dic</entry>
</properties>
- 3)在 stopword.dic 添加停用词
姬霓太美
- 4)重启elasticsearch
# 重启服务
docker restart elasticsearch
docker restart kibana
# 查看 日志
docker logs -f elasticsearch
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
3、elastic 索引库操作
- elastic的索引库类似于MySQL中的数据库。
- mapping属性:是对文档的约束,常见的mapping属性包括:
属性 | 释义 |
type | 字段数据类型,常见的简单类型有:1)text(可分词的文本);2)keyword(精确值,不可拆分,例如:品牌、国家、ip地址);3)数值:long、integer、short、byte、double、float;4)布尔:boolean;5)日期:date;6)对象:object。 |
index | 是否创建索引,默认为true。 |
analyzer | 使用哪种分词器。 |
properties | 该字段的子字段。 |
- 1、创建索引库
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名": {
"type": "text",
"analyzer": "ik_smart"
},
"字段名2": {
"type": "keyword",
"index": "false"
},"字段名3": {
"properties": {
"子字段": {
"type": "keywords"
}
}
},
//......
}
}
}
例子:
- 2、查看/删除索引库
- 请求遵循restful风格(请求方法+服务名称)
- 1)查看: GET /索引库名
- 2)删除: DELETE /索引库名
- 3、修改索引库
- es不允许修改索引库,但是可以添加新字段
4、elastic 文档的CRUD
- 1)新增操作
- 语法:
POST /索引库名给/_doc/文档id
{
"字段1": "值1",
"字段2": "值3",
"字段1": {
"子属性": "值3",
"子属性": "值5"
},
}
- 2)查询文档
- 语法:
GET /索引库名/_doc/文档id
- 3)删除文档
- 语法:
DELETE /索引库名/_doc/文档id
- 4)修改文档
- 语法:
#方式一:全量修改,会删除旧文档,添加新文档(可以替代POST新增操作)
PUT /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
//......
}
#方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
5、RestClient 操作ES索引库
- RestClient: es官方提供不同语言的客户端,用于操作ES(本质上组装DSL语句,发出http请求)。
- 案例:利用JavaRestClient实现创建、删除索引库,判断索引库是否存在。
- 1)创建数据库,导入数据
- 2)根据数据库分析索引库数据结构。
#酒店Mapping
PUT /hotel
{
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
补充:在es中如果想要根据多个字段搜索,可以使用copy_to属性将但当前字段拷贝到指定字段,相当于该字段拥有了多个字段的值,再使用该字段进行搜索。这种方式不是真的创建文档,而是类似于动态sql中的字段拼接。
- 3)初始化JavaRestClient。
- 1)引入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
- 2)保证es依赖版本要与服务端一致(如果使用springboot管理版本依赖,我们需要在引入elasticsearch的地方知名elasticsearch的版本)
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
- 3)初始化RestHighLevelClient
- 4)利用JavaRestClient创建、删除索引库。
- 创建
注:这个MAPPING_TEMPLATE实际上是一个json字符串,其内容就是{ “mapping”: {…}}
- 删除
@Test
void testDeleteHotelIndex() throws IOException {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("hotel");
client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
}
- 5)利用JavaRestClient判断索引库是否存在
- 判断
@Test
void testExistsHotelIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hotel");
boolean exists = highLevelClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
6、RestClient操作ES文档
- 案例:从数据库中查询酒店数据,导入hotel索引库,实现酒店数据的CRUD。
- 1)初始化JavaRestClient(同上略)
- 2)利用client实现新增酒店数据(IndexRequest)
- HotelDoc类:
//对应hotel索引库中的结构
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();//转化经纬度为es中的geo_point
this.pic = hotel.getPic();
}
}
- 示例代码:
@Test
void testSearchHotelIndex() throws IOException {
//1从数据库里查数据
Hotel hotel = hotelService.getById(61083L);
//2转结构
HotelDoc hotelDoc = new HotelDoc(hotel);
//3转json
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());//es中id(keywords)是字符串
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
//发出index请求
highLevelClient.index(request, RequestOptions.DEFAULT);
}
- 验证结果:
- 3)利用client实现根据id查询酒店数据(GetRequest)
@Test
void testGetHotelIndexDoc() throws IOException {
//1封装请求
GetRequest request = new GetRequest("hotel", "61083");
//2发出get请求,获取响应数据
GetResponse response = highLevelClient.get(request, RequestOptions.DEFAULT);
//3解析json
String json = response.getSourceAsString();
HotelDoc doc = JSON.parseObject(json, HotelDoc.class);
System.out.println(doc);
}
- 4)利用client删除酒店数据(DeleteRequest)
@Test
void testDeleteHotelIndexDoc() throws IOException {
//1准备请求
DeleteRequest request = new DeleteRequest("hotel","61083");
//2发出请求
highLevelClient.delete(request, RequestOptions.DEFAULT);
}
- 5)利用client修改酒店数据(UpdateRequest)
@Test
//局部更新
void testUpdateHotelIndexDoc() throws IOException {
//1准备请求
UpdateRequest request = new UpdateRequest("hotel","61083");
request.doc(
"city","西安",
"name","lisa一日酒店"
);
//2发出请求
highLevelClient.update(request, RequestOptions.DEFAULT);
}
- 总结:
补充:批量导入数据库数据(批量操作都差不多,因此此处只演示增加)
@Test
void testBulkHotelIndexDoc() throws IOException {
//1数据库查所有
List<Hotel> hotels = hotelService.list();
//2封装请求
BulkRequest request = new BulkRequest();
for (Hotel hotel: hotels) {
//3结构转化
HotelDoc hotelDoc = new HotelDoc(hotel);
//4添加批处理请求
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
//发出请求
highLevelClient.bulk(request, RequestOptions.DEFAULT);
}
7、ES的搜索功能
- es文档的增删改查已经学会,现在我们将使用es最重要的搜索功能,如何从海量数据中检索出我们想要的信息。
- 以下是常用DSL:
- 1)DSL查询文档
- es提供基于json的DSL来定义查询。常见查询类型包括:
- 全文检索:利用分词器对用户输入内容分词,然后去倒排索引。
格式:
GET /hotel/_search
{
"query": {
"查询条件": {"条件值"}
}
}
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
- match_query:单字段查询
GET /hotel/_search
{
"query": {
"match": {
"FIELD"(字段名): "TEXT"(值)
}
}
}
- multi_match_query:多字段查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "值"
"fields": ["字段名1","字段名2","字段名3"......]
}
}
}
- 精确查询:根据精确词条值找数据,一般是查找keyword、数值、日期、boolean等类型字段。
- range
#2range
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100, gt:大于;gte:大于等于
"lte": 2000 lt:小于;lte:小于等于
}
}
}
}
- term
#term查询
GET /hotel/_search
{
"query": {
"term": {
"city(字段名)": {
"value": "上海"
}
}
}
}
- 地理(geo)查询:根据经纬度查询。
- geo_distance:查询到指定中心点小于某个距离的所有文档()附近的人。
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "15km",
"location": "31.21,121.5"
}
}
}
- geo_bounding_box:查询指定的某个矩形内的所有文档。
- 复合查询:将上述各种条件组合起来,合并查询条件。
- bool(boolean):在满足业务条件下,尽可能减少算分。
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {"name": "如家"}
}
],
"must_not": [
{"range": {
"price": {
"gt": 400
}
}}
],
"filter": [
{"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}}
]
}
}
}
- function_score:修改文档相关性算分:考虑三点:1)过滤条件:那些文档要加分。2)算分函数:如何计算function score。3)加权方式:function score与query score如何运算。
- 相关分算法:es5用的是TF-IDF,es5后采用:BM25
# function socre查询
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
]
}
}
}
- 2)搜索结果处理
- 1)排序:es支持对结果进行排序,默认是按相关算分排序。可以用于排序字段类型有keyword、数值、地理坐标、日期等类型。不适用默认排序之后,es就不用进行相关分计算了。
#sort排序
GET /hotel/_search
{
"query": {
"match_all": {}
}
, "sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}
GET /hotel/_search
{
"query": {
"geo_distance": {
"distance": "15km",
"location": "31.034661,121.612282"
}
}
, "sort": [
{
"_geo_distance": {
"location": {
"lat": 31.034661,
"lon": 121.612282
},
"order": "asc",
"unit": "km"
}
}
]
}
- 2)分页:es默认值差排序中靠前的10条数据,想要查更多,需要自己定义。
#分页
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "asc"
}
}
],
"from": 10, //分页开始位置,默认为0
"size": 10 //显示条数
}
- 1)搜索过程:es先查询前1-20条数据,再做逻辑分页,从1-20条中取出10-20条数据。
- 2)出现问题:在es集群中,每个es下的数据各不相同,使用上面这个查询语句,得出的结果也不同。
- 3)解决办法:每个es都执行这一语句,让后将不同的结果集排序在进行截取。
- 4)导致:es的结果集查询上限为10000,现有的项目中业务都禁止你查询的总条数大于10000。
官方提供解决办法:
第一种方法问题:翻页只能向后翻,不能向前翻;第二种:内存消耗太多,无法实时更新。 - 5)测试:将上述"from"条数改为9991,"size"还是10进行测试
报错:
{
"error" : {
"root_cause" : [
{
"type" : "illegal_argument_exception",
"reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}
],
......
}
- 6)总结:分页的三种方式
- 3)高亮显示
#高亮显示,默认情况下,es搜索字段必须与高亮字段一致。即"require_field_match"为真,这里不相匹配改为false
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}
结果:
- 3)RestClient查询文档
- 1)快速入门
@Test
void testMatchAll() throws IOException {
//1 封装请求
SearchRequest request = new SearchRequest("hotel");
request.source().query(
QueryBuilders.matchAllQuery() /*利用工具写入DSL语句*/
);
//2获取响应
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
System.out.println(search);
//3解析
}
//解析过程:
SearchHits hits = search.getHits();//获取hits
TotalHits totalHits = hits.getTotalHits();//总条数
SearchHit[] hits1 = hits.getHits();//获取数据数组(hits)
//遍历
for (SearchHit hit: hits1) {
//获取文档source
String sourceAsString = hit.getSourceAsString();
//反序列化
HotelDoc doc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(doc);
}
将解析过程进行封装
void handleResponse(SearchResponse search) {
SearchHits hits = search.getHits();
TotalHits totalHits = hits.getTotalHits();//总条数
System.out.println("本次查询共有"+ totalHits.value + "条结果。");
SearchHit[] hits1 = hits.getHits();//获取数据数组(hits)
//遍历
for (SearchHit hit: hits1) {
//获取文档source
String sourceAsString = hit.getSourceAsString();
//反序列化
HotelDoc doc = JSON.parseObject(sourceAsString, HotelDoc.class);
System.out.println(doc);
}
}
- 2)match查询
@Test
void testMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
/*request.source().query(
QueryBuilders.matchQuery("all", "如家")
);*/
request.source().query(
QueryBuilders.multiMatchQuery("如家", "name")
);
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//System.out.println(search);
handleResponse(search);
}
- 3)精确查询
- 4)复合查询
@Test
void testBool() throws IOException {
SearchRequest request = new SearchRequest("hotel");
BoolQueryBuilder builder = new BoolQueryBuilder();
builder.must(QueryBuilders.termQuery("city","上海"));
builder.filter(QueryBuilders.rangeQuery("price").gte(100).lte(2000));
request.source().query(
builder
);
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
handleResponse(search);
}
- 5)排序、分页、高亮
@Test
void pageSort() throws IOException {
int page = 1,size=5;
//1 准备请求
SearchRequest request = new SearchRequest("hotel");
//2准备DSL
//request.source().query(QueryBuilders.matchAllQuery());
request.source().query(QueryBuilders.matchQuery("all", "如家"));
//2.1排序
request.source().sort("price", SortOrder.ASC);
//2.3分页from、size
request.source().from(page).size(size);
//2.4高亮(高亮字段必须与搜索字段一致或者搜索字段中包含高亮字段)
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
//3发出请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
handleResponse(search);
}
void handleResponse(SearchResponse search) {
SearchHits hits = search.getHits();
TotalHits totalHits = hits.getTotalHits();//总条数
System.out.println("本次查询共有"+ totalHits.value + "条结果。");
SearchHit[] hits1 = hits.getHits();//获取数据数组(hits)
//遍历
for (SearchHit hit: hits1) {
//获取文档source
String sourceAsString = hit.getSourceAsString();
//反序列化
HotelDoc doc = JSON.parseObject(sourceAsString, HotelDoc.class);
//获取高亮设置
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)){//判断高亮结果是否为空
HighlightField highlightField = highlightFields.get("name");//获取指定高亮结果数组
if(highlightField!= null){
String name = highlightField.getFragments()[0].toString();//拿出高亮结果数组中的第一条
doc.setName(name);
}
}
System.out.println(doc);
}
}
- 4)案例实现
8、深入elasticsearch
- 1、数据聚合(参加聚合的只能是关键词、数值、日期类型,text不行)
- 1)聚合:实现对文档数据的统计、分析、运算。聚合常见有三类:
- 1)桶聚合:对文档做分组。
- 1.1)termaggregation:按文档字段值分组
- 2.2)DateHistogram:按照日期阶梯分组,例如一周一组
- 2)度量聚合:用以计算一些值,比如最大值最小值。
- 2.1)Avg
- 2.2)Max
- 2.3)Min
- 2.4)Stats:同时求max、min、avg、sum等
- 3)管道聚合:对其他聚合结果为基础做聚合
- 2)聚合实现
- 1)DSL实现Bucket(桶)聚合
#聚合功能自定义排序
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"_count": "asc"
}
}
}
}
}
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"_count": "asc"
}
}
}
}
}
- 2)DSL实现Metrics(度量)聚合
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 10,
"order": {
"scoreAgg.avg": "asc"
}
},
"aggs": {
"scoreAgg": {
"stats": {
"field": "score"
}
}
}
}
}
}
#有条件的聚合
GET /hotel/_search
{
"query": {
"match": {
"brand": "希尔顿"
}
},
"size": 0,
"aggs": {
"cityAgg": {
"terms": {
"field": "city",
"size": 10
}
}
}
}
- 3)RestClient实现聚合
@Test
void testAggregation() throws IOException {
//1 准备请求
SearchRequest request = new SearchRequest("hotel");
//2准备DSL
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(20)
);
//3发出请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//4解析
Aggregations aggregations = search.getAggregations();
//注意导包,Terms是elasticsearch下的类。
Terms terms = aggregations.get("brandAgg");
List<? extends Terms.Bucket> buckets = terms.getBuckets();
for (Terms.Bucket bucket: buckets) {
String keyAsString = bucket.getKeyAsString();
System.out.println(keyAsString);
}
}
- 2、自动补全
- 1)准备拼音分词器(之前已经安装过了中文分词器,结合拼音可以做到按中文与拼音自动补全)
安装:
之后重启elasticsearch容器即可。
- 测试
#测试分词器
POST _analyze
{
"text": ["酒店还不错"],
"analyzer": "ik_smart"
}
POST _analyze
{
"text": ["酒店还不错"],
"analyzer": "pinyin"
}
- 结果:
{
"tokens" : [
{
"token" : "jiu",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "jdhbc",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "dian",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 1
},
{
"token" : "hai",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 2
},
{
"token" : "bu",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 3
},
{
"token" : "cuo",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 4
}
]
}
- 2)自定义分词器(了解es分词器规则)
#自定义拼音分词器
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
}
#测试
POST /test/_analyze
{
"text": ["酒店还不错"],
"analyzer": "my_analyzer"
}
- 结果:
{
"tokens" : [
{
"token" : "酒店",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "jiudian",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "jd",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "还不",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "haibu",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "hb",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "不错",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "bucuo",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "bc",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}
- 出现问题:在test中插入两个文档{“id”: 1, “name”: “狮子”},{“id”: 2, “name”: “虱子”}。在查询”掉入狮子笼“时会得到两条结果,原因如下:
- 3)自动补全查询
#自动补全的索引库
PUT test2
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
#示例数据
POST test2/_doc
{
"title": ["Sony", "WH-1000XM3"]
}
POST test2/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test2/_doc
{
"title": ["Nintendo", "switch"]
}
#自动补全查询
POST /test2/_search
{
"suggest": {
"titleSuggest": {
"text": "s",
"completion": {
"field": "title",
"skip_duplicates": true,
"size": 10
}
}
}
}
- 实现酒店搜索框自动补全
- 1)
#1、修改酒店数据索引库
PUT /hotel
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": {
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": {
"py": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart",
"copy_to": "all"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type": "integer"
},
"score":{
"type": "integer"
},
"brand":{
"type": "keyword",
"copy_to": "all"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type": "geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "text_anlyzer",
"search_analyzer": "ik_smart"
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer"
}
}
}
}
- 2)
//2、修改对应hotel中的结构
@Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
private List<String> suggestion; //添加suggestion字段
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();//转化经纬度为es中的geo_point
this.pic = hotel.getPic();
if(this.business.contains("/"))
{
String[] split = this.business.split("/");
this.suggestion = new ArrayList<>();
this.suggestion.add(this.brand);
Collections.addAll(this.suggestion, split);
}else
this.suggestion = Arrays.asList(this.brand, this.business);//自动补全内容
}
}
- 3)RestAPI实现自动补全
解析
实现代码:
@Test
void testSuggestion() throws IOException {
//1请求准备
SearchRequest request = new SearchRequest("hotel");
//2封装DSL
request.source().suggest(new SuggestBuilder().addSuggestion(
"mySuggestions",/*自定义*/
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")/*相当于键盘输入一个h,根据这个h进行补全*/
.skipDuplicates(true)
.size(10)
));
//3发出请求
SearchResponse search = client.search(request, RequestOptions.DEFAULT);
//4解析响应结果
Suggest suggest = search.getSuggest();
//Suggest.Suggestion<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> mySuggestions
// = suggest.getSuggestion("mySuggestions");
//上述泛型实际上就是CompletionSuggestion(运行时可见)
CompletionSuggestion completionSuggestion = suggest.getSuggestion("mySuggestions");
List<CompletionSuggestion.Entry.Option> options = completionSuggestion.getOptions();
for (CompletionSuggestion.Entry.Option option: options) {
System.out.println(option);
}
}
- 3、数据同步
- 1)数据同步思路
在微服务中操作mysql与操作elasticsearch的功能会在不同的微服务下,所以实现数据同的 方法目前常见有三种:
1.利用feign进行同步调用
2.利用mq实现异步调用
3.利用其它中间件(例如binlog) - 2)利用MQ实现ES与MySql数据同步
- 4、集群
- 1)es集群搭建
- 1)我们会在单机上利用docker容器运行多个es实例来模拟es集群。不过生产环境推荐大家每一台服务节点仅部署一个es的实例。部署es集群可以直接使用docker-compose来完成,但这要求你的Linux虚拟机至少有4G的内存空间。
- 2)compose编排文件
version: '2.2'
services:
es01:
image: elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data02:/usr/share/elasticsearch/data
ports:
- 9201:9200
networks:
- elastic
es03:
image: elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
ports:
- 9202:9200
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
踩坑记录
- 使用compose文件使用如上命令出现:
ERROR: [1] bootstrap checks failed. You must address the points described in the following [1] lines before starting Elasticsearch.
es02 | bootstrap check failure [1] of [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
解决办法:
#1 切换root用户
#2 执行如下命令
sysctl -w vm.max_map_count=262144
sysctl -a|grep vm.max_map_count
sysctl -p
- 出现code137错误是因为内存不够
- 3)集群监控
kibana可以监控es集群,不过新版本需要依赖es的x-pack 功能,配置比较复杂。
这里推荐使用cerebro来监控es集群状态,官方网址:https://github.com/lmenezes/cerebro。
访问http://localhost:9000 即可进入管理界面。
输入你的elasticsearch的任意节点的地址和端口,点击connect即可。 - 4)数据分片
在使用es集群第一个重要好处就是使用分片存储,这里的分片指的是对数据进行切分,每一个es节点里保存不同的数据片,同时将这些数据片在不同的es节点上做一个备份即备份数据片。
分片要在创建索引库时完成:
PUT /myTest
{
"setting": {
"number_of_shards": 3, //分片数量
"number_of_replicas": 1 //副本数量
},
"mappings": {
"properties": {
......
}
}
}
或者使用crerbro工具创建:
- 2)脑裂问题
- 什么是脑裂?假设因为网络问题导致同一集群下同一时间拥有多个主节点,之后网络恢复后这两个主节点因为数据不一致将会出现问题。
- es7之后解决此问题,解决方式是主节点的选取通过节点投票产生,算法为获得当前节点总数+1的一半票数才算竞选成功,因此保险起见es集群中节点数量最好是奇数个。
- 3)集群分布式存储
- 1)es集权的节点角色与设置
注:每个节点都可以做路由节点,只需要将其他三个配置为false即可
- 2)es集群结构示例
- 3)分布式存储过程:
- 4)集群分布式查询
查询过程:
注意:每个节点都独立可以完成这两个阶段的功能
- 5)集群故障转移
- 1)假设在集群中有三个主节点,保存着数据的三个分片,这时node1挂掉了,其他节点如何处理node1负责的数据片?
- 2)数据迁移:新的主节点会获取挂掉的节点的主分片并将副本分片保存在候选节点上。