目录
示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
我们通过match_all来演示下基本api,先看DSL的组织:编辑
我们通过match_all来演示下基本的API,再看结果的解析:编辑
案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合
利用MQ实现mysql于elasticsearch中数据也要完成操作。
初识elasticsearch
了解ES
什么是elasticsearch
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch结合kibana,Logstash,Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析,实时监控等领域。
elasticsearch是elastic stack的核心,负责存储,搜索,分析数据。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:Apache Lucene - Welcome to Apache Lucene 。
Lucene的优势:
-
易扩展
-
高性能(基于倒排索引)
Lucene的缺点:
-
只限于Java语言开发
-
学习曲线陡峭
-
不支持水平扩展
elasticsearch的发展
2004年Shay Banon基于Lucene开发Compass
2010年Shay Banon重写了Compass,取名为Elasticsearch
官网地址:Elasticsearch 平台 — 大规模查找实时答案 | Elastic
相比与lucene,elasticsearch具备下列优势:
-
支持分布式,可水平扩展
-
提供Restful接口,可被任何语言调用
搜索引擎技术排名:
-
Elasticsearch:开源的分布式搜索引擎
-
Splunk:商业项目
-
Solr:Apache的开源搜索引擎
总结
什么是elaticsearch?
一个开源的分布式搜索引擎,可以用来实现搜索,日志统计,分析,系统监控等功能
什么是elastic stack(ELK)?
是以elaticsearch为核心的技术栈,包括bears,Logstash,kibana,elaticsearch
什么是Lucene?
是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
倒排索引
正向索引和倒排索引
正向索引
传统数据库(如MySQL)采用正向索引,例如给下表(tb_goods)中的id创建索引:
倒排索引
elasticsarch采用倒排索引:
-
文档(document):每条数据就是一个文档
-
词条(term):文档按照语义分成的词语
总结
什么是文档和词条?
-
每一条数据就是一个文档
-
对文档中的内容分词,得到的词语就是词条
什么是正向索引?
-
基于文档id创建索引,查询词条时必须先找到文档,而后判断是否包含词条
什么是倒排索引?
-
对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先更具词条查询到文档id,而后获取到文档
es的一些概念
文档
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
索引
索引(index):相同类型的文档的集合
映射(mapping):索引中文档的字段约束信息,类似表的结构约束
概念对比
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticserch,实现CRUD |
架构
MySQL:擅长事物类型操作,可以确保数据的安全和一致性
Elaticsearch:擅长海量数据的搜索,分析,计算
总结
文档:一条数据就是一个文档,es中是Json格式
字段:Json文档中的字段
索引:同类型文档的集合
映射:索引中文档的约束,比如字段名称,类型
elasticaserch与数据库的关系:
-
数据库负责事物类型操作
-
elasticsearch负责海量数据的搜索,分析,计算
安装es,kibana
安装es
-
创建网络
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
-
加载镜像
docker pull elasticsearch:7.12.1
-
运行es
docker run -d \ --name es \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "discovery.type=single-node" \ -v es-data:/usr/share/elasticsearch/data \ -v es-plugins:/usr/share/elasticsearch/plugins \ --privileged \ --network es-net \ -p 9200:9200 \ -p 9300:9300 \ elasticsearch:7.12.1
命令解释:
-
-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
:端口映射配置
-
-
开放9200端口,访问端口
安装kibana
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
-
部署kibana
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.150.101:5601,即可看到结果
-
安装分词器
分词器
es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。
我们在kibana中DevTools中测试:
POST /_analyze
{
"analyzer": "standard",
"text": "黑马程序员学习java太棒了"
}
安装IK分词器
查看数据卷
上传ik安装包
重启docker容器
docker restart es
测试
IK分词器包含两种模式:
-
ik_smart:最少切分
-
ik_max_word:最细切分
POST /_analyze { "analyzer": "ik_max_word", "text": "黑马程序员学习java太棒了" }
IK分词器扩展和停用词典
总结
分词器的作用是什么?
-
创建倒排索引时对文档分词
-
用户搜索时,对输入的内容分词
IK分词器又几种模式?
-
ik_smart:智能切分,粗粒度
-
ik_max_word:最细切分,细粒度
IK分词器如何扩展词条?如何停用词条?
-
利用config目录的IKAnalyzer.cfg.xml文件添加扩展词条和停用词典
-
在词典中添加扩展词条或者停用词条
索引库操作
mapping映射属性
mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
-
type:字段数据类型,常见的简单类型有:
-
字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,ip地址)
-
数值:long,integer,short,byte,double,float
-
布尔:boolean
-
日期:date
-
对象:object
-
-
index:是否创建索引,默认为true
-
analyzer:使用哪种分词器
-
properties:该字段的子字段
总结
mapping常见属性有哪些?
-
type:数据类型
-
index:是否索引
-
analyzer:分词器
-
prperties:子字段
type常见的有哪些?
-
字符串:text,keyword
-
数字:long,integer,short,byte,double,float
-
布尔:boolean
-
日期:date
-
对象:object
索引库CRUD
创建索引库
ES通过Restful请求操作索引库,文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下:
#创建索引库
PUT /heima
{
"mappings": {
"properties": {
"info" : {
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type" : "keyword",
"index": false
},
"name" : {
"type": "object",
"properties": {
"firstName" : {
"type" : "keyword"
},
"lastName" : {
"type" : "keyword"
}
}
}
}
}
}
成功运行
查看,删除索引库
示例:查询
示例:删除
修改索引库
DELETE /heima
示例:修改
总结
索引库操作有哪些?
-
创建索引库:PUT/索引库名
-
查询索引库:GET/索引库名
-
删除索引库:DELETE/索引库名
-
添加字段:PUT/索引库名/_mapping (可以添加字段但不能修改以前的字段)
文档操作
新增文档
新增文档的DSL语法如下:
示例
#插入文档
POST /heima/_doc/1
{
"info" : "黑马程序员Java讲师",
"email" : "zy@itcast.cn",
"name" : {
"firstName" : "云",
"lastName" : "赵"
}
}
运行结果
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
查询文档
DSL语法
示例
#查询文档
GET /heima/_doc/1
运行结果
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"info" : "黑马程序员Java讲师",
"email" : "zy@itcast.cn",
"name" : {
"firstName" : "云",
"lastName" : "赵"
}
}
}
删除文档
DSL语法
示例
#删除文档
DELETE /heima/_doc/1
运行结果
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
修改文档
方式一:全量修改,会删除旧文档,添加新文
示例
#全量修改文档
PUT /heima/_doc/1
{
"info" : "黑马程序员Java讲师",
"email" : "ZhaoYun@itcast.cn",
"name" : {
"firstName" : "云",
"lastName" : "赵"
}
}
运行结果(版本增加1)
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
方式二:增加修改,修改指定字段值
示例
#局部修改文档字段
POST /heima/_update/1
{
"doc" : {
"email" : "ZYun@itcast.cn"
}
}
运行结果
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 4,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1
}
总结
文档操作有哪些?
-
创建文档:POST /索引库名/_doc/文档id {json文档}
-
查询文档:GET /索引库名/_doc/文档id
-
修改文档:DELETE /索引库名/_doc/文档id
-
修改文档:
-
全量修改: PUT /索引库名/_doc/文档id {json文档}
-
增量修改: POST/索引库名/_update/文档id {"doc":{字段}}
-
RestAPI
什么是RestClient:
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:Elasticsearch Clients | Elastic
RestClient操作索引库
mapping要考虑的问题:
字段名,数据类型,是否参与搜索,是否分词,如果分词,分词器是什么?
ES中支持两种地理
-
geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:"32.83232,120.233231"
-
geo_shape:有多个geo_point组成的复杂几何图形。例如一条直线,"LINESTRING(-77.2344232 36.421231,-77.009099 38.8821384)"
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:
"business" : { "type": "keyword", "copy_to": "all" }, "all" : { "type": "text", "analyzer": "ik_max_word" }
初始化JavaRestClient
-
引入es的RestHighLeveClient依赖:
<!--elasticsearch--> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.12.1</version> </dependency>
-
因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
-
初始化RestHighLevelClient:
新建一个测试类
public class HotelIndexTest { private RestHighLevelClient client; @Test void name(){ System.out.println(client); } //初始化 @BeforeEach void setUp(){ this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://8.137.59.245:9200") )); } //清理 @Test @AfterEach void tearDown() throws IOException { this.client.close(); } }
创建索引库
编写DSL语句
public class HotelConstants {
public static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\" : {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\" : {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"addres\" : {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"price\" : {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"score\" : {\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"brand\" : {\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\n" +
" },\n" +
" \"city\" : {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"starName\" : {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \n" +
" \"location\" : {\n" +
" \"type\": \"geo_point\"\n" +
" },\n" +
" \"pic\" : {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"all\" : {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}
编写测试类
@Test
void createHotelIndex() throws IOException {
//1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
//2.准备请求的参数:DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
//3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
查看运行结果
删除索引库
// 删除索引
@Test
void testDeleteHotelIndex() throws IOException {
//1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
运行结果
判断索引库是否存在
// 判断索引是否存在
@Test
void testExistHotelIndex() throws IOException {
//1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
//2.发送请求
boolean exist = client.indices().exists(request, RequestOptions.DEFAULT);
//3.判断结果
System.out.println(exist ? "索引库已经存在!" : "索引库不存在!");
}
运行结果
总结
索引库操作的基本步骤:
-
初始化RestHighLevelClient
-
创建XxxIndexRequest。xxx是CREATE,Get,Delete
-
准备DSL(CREATE时需要)
-
发送请求。调用RestHighLevelClient#indices().xx()方法,xxx时create, exists, delete
RestClient操作文档
基本步骤
利用JavaRestClient实现文档的CRUD
去数据库查询酒店数据,导入到hotel索引库,实现酒店数据的CRUD。
基本步骤如下:
-
初始化JavaRestClient
-
利用JavaRestClient新增酒店数据
-
利用JavaRestClient根据id查询酒店数据
-
利用javaRestClient删除酒店数据
-
利用JavaRestClient修改酒店数据
新增文档
先查询酒店数据,然后给这条数据创建倒排索引,即可完成添加:
示例
//RestClient的新增数据
@Test
void testAddDocument() throws IOException {
// 根据id查询酒店数据
Hotel hotel = hotelService.getById(39141L);
// 转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//2.准备Json文档
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3.发送请求
client.index(request,RequestOptions.DEFAULT);
}
运行结果
查询文档
根据id查询到文档数据是json,需要反序列化为java对象:
示例
//RestClient的查询数据
@Test
void testGetDocument() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "39141");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
运行结果
修改文档
修改文档数据有两种方式:
方式一:全局更新。再次写入id一样的文档,就会删除旧文档,添加新文档
方式二:局部更新。只跟新局部字段,我们演示方式二
示例
//RestClient的修改数据
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "39141");
// 2.准备请求参数
request.doc(
"price" , "952",
"starName" , "四钻"
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}
运行结果
修改前
修改后
删除文档
示例
//RestClient的删除数据
@Test
void testDeleteDocument() throws IOException {
//准备Request
DeleteRequest request = new DeleteRequest("hotel", "39141");
//发送请求参数
client.delete(request, RequestOptions.DEFAULT);
}
运行结果
删除前
删除后
总结
文档操作的基本步骤
-
初始化RestHighLevelClient
-
创建XxxRequest。XXX是Index,Get,Update,Delete
批量导入文档
利用JavaRestClient批量导入索引库中
思路:
-
利用mybatis-plus查询酒店数据
-
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
-
利用JavaRestClient总的Bulk批量处理,实现批量新增文档
//批量导入es @Test void testBulkRequest() throws IOException { // 批量查询酒店数据 List<Hotel> hotels = hotelService.list(); // 1.创建Request BulkRequest request = new BulkRequest(); // 2.准备参数,添加多个新增的Request //转换为文档类型HotelDoc for (Hotel hotel : hotels) { HotelDoc hotelDoc = new HotelDoc(hotel); //创建新增文档的Request对象 request.add(new IndexRequest("hotel") .id(hotelDoc.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON)); } // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT); }
运行结果
DSL查询文档
DSL查询分类
Elasticashearch提供了基于JSON的DSL来定义查询。常见查询类型包括:
-
查询所有:查询出所有数据,一般测试用。例如:mathc_all
-
全文检索(full text)查询:利用分词器对用户输入内容分词,然后倒排索引库中匹配。例如:
-
match_query
-
multi_match_query
-
-
精准查询:根据精准词条查找数据,一般是查找keyword,数值,日期,boolean等类型字段。例如
-
ids
-
range
-
term
-
-
地理(geo)查询:根据经纬度查询。例如
-
geo_distance
-
geo_bounding_box
-
-
复合(compound)查询:复合查询可以将上述各种条件组合起来,合并查询条件。例如:
-
bool
-
funcation_score
-
DSL Query基本语法
#查询所有
GET /hotel/_search
{
"query": {
"match_all": {}
}
}
运行结果
总结
查询DSL的基本语法是什么?
-
GET /索引库名/_search
-
{ "query" : { "查询类型" : { "FIELD" : "TEXT" } } }
全文检索查询
match查询:
全文检索查询的一种,会对用户输入内容分词,然后去倒排索引检索,语法:
# match查询
GET /hotel/_search
{
"query": {
"match": {
"all": "外滩如家"
}
}
}
multi_match查询:
与match查询类似,只不过允许同时查询多个字段,语法:
# multi_match查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "外滩如家",
"fields": ["brand","name","business"]
}
}
}
总结
match和multi_match的区别是什么?
-
match:更具一个字段查询
-
muti_match:根据多个字段查询,参与查询字段越多,查阅性能越差
精准查询
准确查询一般是查找keyword,数值,日期,boolean等类型字段。所以不会对搜索条件分词。常见的有:
term查询:
#term查询
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}
range查询:
# range查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 200
}
}
}
}
总结
精确查询常见的有哪些?
-
term查询:根据词条精确匹配,一般搜索keywored类型,数值类型,布尔类型,日期类型字段
-
range查询:根据数值查询范围,可以是数值,日期的范围
地理坐标查询
根据经纬度查询。常见的使用场景包括:
-
携程:搜索我的附近的酒店
-
滴滴:搜索我附近的出租车
-
微信:搜索我附近的人
根据经纬度查询。例如:
geo_bounding_box:
查询geo_point值落在某个矩形范围所有文档
geo_distance:
查询到指定中心点小于某个距离值的所有文档
# distance查询 GET /hotel/_search { "query": { "geo_distance": { "distance": "5km", "location": "31.21, 121.5" } } }
复合查询
复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,例如:
function score
算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时安装分值降序排列。
例如,我们搜索”虹桥如家“,结果如下:
使用function score query,可以修改文档的相关性算分(query score),根据新得到得算分排序。
# function score查询
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
],
"boost_mode": "sum"
}
}
}
总结
function score query定义得三要素是什么?
-
过滤条件:哪些文档要加分
-
算分函数:如何计算function score
-
加权方式:function score 与 query score如何运算
Bloolean Query
参与算分越多,越影响性能。
布尔查询是一个或多个子句得组合。子查询得组合方式有:
-
must:必须匹配每个子查询,类似 ”与“
-
should:选择性匹配子查询,类似 ”或“
-
must_not:必须不匹配,不参与算法,类似”非“
-
filter:必须匹配,不参与算分
案例:
利用bool查询名字包含 ”如家“,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
# bool 查询
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
}
}
}
]
}
}
}
总结
elasticsearch中的相关性打分算法时什么?
-
TF-IDF:在elasticserch5.0之前,会随着词频增加反而越来越大
-
BM25:在elasticsearch5.0之后,会随着词频增大而增大,但增长曲线会趋于水平
搜索结果处理
排序
elaticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型,数值类型,地理坐标类型,日期类型等。
示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
# sort排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": "desc"
},
{
"price": "asc"
}
]
}
示例:实现对酒店数据按照你的位置坐标的距离升序排序
获取经纬度的方式:获取鼠标点击经纬度-地图属性-示例中心-JS API 2.0 示例 | 高德地图API
# 找到121.612282,31.034661周围的酒店,距离升序排序
GET /hotel/_search
{
"query": {
"match_all": {
}
},
"sort" : [
{
"_geo_distance":{
"location":{
"lat": "31.034661",
"lon": "121.612282"
},
"order": "asc",
"unit" : "km"
}
}
]
}
分页
elatcsearch默认情况下只返回top10的数据,而如果要查询更多数据就需要修改分页参数了。
elatcsearch中通过修改from,size参数来控制返回的分页结果:
# 分页查询
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": "asc"
}
],
"from": 10,
"size": 10
}
深度ES是分布式的,所以会面临深度分页问题。例如按price排序后,后去from = 990,size = 10的数据:
-
首先在每个数据分片上都排序并查询前1000条文档。
-
然后将所有结点的结果聚合,在内存中重新排序选出前1000条文档
-
最后从这1000条中,选取从990开始的10条文档
如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗越高。硬扯ES设定结果查询上限时10000
针对深度分页,ES提供了两种解决方案:
-
search after:分页时需要排序,原理上是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
-
scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
总结
from + size:
-
优点:支持随机翻页
-
缺点:深度分页问题,默认查询上限(from+size)是10000
-
场景:百度,京东,谷歌,淘宝这样的随机翻页搜索
after search:
-
优点:没有查询上线(单次查询的size不超过10000)
-
缺点:智能下岗后逐页查询,不支持随机翻页
-
场景:没有随机分页需求的搜索,例如手机向下滚动翻页
scroll:
-
优点:没有查询上限(单次查询的size不超过10000)
-
缺点:会有额外内存损耗,并且搜索结果是非实时的
-
场景:海量数据的获取和迁移。重ES7.1开始不推荐使用,建议用after search 方案
高亮
高亮:就是在搜索结果中把收索关键字突出显示。
原理是这样的:
-
将搜索结果中的关键字用标签标记出来
-
在页面中给标签添加css样式
语法:
总结
RestClient查询文档
快速入门
我们通过match_all来演示下基本api,先看DSL的组织:
代码
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
}
运行结果
我们通过match_all来演示下基本的API,再看结果的解析:
代码
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
//4.解析响应
SearchHits searchHits = response.getHits();
//4.1. 获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到"+ total+"条数据");
//4.2. 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3. 便利
for (SearchHit hit : hits){
// 获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc"+hotelDoc);
}
System.out.println(response);
}
运行结果
RestAPI其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询,排序,分页,高亮等所有功能
RestAPI中其中构建查询条件的核心是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法:
总结
-
创建SearchRequest对象
-
准备Request.source(),也就是DSL.
-
QueryBuilders来构建查询条件
-
传入Request.source()的query()方法
-
-
发送请求,得到结果
-
解析结果(参考JSON结果,从外到内,逐层解析)
match查询
代码
@Test
void testMatch() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchQuery("all","如家"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
extracted(response);
System.out.println(response);
}
private void extracted(SearchResponse response) {
//4.解析响应
SearchHits searchHits = response.getHits();
//4.1. 获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到"+ total+"条数据");
//4.2. 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3. 便利
for (SearchHit hit : hits){
// 获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc"+hotelDoc);
}
}
运行结果
精确查询
复合查询
精确查询常见的有term查询和range查询,同样利用QueryBuilders实现:
@Test void testBool() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest("hotel"); // 2.准备DSL // 2.1. 准备BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //2.2. 添加term boolQuery.must(QueryBuilders.termQuery("city","上海")); // 2.3. 添加range boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250)); request.source().query(boolQuery); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); System.out.println(response); extracted(response); } private void extracted(SearchResponse response) { //4.解析响应 SearchHits searchHits = response.getHits(); //4.1. 获取总条数 long total = searchHits.getTotalHits().value; System.out.println("共搜索到"+ total+"条数据"); //4.2. 文档数组 SearchHit[] hits = searchHits.getHits(); //4.3. 便利 for (SearchHit hit : hits){ // 获取文档source String json = hit.getSourceAsString(); //反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println("hotelDoc"+hotelDoc); } }
总结
要构建查询条件,只要记住一个类:QueryBuilders
排序和分页
代码
@Test
void testPageAndSort() throws IOException {
//页码,每页大小
int page = 2,size = 5;
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1. 准备Query
request.source().query(QueryBuilders.matchAllQuery());
// 2.2. 排序 sort
request.source().sort("price", SortOrder.ASC);
// 2.3. 分页 from,size
request.source().from((page-1)*size).size(5);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
extracted(response);
}
运行结果
高亮
高亮API包括请求DSL构建和结果解析两部分,我们先看请求的DSL构建:
高亮结果处理
代码
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1. 准备Query
request.source().query(QueryBuilders.matchQuery("all","如家"));
// 2.2. 高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
handleResponse(response);
}
private void handleResponse(SearchResponse response) {
//4.解析响应
SearchHits searchHits = response.getHits();
//4.1. 获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到"+ total+"条数据");
//4.2. 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3. 遍历
for (SearchHit hit : hits){
// 获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, 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].string();
// 覆盖非高亮结果
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc"+hotelDoc);
}
}
运行结果
总结
-
所有的搜索DSL的构建,记住一个API:SearchRequest的source()方法
-
高亮结果解析是参考JSON结果,逐层解析
黑马旅游案例
案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
步骤:
-
定义实体类,接收前端请求
-
定义controller接口,接收页面请求,调用IHotelService的search方法
-
定义IHotelService中的search方法,利用match查询实现根据关键字搜索酒店信息
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1 query
String key = params.getKey();
if (key == null || key.trim().length() == 0){
request.source().query(QueryBuilders.matchAllQuery());
} else {
request.source().query(QueryBuilders.matchQuery("all",key));
}
// 2.2. 分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
handleResponse(response);
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private PageResult handleResponse(SearchResponse response) {
//4.解析响应
SearchHits searchHits = response.getHits();
//4.1. 获取总条数
long total = searchHits.getTotalHits().value;
//4.2. 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3. 遍历
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits){
// 获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
hotels.add(hotelDoc);
}
// 4.4.封装返回
return new PageResult(total, hotels);
}
案例2:添加品牌,城市,星际,价格等过滤功能
步骤:
-
修改RequestParams类,添加brand,city,starName,minPrice,maxPrice等参数
-
修改search方法的实现类,再关键字搜索时,如果brand等参数存在,对其做过滤
-
city精确匹配
-
brand精确匹配
-
starNmae精确匹配
-
price范围过滤
-
注意事项
-
多个条件之间时AND关系,组合多条件用BooleanQuery
-
参数存在才需要过滤,做好非空判断
-
-
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {
try {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1 query
// 构建BooleanQuery
buildBasicQuery(params,request);
// 2.2. 分页
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void buildBasicQuery(RequestParams params,SearchRequest request) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//关键字搜索
String key = params.getKey();
if (key == null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
boolQuery.must(QueryBuilders.matchQuery("all",key));
}
// 城市条件
if (params.getCity() != null && !params.getCity().equals("")){
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// 品牌条件
if (params.getBrand() != null && !params.getBrand().equals("")){
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// 星级条件
if (params.getStarName() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// 价格条件
if (params.getMinPrice() != null && !params.getMinPrice().equals("")){
boolQuery.filter(QueryBuilders
.rangeQuery("price").gte(params.getMaxPrice()).lte(params.getMaxPrice()));
}
request.source().query(boolQuery);
}
private PageResult handleResponse(SearchResponse response) {
//4.解析响应
SearchHits searchHits = response.getHits();
//4.1. 获取总条数
long total = searchHits.getTotalHits().value;
//4.2. 文档数组
SearchHit[] hits = searchHits.getHits();
//4.3. 遍历
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits){
// 获取文档source
String json = hit.getSourceAsString();
//反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
hotels.add(hotelDoc);
}
// 4.4.封装返回
return new PageResult(total, hotels);
}
案例3:我附近的酒店
步骤
-
前端页面定位后,会将你所有的位置发送到后台:
-
我们根据这个坐标,将酒店结果按照这个点的距离升序排序。
-
思路如下:
-
修改RequestParams参数,接收location字段
-
修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
-
案例4:让指定的酒店再搜索中排名位置置顶
步骤
我们给需要置顶的酒店文档添加一个标记。然后利用function score给带有标记的文档增加权重。
实现步骤分析:
-
给HotelDoc类添加isAD字段,Boolean类型
-
挑选几个你喜欢的酒店,给它的文档数据添加isAD字段,值为true
-
修改search方法,添加function score功能,给isAD值为true的酒店增加权重
数据聚合
聚合的种类
聚合分类
聚合(aggregatons)可以实现对文档数据的统计,分析,运算。聚合常见的有三类:
-
桶(Bucket)聚合:用来对文档做分组
-
TermaAggregation:按照文档字段值分组
-
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
-
度量(Metric)聚合:用以计算一些值,比如:最大值,最小值,平均值等
-
Avg:求平均值
-
Max:求最大值
-
Min:求最小值
-
Stats:同时求max,min,avg,sum等
-
-
管道(pipeline)聚合:其它聚合的结果为基础做聚合
总结
什么是聚合?
聚合是对文档数据的统计,分析,计算
聚合的常见种类有哪些?
-
Bucket:对文档数据分组,并统计每组数量
-
Meric:最文档数做计算,例如avg
-
Pipeline:基于其他聚合结果在做聚合
参与聚合的字段类型必须是:
-
keword
-
数值
-
日期
-
布尔
DSL实现聚合
DSL实现Bucket聚合
现在,我们要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。
类型为term类型,DSL示例:
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照 _count升序排序。
我们可以修改结果排序方式:
#聚合功能,自定义排序规则
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20,
"order": {
"_count": "asc"
}
}
}
}
}
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以根据聚合的文档范围,只要添加query条件即可:
#聚合功能,限定聚合范围
GET /hotel/_search
{
"query": {
"range": {
"price": {
"lte": 200
}
}
},
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
总结
aggs代表聚合,与query同级,此时query的作用是?
限定聚合的文档范围
聚合必须的三要素
-
聚合名称
-
聚合类型
-
聚合字段
聚合可配置属性有:
-
size:指定聚合结果数量
-
order:指定聚合结果排序方式
-
field:指定聚合字段
DSL实现Metrics聚合
例如,我们要求获取每个品牌的用户评分的min,max,avg等值。
我们可以利用stats聚合:
#嵌套聚合metric
GET /hotel/_search
{
"size": 0,
"aggs": {
"brandAgg": {
"terms": {
"field": "brand",
"size": 20,
"order": {
"scoreAgg.avg": "desc"
}
},
"aggs": {
"scoreAgg": {
"stats": {
"field": "score"
}
}
}
}
}
}
RestAPI实现聚合
我们以品牌聚合为例,演示以下Java的RestClient使用,先看请求组装:
再看下聚合结果解析
@Test
void testAggregation() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
// 2.1 设置size
request.source().size(0);
// 2.2. 聚合
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(10)
);
// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析结果
Aggregations aggregations = response.getAggregations();
Terms brandTerms = aggregations.get("brandAgg");
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3遍历
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println(key);
}
}
案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合
需求:在搜索页面的品牌,城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的:
@Override
public Map<String, List<String>> filters() {
try {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备DSL
// 2.1 设置size
request.source().size(0);
// 2.2. 聚合
buildAggregation(request);
// 3. 发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Map<String, List<String>> result = new HashMap<>();
// 4. 解析结果
Aggregations aggregations = response.getAggregations();
// 4.1. 根据品牌名称,获取品牌结果
List<String> brandList = getAggByName(aggregations,"brandAgg");
// 4.4. 放入map
result.put("品牌",brandList);
List<String> cityList = getAggByName(aggregations,"cityAgg");
// 4.4. 放入map
result.put("城市",cityList);
List<String> starList = getAggByName(aggregations,"starAgg");
// 4.4. 放入map
result.put("星级",starList);
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private List<String> getAggByName(Aggregations aggregations,String aggName) {
// 4.1. 根据聚合名称获取聚合结果
Terms brandTerms = aggregations.get(aggName);
// 4.2. 获取buckets
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
// 4.3遍历
List<String> brandList = new ArrayList<>();
for (Terms.Bucket bucket : buckets) {
String key = bucket.getKeyAsString();
System.out.println(key);
brandList.add(key);
}
return brandList;
}
private void buildAggregation(SearchRequest request) {
request.source().aggregation(AggregationBuilders
.terms("brandAgg")
.field("brand")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("cityAgg")
.field("city")
.size(100)
);
request.source().aggregation(AggregationBuilders
.terms("starAgg")
.field("starName")
.size(100)
);
}
前端页面会向服务端发起请求,查询品牌,城市,星级等字段的聚合结果:
自动补全
拼音分词器
要实现根据字母做补全,就必须对文档按照拼音分词。在GiHub上恰好有elasticsearch的拼音分词插件。地址:
测试
自定义分词器
elasticsearch中分词器(analyzer)的组成包含三部分:
-
character fiters:在tokenizer之前对文本进行处理。例如删除字符,替换字符
-
tokenizer:将文本按照按照一定的规则切割词条(term)。例如keyword,就是不分词;还有ik_smart
-
tokenizer filter :将tokenizer输出的词条做进一步处理。例如大小写转换,同义词处理,拼音处理等
拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。
# 自定义分词器
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"
, "search_analyzer": "ik_smart"
}
}
}
}
POST /test/_doc/1
{
"id": 1,
"name": "狮子"
}
POST /test/_doc/2
{
"id": 2,
"name": "虱子"
}
GET /test/_search
{
"query": {
"match": {
"name": "调入狮子笼子咋办"
}
}
}
总结
如何使用拼音分词器?
-
下载pinyin分词器
-
解压并放到elasticsearch的plugin目录
-
重启即可
如何自定义分词器?
-
创建索引库,在settings中配置,可以包含三部分
-
character filter
-
tokenizer
-
filter
拼音分词器注意思事项?
为了避免搜索到同音字,搜索时不要使用拼音分词器
自动补全查询
completion suggester查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
-
参与补全查询的字段必须时completion类型。
-
字段的类容一般是用来补全的多词条形成的数组
#自动补全索引库
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"]
}
# 自动补全查询
GET /test2/_search
{
"suggest": {
"titleSuggest": {
"text": "s",
"completion": {
"field": "title",
"skip_duplicates": true,
"size": 10
}
}
}
}
总结
自动补全对字段的要求:
-
类型是completion类型
-
字段值是多词条的数组
实现酒店搜索框自动补全
实现思路如下:
-
修改hotel索引库结构,设置自定义拼音分词器
-
修改索引库的name,all字段,使用自定义分词器
-
索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
-
给HoteDoc类添加suggestio字段,内容包含brand,susiness
-
重新导入数据到hotel库
@Override
public List<String> getSuggestions(String prefix) {
try {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备SDL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix(prefix)
.skipDuplicates(true)
.size(10)
));
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析响应结果
Suggest suggest = response.getSuggest();
// 4.1. 根据补全查询名称,获取补全结果
CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
//4.2. 获取options
List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
//4.3. 遍历
List<String> list = new ArrayList<>(options.size());
for (CompletionSuggestion.Entry.Option option : options){
String text = option.getText().toString();
list.add(text);
}
return list;
} catch (IOException e) {
throw new RuntimeException();
}
}
RestAPI实现自动补全
先看请求参数构造API
@Test
void testSuggest() throws IOException {
// 1. 准备Request
SearchRequest request = new SearchRequest("hotel");
// 2. 准备SDL
request.source().suggest(new SuggestBuilder().addSuggestion(
"suggestions",
SuggestBuilders.completionSuggestion("suggestion")
.prefix("h")
.skipDuplicates(true)
.size(10)
));
// 3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4. 解析响应结果
System.out.println(response);
}
自动补全
数据同步
数据同步思路分析
elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticserch于mysql之间的数据同步。
方案一:同步调用
方案二:异步调用
方案三:监听binlog
总结
方式一:同步调用
-
优点:实现简单,粗暴
-
缺点:业务耦合度高
方式二:异步通知
-
优点:低耦合,实现难度一般
-
缺点:依赖mq的可靠性
方式三:监听binlog
-
优点:完全解除服务耦合
-
缺点:开启binlog增加数据库负担,实现复杂度高
实现elasticsearch与数据库同步
利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增,删,查,改时,要求对elasticsearch中数据也要完成相同操作。
利用MQ实现mysql于elasticsearch中数据也要完成操作。
步骤:
-
导入课前资料提供的hotel-admin项目,启动并测试酒店数据的CRUD
-
声明exchange,queue,RoutingKey
-
在hotel-admin中的增,删,改业务中完成发送消息
-
在hotel-demo中完成消息监听,并更新elasticsearch中数据
-
启动并测试数据同步功能
elasticsearch集群
搭建ES集群
ES集群结构
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题,单点故障问题。
-
海量数据存储问题:将索引从逻辑上拆分为N个分片(shard),存储到多个结点
-
单点故障问题:将分片数据在不同结点备份(replica)
文件资料里有对应文档。
集群脑裂问题
elasticsearch中集群结点有不同的职责划分:
结点类型 | 配置参数 | 默认值 | 节点职责 |
---|---|---|---|
master eligible | node.master | true | 备选主节点:主节点可以管理和记录集群状态,决定分片在哪个节点,处理创建和删除索引库的请求 |
data | node.data | true | 数据节点:存储数据,搜索,聚合,CRUD |
ingest | node.ingest | true | 数据存储之前的预处理 |
coordinating | 上面3个参数都是为false则为coordinating节点 | 无 | 路由请求到其他节点,合并其它节点处理的结果,返回给用户 |
ES集群的脑裂
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点于其它节点网路解耦故障时,可能发生脑裂问题。
为了避免脑裂,需要要求选票超过(eligible节点数量+1)/ 2 才能当选为主节点,因此eligible节点数量组好是奇数。对于配置项discovery.zen.minimum_master_nodes,在es7.0以后,已经成为我们配置,因此一般不会发生脑裂问题
总结
master eligible节点的作用是上面?
-
参与集群选主
-
主节点可以管理集群状态,管理分片信息,处理创建和删除索引库的请求
-
data节点的作用是上面?
data节点的作用是上面?
-
数据的CRUD
coordinator节点的作用是什么?
-
路由请求到其它节点
-
合并查询到的结果,返回给用户
集权故障转移
集群分布式存储
当新增文档时,应该保存不同分片,保证数据均衡,那么coordinating node如何确定数据存储到哪个分片呢?
elasticsearch会通过hash算法来计算文档应该存储到哪个分片:
shard = hash(_routing) % number_of_shards
说明:
-
_routing默认是文旦的id
-
算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
集群分布式查询
elasticsearch的查询分成两个阶段:
-
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
-
gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
总结
分布式新增如何确定分片?
-
coordinating node根据id做hash运算,得到结果对shard数量取余,余数就是对应的分片
分布式查询:
-
分散阶段:coordinating node将查询请求分发给不同分片
-
收集阶段:将查询结果汇总到coordinating node,整理并返回给用户
ES集群故障转移
集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它急待你,确保数据安全,这个叫做故障转移
总结
故障转移
-
master宕机后,EligibleMaster选举为新的主节点。
-
master节点监控分片,阶段状态,将故障节点上的分片转移到正常节点,确保数据安全。