Elasticsearch下篇
文章目录
- Elasticsearch下篇
- 1 DSL查询
- 1.1 快速入门
- 1.2 叶子查询
- 1.2.1 全文检索查询
- 1.2.2 精确查询
- 1.3 复合查询
- 1.4 排序和分页
- 1.5 高亮显示
- 2 JavaRestClient
- 2.1 快速入门
- 2.2 构建查询条件
- 2.3 排序和分页
- 2.4 高亮显示
- 3 数据聚合
- 3.1 DSL聚合
- 3.2 RestClient聚合
在上次学习中,我们已经导入了大量数据到elasticsearch中,实现了商品数据的存储。不过查询商品数据时 依然采用的是根据id查询,而非模糊搜索 。
所以今天,我们来研究下elasticsearch的数据搜索功能。Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。
因此,我们先学习DSL的查询语法,然后再基于DSL来对照学习JavaAPI,就会事半功倍。
1 DSL查询
Elasticsearch提供了DSL(Domain Specific Language)查询,就是以JSON格式来定义查询条件,类似这样:
DSL查询可以分为两大类:
- 叶子查询(Leaf query clauses): 一般就是在特定的字段里查询特定值,属于简单查询,很少单独使用。
- 复合查询(Compound query clauses): 以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
在查询以后,还可以对查询的结果做处理,包括:
- 排序:按照一个或多个字段值做排序
- 分页:根据from和size做分页,类似MySQL
- 高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目
- 聚合:对搜索结果做数据统计以形成报表
1.1 快速入门
基于DSL的查询语法如下:
# 查询所有
GET /items/_search
{
"query": {
"match_all": {
}
}
}
会发现虽然是match_all,但是响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数。
1.2 叶子查询
叶子查询还可以进一步细分,常见的有:
- 全文检索(full text)查询: 利用分词器对用户输入内容分词,然后去词条列表中匹配。例如:
- match_query
- multi_match_query
- 精确查询: 不对用户输入内容分词,直接精确匹配,一般是查找Keyword、数值、日期、布尔等类型。例如:
- ids
- range
- term
- 地理(geo)查询: 用于搜索地理位置,搜索方式很多。例如:
- geo_distance
- geo_bounding_box
1.2.1 全文检索查询
match查询: 全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索,语法:
multi_match: 与match查询类似,只不过允许同时查询多个字段,语法:
# match查询
GET /items/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
}
}
# multi_match查询
GET /items/_search
{
"query": {
"multi_match": {
"query": "牛奶",
"fields": ["name","category"]
}
}
}
1.2.2 精确查询
精确查询 ,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件在分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查询keyword、数值、日期、boolean类型的字段。例如id、price、城市、地名、人名等作为一个整体才有含义的字段。
注意:
#term 所有
GET /items/_search
{
"query": {
"term": {
"name": {
"value": "脱脂牛奶"
}
}
}
}
对name进行精确查询,很容易查询不到任何信息。原因是,name是进行分词处理的,属性就是 可分词的文本 ,那么由于脱脂牛奶可以分成两个词语,一个脱脂,一个牛奶,当进行精确查询时,由于此查询中“脱脂牛奶”不分词,因此找不到一个对应的信息,查询结果为空。
#range 所有
GET /items/_search
{
"query": {
"range": {
"price": {
"gt": 10000,
"lte": 20000
}
}
}
}
#ids 所有
GET /items/_search
{
"query": {
"ids": {
"values": ["584387","584392"]
}
}
}
1.3 复合查询
复合查询大致可以分为两类:
- 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如:
- bool
- 第二类:基于某种运算修改查询时的文档相关性算分,从而改变文档排名。例如:
- function_score
- dis_max
布尔查询 是一个或多个查询子句的组合。子查询的组合方式有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
需求: 搜索“智能手机”,但品牌必须是华为,价格是900-1599
GET /items/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "智能手机"
}
}
],
"filter": [
{
"term": {
"brand": "华为"
}
},
{
"range": {
"price": {
"gte": 90000,
"lte": 159900
}
}
}
]
}
}
}
1.4 排序和分页
elasticsearch支持对搜索结果排序,默认是根据相关度算法(_score)来排序,也可以指定字段排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
需求: 搜索商品,按照销量排序,销量一样则按照价格升序
# 排序查询
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"sold" : "desc"
},
{
"price": "asc"
}
]
}
elasticsearch默认情况下只返回top10的数据。而如果要查询更对数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:
- from:从第几文档开始
- size:总共查询几个文档
需求: 搜索商品,查询出销量排名前十的商品,销量一样时按照价格升序
#排序查询
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"sold" : "desc"
},
{
"price": "asc"
}
],
"from": 0,
"size": 10
}
深度分页问题
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。查询数据时需要汇总各个分片的数据。
假如要查询第100页数据,每页查10条:
实现思路:
① 对数据排序
② 找出第990-1000名
假如我们现在要查询的是第999页数据呢,是不是要找第9990~10000的数据,那岂不是需要把每个分片中的前10000名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。
因此elasticsearch会禁止from+ size
超过10000的请求。
针对深度分页,ES提供了两种解决方案:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式
- scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用
search after模式:
- 优点:没有查询上线,支持深度分页
- 缺点:只能向后逐页查询,不能随机翻页
- 场景:数据迁移,手机滚动查询
1.5 高亮显示
高亮显示: 就是在搜索结果中把搜索关键字突出显示。
# 高亮
GET /items/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
2 JavaRestClient
2.1 快速入门
数据搜索的java代码分成两部分:
- 构建并发起请求
- 解析查询结果
@Test
void testMatchAll() throws IOException {
//1. 创建request对象
SearchRequest request = new SearchRequest("items");
//2. 配置request参数
request.source()
.query(QueryBuilders.matchAllQuery());
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println("response = " + response);
}
解析查询结果的API:
@Test
void testMatchAll() throws IOException {
//1. 创建request对象
SearchRequest request = new SearchRequest("items");
//2. 配置request参数
request.source()
.query(QueryBuilders.matchAllQuery());
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
SearchHits searchHits = response.getHits();
//4.1 总条数
long value = searchHits.getTotalHits().value;
System.out.println("value = " + value);
//4.2 命中的数据
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//4.2.1 获取Source的结果
String json = hit.getSourceAsString();
//4.2.2 处理对象 比如转为ItemDoc
ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
System.out.println("doc = " + doc);
}
}
2.2 构建查询条件
在javaRestAPI中,所有类型的query查询条件都是有QueryBuilders来构建的:
全文检索的查询条件构造API如下:
精确查询的查询条件构造API如下:
布尔查询的查询条件构造API如下:
案例
需求:利用javaRestClient实现搜索功能,条件如下:
- 搜索关键字为脱脂牛奶
- 品牌必须为德亚
- 价格必须低于300
@Test
void testSearch() throws IOException {
//1. 创建request对象
SearchRequest request = new SearchRequest("items");
//2. 组织DSL参数
request.source()
.query(QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("name","脱脂牛奶"))
.filter(QueryBuilders.termQuery("brand.keyword","德亚"))
.filter(QueryBuilders.rangeQuery("price")
.lt(130000)));
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
parseResponseResult(response);
}
2.3 排序和分页
与query类似,排序和分页参数都是基于request.source()来设置:
2.4 高亮显示
高亮显示的条件构造API如下:
高亮显示的结果解析API如下:
@Test
void testHighlight() throws IOException {
//1. 创建request对象
SearchRequest request = new SearchRequest("items");
//2. 组织DSL参数
//2.1 query条件
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
//2.2 高亮条件
request.source().highlighter(SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>"));
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
parseHighlightResponseResult(response);
}
private static void parseHighlightResponseResult(SearchResponse response) {
//4. 解析结果
SearchHits searchHits = response.getHits();
//4.1 总条数
long value = searchHits.getTotalHits().value;
System.out.println("value = " + value);
//4.2 命中的数据
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
//4.2.1 获取Source的结果
String json = hit.getSourceAsString();
//4.2.2 处理对象 比如转为ItemDoc
ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
//4.3 处理高亮结果
Map<String, HighlightField> hfs = hit.getHighlightFields();
if(hfs != null && !hfs.isEmpty()){
//4.3.1 根据高亮字段名获取高亮结果
HighlightField hf = hfs.get("name");
//4.3.2 获取高亮结果,覆盖非高亮结果
String hfName = hf.getFragments()[0].string();
doc.setName(hfName);
}
System.out.println("doc = " + doc);
}
}
3 数据聚合
聚合(aggregations)可以实现对文档数据的统计、分析、运算。运算常见的有三类:
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
3.1 DSL聚合
我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。
默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可。例如,我想知道价格高于3000元的手机品牌有哪些:
# 聚合
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category.keyword": "牛奶"
}
},
{
"range": {
"price": {
"gte": 20000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg":{
"terms": {
"field": "brand.keyword",
"size": 10
}
}
}
}
除了对数据分组(Bucket)以外,还可以对每个Bucket内的数据进一步做数据计算和统计。例如:我想知道手机有哪些品牌,每个品牌的价格最小值、最大值、平均值。
# 聚合
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category.keyword": "牛奶"
}
},
{
"range": {
"price": {
"gte": 20000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg":{
"terms": {
"field": "brand.keyword",
"size": 10
},
"aggs": {
"price_stats": {
"stats": {
"field": "price"
}
}
}
}
}
}
3.2 RestClient聚合
@Test
void testAgg() throws IOException {
//1. 创建request对象
SearchRequest request = new SearchRequest("items");
//2. 组织DSL参数
//分页
request.source().size(0);
//聚合条件
String brandAggName = "brandAgg";
request.source().aggregation(AggregationBuilders.terms(brandAggName).field("brand.keyword").size(10));
//3. 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4. 解析结果
Aggregations aggregations = response.getAggregations();
//4.1 根据聚合名称获取对应的聚合
Terms aggregation = aggregations.get(brandAggName);
//4.2 获取Buckets
List<? extends Terms.Bucket> buckets = aggregation.getBuckets();
//4.3 遍历获取每一个bucket
for (Terms.Bucket bucket : buckets) {
System.out.println("brand: " + bucket.getKeyAsString());
System.out.println("count: " + bucket.getDocCount());
}
}