SpringCloud -- elasticsearch(二)
目录
一、DSL查询
1. 基础语法
2. 叶子查询
2.1 全文检索查询
2.2 精确查询
3. 复合查询
4. 排序
5. 分页
5.1 基础分页
5.2 深度分页
6. 高亮
二、RestClient查询
1. 入门案例
1.1 发送请求
1.2 解析响应结果
1.3 总结
2. 叶子查询
3. 复合查询
4. 排序与分页
三、数据聚合
1. DSL实现聚合
1.1 Bucket聚合
1.2 Metric聚合
2. RestClient实现聚合
一、DSL查询
Elasticsearch的查询可以分为两大类:
-
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
-
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
1. 基础语法
查询的语法结构如下:
GET /{索引库名}/_search
:其中的_search
是固定路径,不能修改。
以最简单的无条件查询为例,无条件查询的类型是“match_all”,因此查询语句如下:
GET /items/_search
{"query": {"match_all": {}}
}
因为是无条件查询,所以查询条件可以不写。match_all类型表示就是查询所有的数据。
执行结果如下:
响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数。
2. 叶子查询
这里列举一些常见的叶子查询的类型:
全文检索查询:利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:match;multi_match。
精确查询:不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:ids;term;range。
地理坐标查询:用于搜索地理位置,搜索方式很多,例如:geo_bounding_box
:按矩形搜索;geo_distance
:按点和半径搜索
2.1 全文检索查询
以全文检索中的match为例,语法如下:
GET /{索引库名}/_search
{"query": {"match": {"字段名": "搜索条件"}}
}
示例:
一共查出7119条数据,都是name字段中包含“华为”和“荣耀”的数据。
与match
类似的还有multi_match
,区别在于可以同时对多个字段搜索,而且多个字段都要满足,语法示例:
GET /{索引库名}/_search
{"query": {"multi_match": {"query": "搜索条件","fields": ["字段1", "字段2"]}}
}
示例:
2.2 精确查询
精确查询是词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。
以term查询为例,语法如下:
GET /{索引库名}/_search
{"query": {"term": {"字段名": {"value": "搜索条件"}}}
}
range查询是范围查询,语法如下:
GET /{索引库名}/_search
{"query": {"range": {"字段名": {"gte": {最小值},"lte": {最大值}}}}
}
gte表示大于等于,lte表示小于等于。
3. 复合查询
复合查询就是基于逻辑运算组合叶子查询,实现组合条件,例如:bool。
bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:
-
must:必须匹配每个子查询,类似“与”
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
-
filter:必须匹配,不参与算分
如果参与算分,那么得分高的就会在前面进行展示。不参与算法,那么输入的搜索关键字就必须进行展示。
一般在搜索框中输入的条件都参与算分,对于点击的条件一般不参与算分。如下图:
bool查询的语法如下:
GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手机"}}],"should": [{"term": {"brand": { "value": "vivo" }}},{"term": {"brand": { "value": "小米" }}}],"must_not": [{"range": {"price": {"gte": 2500}}}],"filter": [{"range": {"price": {"lte": 1000}}}]}}
}
比如,我们要搜索手机
,但品牌必须是华为
,价格必须是900~1599
,那么可以这样写:
GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手机"}}],"filter": [{"term": {"brand": { "value": "华为" }}},{"range": {"price": {"gte": 90000, "lt": 159900}}}]}}
}
4. 排序
排序需要用到sort类型,它与query是同级别的。
语法:
GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"排序字段": {"order": "排序方式asc和desc"}}]
}
例如,按照商品价格降序排序:
GET /items/_search
{"query": {"match_all": {}},"sort": [{"price": {"order": "desc"}}]
}
5. 分页
Elasticsearch默认情况下只返回算分前十的数据,如果想查询更多的数据就需要修改分页参数了。
5.1 基础分页
elasticsearch中通过修改from
、size
参数来控制要返回的分页结果:
-
from
:从第几个文档开始 -
size
:总共查询几个文档
类似于MySQL中的limit ?,?
语法如下:
GET /items/_search
{"query": {"match_all": {}},"from": 0, // 分页开始的位置,默认为0"size": 10, // 每页文档数量,默认10"sort": [{"price": {"order": "desc"}}]
}
上述代码的意思是从第一页开始查,查10条数据,按照price字段进行降序排序。
5.2 深度分页
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。适合集群部署,这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
假设一个索引库中有100000条数据,分别存储到4个分片当中,每个分片25000条数据。如果我想要查询price字段大小前1000的文档,只能在每一个分片上都找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取990~1000的数据。
假如我们现在要查询的是第999页数据呢,是不是要找第9990~10000的数据,那岂不是需要把每个分片中的前10000名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。
因此elasticsearch会禁止from+ size
超过10000的请求。
针对深度分页,Elasticsearch使用search after来进行解决:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。
6. 高亮
高亮显示指的就是搜索的关键字会变成红色,例如我在浏览器搜索CSDN,CSDN就会高亮显示。
基本语法如下:
GET /{索引库名}/_search
{"query": {"match": {"搜索字段": "搜索关键字"}},"highlight": {"fields": {"高亮字段名称": {"pre_tags": "<em>","post_tags": "</em>"}}}
}
示例:
通过关键字“脱脂牛奶”进行搜索,对name字段中的相关词条进行高亮显示。
二、RestClient查询
1. 入门案例
1.1 发送请求
以match_all
查询为例,其DSL和JavaAPI的对比如图:
代码解读:
-
第一步,创建
SearchRequest
对象,指定索引库名 -
第二步,利用
request.source()
构建DSL,DSL中可以包含查询、分页、排序、高亮等-
query()
:代表查询条件,利用QueryBuilders.matchAllQuery()
构建一个match_all
查询的DSL
-
-
第三步,利用
client.search()
发送请求,得到响应
request.source()后可以跟sort、from、size个query同级别的参数。
1.2 解析响应结果
利用DSL查询,查询出来的是一对JSON数据。
解析SearchResponse
的代码就是在解析JSON结果,对比如下:
因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:
-
SearchHits
:通过response.getHits()
获取,就是JSON中的最外层的hits
,代表命中的结果-
SearchHits.
getTotalHits().value
:获取总条数信息 -
SearchHits.getHits()
:获取SearchHit
数组,也就是文档数组-
SearchHit#getSourceAsString()
:获取文档结果中的_source
,也就是原始的json
文档数据
-
-
1.3 总结
文档搜索的基本步骤是:
-
创建
SearchRequest
对象 -
准备
request.source()
,也就是DSL。-
QueryBuilders
来构建查询条件 -
传入
request.source()
的query()
方法
-
-
发送请求,得到结果
-
解析结果(参考JSON结果,从外到内,逐层解析)
2. 叶子查询
所有的查询条件都是由QueryBuilders来构建的,叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式,其它不动。
例如match查询代码如下:
@Test
void testMatch() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
multi_match查询:
@Test
void testMultiMatch() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
rang查询:
term查询:
@Test
void testTerm() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数request.source().query(QueryBuilders.termQuery("brand", "华为"));// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
3. 复合查询
复合查询也是由QueryBuilders
来构建,我们以bool
查询为例,DSL和JavaAPI的对比如图:
假如我想要搜索品牌为“德亚”的“脱脂牛奶”,同时价格要低于300,代码如下:
@Test
void testBool() throws IOException {// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数// 2.1.准备bool查询BoolQueryBuilder bool = QueryBuilders.boolQuery();// 2.2.关键字搜索bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));// 2.3.品牌过滤bool.filter(QueryBuilders.termQuery("brand", "德亚"));// 2.4.价格过滤bool.filter(QueryBuilders.rangeQuery("price").lte(30000));request.source().query(bool);// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
4. 排序与分页
排序、分页其DSL和JavaAPI的对比如下:
完整代码如下:
@Test
void testPageAndSort() throws IOException {int pageNo = 1, pageSize = 5;// 1.创建RequestSearchRequest request = new SearchRequest("items");// 2.组织请求参数// 2.1.搜索条件参数request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));// 2.2.排序参数request.source().sort("price", SortOrder.ASC);// 2.3.分页参数request.source().from((pageNo - 1) * pageSize).size(pageSize);// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}
三、数据聚合
聚合可以让我们极其方便地实现对数据的统计、分析、运算。
聚合常见的有三类:
-
桶(
Bucket
)聚合:用来对文档做分组-
TermAggregation
:按照文档字段值分组,例如按照品牌值分组、按照国家分组 -
Date Histogram
:按照日期阶梯分组,例如一周为一组,或者一月为一组
-
-
度量(
Metric
)聚合:用以计算一些值,比如:最大值、最小值、平均值等-
Avg
:求平均值 -
Max
:求最大值 -
Min
:求最小值 -
Stats
:同时求max
、min
、avg
、sum
等
-
-
管道(
pipeline
)聚合:其它聚合的结果为基础做进一步运算
1. DSL实现聚合
1.1 Bucket聚合
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket
聚合中的Term
聚合。
把size设置为0表示不需要有数据的传输。下面代码的意思就是将所有文档按category这个字段进行分类,category一样的分到一类(一个桶)中。
GET /items/_search
{"size": 0, "aggs": {"category_agg": {"terms": {"field": "category","size": 20}}}
}
语法说明:
-
size
:设置size
为0,就是每页查0条,则结果中就不包含文档,只包含聚合 -
aggs
:定义聚合-
category_agg
:聚合名称,自定义,但不能重复-
terms
:聚合的类型,按分类聚合,所以用term
-
field
:参与聚合的字段名称 -
size
:希望返回的聚合结果的最大数量
-
-
-
查询的结果如下:
可以看到上述的代码中并没有query关键字,这是因为对所有文档进行聚合,并没有加上条件。如果要进行条件聚合就要加上query关键字进行搜索。
1.2 Metric聚合
现有如下需求:统计所有价格高于3000的手机品牌,同时获取每个品牌价格的最小值、最大值、平均值。
代码如下:
GET /items/_search
{"query": {"bool": {"filter": [{"term": {"category": "手机"}},{"range": {"price": {"gte": 300000}}}]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20},"aggs": {"stats_meric": {"stats": {"field": "price"}}}}}
}
query
部分就不说了,重点解读聚合部分语法。
可以看到我们在brand_agg
聚合的内部,我们新加了一个aggs
参数。这个聚合就是brand_agg
的子聚合,会对brand_agg
形成的每个桶中的文档分别统计。
-
stats_meric
:聚合名称-
stats
:聚合类型,stats是metric
聚合的一种-
field
:聚合字段,这里选择price
,统计价格
-
-
由于stats是对brand_agg形成的每个品牌桶内文档分别做统计,因此每个品牌都会统计出自己的价格最小、最大、平均值。
2. RestClient实现聚合
可以看到在DSL中,aggs
聚合条件与query
条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()
方法来设置。
聚合条件的要利用AggregationBuilders
这个工具类来构造。DSL与JavaAPI的语法对比如下:
聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下: