目录
初识elasticsearch
了解ES
什么是elasticsearch
elasticsearch的发展
搜索引擎技术排名:
总结
倒排索引
正向索引和倒排索引
正向索引
倒排索引
总结
es的一些概念
文档
索引
概念对比
架构
总结
安装es,kibana
安装es
安装kibana
安装分词器
分词器
安装IK分词器
查看数据卷
上传ik安装包编辑
重启docker容器
测试
IK分词器扩展和停用词典编辑
总结
索引库操作
mapping映射属性
mapping属性编辑
总结
索引库CRUD
创建索引库
查看,删除索引库编辑
示例:查询
示例:删除
修改索引库
示例:修改
总结
文档操作
新增文档
新增文档的DSL语法如下:编辑
示例
查询文档
DSL语法编辑
示例
删除文档
DSL语法编辑
示例
修改文档
方式一:全量修改,会删除旧文档,添加新文编辑
示例
方式二:增加修改,修改指定字段值编辑
示例
总结
RestAPI
RestClient操作索引库
初始化JavaRestClient
创建索引库编辑
删除索引库
判断索引库是否存在
总结
RestClient操作文档
基本步骤
新增文档
示例
运行结果编辑编辑
查询文档
示例
运行结果编辑
修改文档
示例
运行结果
修改前编辑
修改后编辑
删除文档
示例
运行结果
删除前
删除后编辑
总结
批量导入文档
利用JavaRestClient批量导入索引库中
DSL查询文档
DSL查询分类
DSL Query基本语法编辑
总结
全文检索查询编辑
match查询:
multi_match查询:
总结
精准查询
term查询:
range查询:
总结
地理坐标查询
geo_bounding_box:
geo_distance:
复合查询
function score
总结
Bloolean Query
案例:
总结
搜索结果处理
排序
示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
示例:实现对酒店数据按照你的位置坐标的距离升序排序
分页
针对深度分页,ES提供了两种解决方案:
总结
高亮编辑
总结编辑
RestClient查询文档
快速入门
我们通过match_all来演示下基本api,先看DSL的组织:编辑
代码
运行结果编辑
我们通过match_all来演示下基本的API,再看结果的解析:编辑
代码
运行结果编辑
总结
match查询
代码
运行结果编辑
精确查询编辑
复合查询
总结
排序和分页
代码
运行结果编辑
高亮
高亮结果处理编辑
代码
运行结果编辑
总结
黑马旅游案例
案例1:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
步骤:
案例2:添加品牌,城市,星际,价格等过滤功能
步骤:
案例3:我附近的酒店
步骤
案例4:让指定的酒店再搜索中排名位置置顶
步骤
数据聚合
聚合的种类
聚合分类
总结
什么是聚合?
聚合的常见种类有哪些?
参与聚合的字段类型必须是:
DSL实现聚合
DSL实现Bucket聚合
总结
DSL实现Metrics聚合
RestAPI实现聚合
案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合
自动补全
拼音分词器
测试编辑
自定义分词器
总结
自动补全查询
总结
实现酒店搜索框自动补全
RestAPI实现自动补全
自动补全编辑
数据同步
数据同步思路分析
方案一:同步调用编辑
方案二:异步调用编辑
方案三:监听binlog编辑
总结
实现elasticsearch与数据库同步
利用MQ实现mysql于elasticsearch中数据也要完成操作。
elasticsearch集群
搭建ES集群
ES集群结构
集群脑裂问题
ES集群的脑裂
总结
集权故障转移
集群分布式存储
集群分布式查询编辑
elasticsearch的查询分成两个阶段:
总结
ES集群故障转移
总结
初识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端口,访问端口
8.137.59.245: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的拼音分词插件。地址:
GitHub - infinilabs/analysis-pinyin: 🛵 This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.
测试
自定义分词器
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节点监控分片,阶段状态,将故障节点上的分片转移到正常节点,确保数据安全。