赞
踩
目录
示例:对酒店数据按照用户评价降序排序,评价相同的按照价格升序排序
我们通过match_all来演示下基本api,先看DSL的组织:编辑
我们通过match_all来演示下基本的API,再看结果的解析:编辑
案例:在IUserService中定义方法,实现对品牌,城市,星级的聚合
利用MQ实现mysql于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语言开发
学习曲线陡峭
不支持水平扩展
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,而后获取到文档
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负责海量数据的搜索,分析,计算
创建网络
因为我们还需要部署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可以给我们提供一个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太棒了"
- }
docker restart es
IK分词器包含两种模式:
ik_smart:最少切分
ik_max_word:最细切分
- POST /_analyze
- {
- "analyzer": "ik_max_word",
- "text": "黑马程序员学习java太棒了"
- }
分词器的作用是什么?
创建倒排索引时对文档分词
用户搜索时,对输入的内容分词
IK分词器又几种模式?
ik_smart:智能切分,粗粒度
ik_max_word:最细切分,细粒度
IK分词器如何扩展词条?如何停用词条?
利用config目录的IKAnalyzer.cfg.xml文件添加扩展词条和停用词典
在词典中添加扩展词条或者停用词条
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
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 (可以添加字段但不能修改以前的字段)
- #插入文档
- 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
- }
-
- #查询文档
- 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" : "赵"
- }
- }
- }
- #删除文档
- 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":{字段}}
什么是RestClient:
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:Elasticsearch Clients | Elastic
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" }
引入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
利用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
思路:
利用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);
- }
运行结果
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
- #查询所有
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- }
- }
运行结果
查询DSL的基本语法是什么?
GET /索引库名/_search
{ "query" : { "查询类型" : { "FIELD" : "TEXT" } } }
全文检索查询的一种,会对用户输入内容分词,然后去倒排索引检索,语法:
- # match查询
- GET /hotel/_search
- {
- "query": {
- "match": {
- "all": "外滩如家"
- }
- }
- }
与match查询类似,只不过允许同时查询多个字段,语法:
- # multi_match查询
- GET /hotel/_search
- {
- "query": {
- "multi_match": {
- "query": "外滩如家",
- "fields": ["brand","name","business"]
- }
- }
- }
match和multi_match的区别是什么?
match:更具一个字段查询
muti_match:根据多个字段查询,参与查询字段越多,查阅性能越差
准确查询一般是查找keyword,数值,日期,boolean等类型字段。所以不会对搜索条件分词。常见的有:
- #term查询
- GET /hotel/_search
- {
- "query": {
- "term": {
- "city": {
- "value": "上海"
- }
- }
- }
- }
- # range查询
- GET /hotel/_search
- {
- "query": {
- "range": {
- "price": {
- "gte": 100,
- "lte": 200
- }
- }
- }
- }
精确查询常见的有哪些?
term查询:根据词条精确匹配,一般搜索keywored类型,数值类型,布尔类型,日期类型字段
range查询:根据数值查询范围,可以是数值,日期的范围
根据经纬度查询。常见的使用场景包括:
携程:搜索我的附近的酒店
滴滴:搜索我附近的出租车
微信:搜索我附近的人
根据经纬度查询。例如:
查询geo_point值落在某个矩形范围所有文档
查询到指定中心点小于某个距离值的所有文档
# distance查询 GET /hotel/_search { "query": { "geo_distance": { "distance": "5km", "location": "31.21, 121.5" } } }
复合(compound)查询:复合查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑,例如:
算分函数查询,可以控制文档相关性算分,控制文档排名。例如百度竞价
当我们利用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如何运算
参与算分越多,越影响性能。
布尔查询是一个或多个子句得组合。子查询得组合方式有:
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
search after:分页时需要排序,原理上是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
from + size:
优点:支持随机翻页
缺点:深度分页问题,默认查询上限(from+size)是10000
场景:百度,京东,谷歌,淘宝这样的随机翻页搜索
after search:
优点:没有查询上线(单次查询的size不超过10000)
缺点:智能下岗后逐页查询,不支持随机翻页
场景:没有随机分页需求的搜索,例如手机向下滚动翻页
scroll:
优点:没有查询上限(单次查询的size不超过10000)
缺点:会有额外内存损耗,并且搜索结果是非实时的
场景:海量数据的获取和迁移。重ES7.1开始不推荐使用,建议用after search 方案
高亮:就是在搜索结果中把收索关键字突出显示。
原理是这样的:
将搜索结果中的关键字用标签标记出来
在页面中给标签添加css样式
语法:
- @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);
- }
- @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结果,从外到内,逐层解析)
- @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结果,逐层解析
定义实体类,接收前端请求
定义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);
-
- }
修改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);
-
- }
前端页面定位后,会将你所有的位置发送到后台:
我们根据这个坐标,将酒店结果按照这个点的距离升序排序。
思路如下:
修改RequestParams参数,接收location字段
修改search方法业务逻辑,如果location有值,添加根据geo_distance排序的功能
我们给需要置顶的酒店文档添加一个标记。然后利用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
数值
日期
布尔
现在,我们要统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。
类型为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:指定聚合字段
例如,我们要求获取每个品牌的用户评分的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"
- }
- }
- }
- }
- }
- }
我们以品牌聚合为例,演示以下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);
- }
- }
需求:在搜索页面的品牌,城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的:
-
- @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的拼音分词插件。地址:
https://www.wpsshop.cn/w/weixin_40725706/article/detail/819942
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。