Elasticsearch搜索引擎
���录
初识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 /_***yze{ "***yzer": "standard", "text": "黑马程序员学习java太棒了"} 安装IK分词器
查看数据卷
上传ik安装包
重启docker容器
docker restart es
测试
IK分词器包含两种模式:
-
ik_smart:最少切分
-
ik_max_word:最细切分
POST /_***yze{ "***yzer": "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
-
***yzer:使用哪种分词器
-
properties:该字段的子字段
总结
mapping常见属性有哪些?
-
type:数据类型
-
index:是否索引
-
***yzer:分词器
-
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", "***yzer": "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", "***yzer": "ik_max_word" }
初始化JavaRestClient
-
引入es的RestHighLeveClient依赖:
org.elasticsearch.client elasticsearch-rest-high-level-client 7.12.1
-
因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
1.8 7.12.1
-
初始化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" + " \"***yzer\": \"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" + " \"***yzer\": \"ik_max_word\"\n" + " }\n" + " }\n" + " }\n" + "}";} 编写测试类
@Testvoid 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的新增数据@Testvoid 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的查询数据@Testvoid 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的修改数据@Testvoid testUpdateDocument() throws IOException { // 1.准备Request UpdateRequest request = new UpdateRequest("hotel", "39141"); // 2.准备请求参数 request.doc( "price" , "952", "starName" , "四钻" ); // 3.发送请求 client.update(request, RequestOptions.DEFAULT);} 运行结果
修改前
修改后
删除文档
示例
//RestClient的删除数据@Testvoid 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@Testvoid testBulkRequest() throws IOException { // 批量查询酒店数据 List 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的组织:
代码
@Testvoid 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,再看结果的解析:
代码
@Testvoid 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查询
代码
@Testvoid 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
排序和分页
代码
@Testvoid 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构建:
高亮结果处理
代码
@Testvoid 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 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查询实现根据关键字搜索酒店信息
@Overridepublic 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 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
-
参数存在才需要过滤,做好非空判断
-
-
@Autowiredprivate RestHighLevelClient client;@Overridepublic 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 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聚合:
#嵌套聚合metricGET /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