此为笔者学习ES 的第二篇总结,其他基础相关内容可自行查询:ElasticSearch(一)
前言
本篇内容为学习ElasticSearch 的课后总结(二),本文大致包括以下内容:
- DSL 查询
- 查询结果常见的处理方式
- Rest Client 的是使用总结
整个过程记录详细,每个步骤亲历亲为,实测可用。
延续前一篇文章的内容,我们已经完成了ES的安装搭建、ES对索引库和文档的CRUD。在下面一小节我们将学习DSL查询语法的基本使用。
一、DSL查询
ES提供了基于JSON的DSL(Domain Specific Language) 来定义查询。常见的查询类型有:
-
查询所有: 查询出所有数据。
match_all
-
全文检索: 也就是全部文档进行指定关键字查询,利用分词器对关键字分词,然后再去倒排索引库中匹配。
match_query
:查询某个单一字段的值。multi_match_query
:查询多个字段的值。
-
精确查询: 不对关键字进行分词,根据精确词条巡查找数据,一般用于查询keyword等。
ids
:查询某个值在指定数组中。range
:查询在指定范围。term
:精确查询某个字段的值。
-
地理(geo)查询: 根据经纬度查询。
geo_distance
:查询指定点 附近的 指定范围内的数据。geo_bounding_box
:查询某个坐标在这个坐标数组中的数据。
-
复合查询; 复合查询可以将上述的各种查询条件组合起来,合并一个多重查询。
bool
:组合各种条件的查询。function_score
:对于查询的结果重新算分。
-
查询的基本语法:
GET /索引库名/_search { "query": { "查询类型" :{ "查询字段" : "具体值" } } }
-
查询全部
对应的DSL语句如下所示:
# 查询所有(match_all):match_all 中不需要跟其他条件 GET /hotel/_search { "query": { "match_all": {} } }
对应的Java代码如下所示:
@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); // 解析结果 handlerResponse(response); }
-
全文检索
3.1 match 查询
对应的DSL语句如下所示:
# 全文检索查询。对用户输入的内容分词,常用于搜索框搜索(match) # 这里的查询字段为all 时指将多个要查询的字段关联在all 上,避免了多重检索。相当于下面multi_match 中的设置多个字段。 GET /hotel/_search { "query": { "match": { "all": "外滩如家" } } }
对应的Java代码如下所示:
@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); // 解析结果 handlerResponse(response); }
3.2 multi_match 查询
对应的DSL语句如下所示:
# 在match 的基础上,multi_match 是对多个字段进行查询 GET /hotel/_search { "query": { "multi_match": { "query": "外滩如家", "fields": ["brand","name","business"] } } }
对应的Java代码如下所示:
@Test void testMultiMatch() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source() .query(QueryBuilders.multiMatchQuery("外滩如家","brand","name","business")); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
-
精确查询
4.1 term 查询: 按照词条精确值查询。对应的DSL语句如下所示:
# 精确查询,不对搜索内容进行分词,查询必须完全匹配! # (term 查询) GET /hotel/_search { "query": { "term": { "city": { "value": "上海" } } } }
对应的Java代码如下所示:
@Test void testTerm() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source() .query(QueryBuilders.termQuery("city","上海")); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
4.2 range 查询: 根据值的范围查询。
对应的DSL语句如下所示:
# (range 查询) GET /hotel/_search { "query": { "range": { "price": { "gte": 1000, "lte": 2000 } } } }
对应的Java代码如下所示:
@Test void testRange() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source() .query(QueryBuilders.rangeQuery("price").gte(1000).lte(2000)); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
-
地理查询
这里只介绍稍微常用的geo_distance
:查询到指定中心点小于某个距离值的所有文档。对应的DSL语句如下所示:
# 地理查询(distance 查询) GET /hotel/_search { "query": { "geo_distance":{ "distance":"5km", "location":{ "lat": 31.21, "lon": 121.5 } } } }
对应的Java代码如下所示:
@Test void testGeoDistanceQuery() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source() .query(QueryBuilders.geoDistanceQuery("location") .point(new GeoPoint(31.21,121.5)) .distance(5, DistanceUnit.KILOMETERS)); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
-
复合查询
复合(Compound)查询可以将其他简单查询组合起来,实现更复杂的搜索逻辑。常用的复合查询有:function_score 查询
和bool 查询
。6.1 相关性算分
a) 算分函数查询。 我们在多文档进行全文检索时,文档结果会根据搜索词条的关联度打分,在正常情况下,返回结果按照分值降序排列。
b) 常见的打分机制有三种:
-
TF
-
TF-IDF:TF-IDF 在TF 的基础上,添加了IDF的概念。 IDF可以有效的避免每个文档都符合条件依然算分导致的最终分数差距小的问题。
-
BM25算法:blabla 看不懂。
c) TF-IDF 与 BM25 的区别
- TF-IDF:在ES5.0 之前使用,分值会随着词频增加而越来越大。
- BM25:在ES5.0 后使用,分值也会随着词频增加而增大,但增长曲线会趋于水平。
6.2 function_score
使用function_score 可在相关性算分的基础上,找到符合条件的文档进行分数调整。 例如:某度经常会出现,搜索某一词条,前面的几条记录几乎全是广告。基本语法规则如下所示:
function score query 定义的三要素:- 过滤条件:对符合条件的文档进行操作。
- 算分函数:如何计算 function_score。
- 加权方式:function_score如何与原始的 query sore 运算。
例如: 给“如家” 这个品牌的酒店排名靠前一些:
对应的DSL语句如下所示:
# function_score 查询 GET /hotel/_search { "query": { "function_score": { "query": { "match": { "all": "外滩" } }, "functions": [ { "filter": { "term": { "brand": "如家" } }, "weight": 10 } ], "boost_mode": "sum" } } }
对应的Java代码如下所示:
@Test void testFunctionScore() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source().query(QueryBuilders.functionScoreQuery( QueryBuilders.matchQuery("all","外滩"), new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery("brand","如家"), ScoreFunctionBuilders.weightFactorFunction(10) ) } ).boostMode(CombineFunction.SUM) ); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
6.3 Boolean Query
布尔查询是一个多多个查询子句的组合。子查询的组合方式有must
:必须匹配某个子查询,类似 “与”,参与算分;- ‘should’:选择性匹配子查询,类似 “或”,参与算分;
must_not
:必须不匹配,类似 “非”,不参与算分;filter
:必须匹配,不参与算分。
例如: 查询名称为 “如家”,价格低于400,且在
(121.5,31.21)
十公里范围内的酒店信息。对应的DSL语句如下所示:
# boolean_query 查询 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 } } } ] } } }
对应的Java代码如下所示:
@Test void testBoolean() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.termQuery("name","如家")); boolQuery.mustNot(QueryBuilders.rangeQuery("price").gte(400)); boolQuery.filter(QueryBuilders.geoDistanceQuery("location") .point(new GeoPoint(31.21,121.5)) .distance(10,DistanceUnit.KILOMETERS)); request.source().query(boolQuery); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
-
上一小节,我们学习使用了ES 中常用的几种查询方式,接下来,我们将对查询出来的结果进行各种处理。
二、查询结果处理
-
排序
ES 支持对查询记过进行排序,默认根据相关度算分(_score)来排序。当指定排序规则后,则按照用户定义的排序规则排序。可以排序的字段类型有:keyword、数值、日期等。基本语法:
# 排序 # 按照用户的评分降序排序,再按酒店的价格升序排序 GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "score": { "order": "desc" } }, { "price": "asc" } ] } # 按照指定坐标的距离进行升序排序 GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "_geo_distance": { "location": { "lat": 31, "lon": 121 }, "order": "asc", "unit": "km" } } ] }
-
分页
ES默认只返回查询结果文档中的前十条数据。如果需要更多的数据,则需要修改分页参数。ES 中通过修改 from、size 参数来控制要返回的分页结果(与MySql 的分页查询类似):
# 分页查询 GET /hotel/_search { "query": { "match_all": {} }, "sort": [ { "price": { "order": "asc" } } ], "from": 990, "size": 10 }
Java 代码样例如下:
@Test void testPageAndSort() throws IOException { // 设置页码和每页大小 int page = 2,pageSize = 5; // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL request.source().query(QueryBuilders.matchAllQuery()); request.source().sort("price", SortOrder.ASC); request.source().from((page-1) * pageSize).size(pageSize); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
初看ES 的分页与MySql 的分页机制一摸一样,但ES 的底层原理与MySql 分页的原理截然不同。
由于ES 无法锁定from 参数的位置,故ES只能先读取
from + size
数量的文档到内存中,然后再进行分隔返回结果。此时,如果ES是单机运行,那么这么看好像不会存在什么问题。但往往我们为了系统的健壮性,常常搭建了ES集群,此时,ES更无法确定前1000条数据来自哪个分片。
我们的ES 就会含辛茹苦的从每个分区读取 1000 条数据到内存中,然后再经过排序后,截取选择指定范围的文档数据。例如,我们共搭建了10台ES 集群,此时,ES 就会读取10000 条数据到内存中。如果我们的集群数量增加,搜索页数过深,就会出现 深度分页 的问题,导致结果集非常大,对内存和CPU 的消耗就会急剧增加。
因此,ES 设定的结果集查询的上限是10000。当我们的
from + size
大于 10000 时,ES 就会直接报错。其实,在我们的业务逻辑中,往往也限定了访问的页数。例如:百度的访问最大页码大概在70几。此外。还存在另外两种思想的分页查询:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页的数据。
- scroll:将排序后的数据形成快照,保存在内存中。
我们将三者进行对比分析:
from + size after search scroll 优点 支持随机分页 没有查询上限(单次查询的size 不超过10000) 没有查询上限(单次查询的size 不超过10000) 缺点 存在深度分页问题,默认查询上限(from + size)为 10000 只能向后逐页查询。不支持随机翻页 有额外的内存消耗,并且搜索结果非实时 -
高亮
简单来说,就是在搜索结果中把搜索关键字突出显示。原理:
- 将搜索结果中的关键字用指定标签进行包裹。
- 在页面中给包裹标签添加css 样式。
语法如下:
例如:给“如家” 相关酒店的品牌名称进行高亮显示。默认情况下,ES的搜索字段必须与高亮字段一致才能高亮。可通过设置"require_field_match" 字段修改。
对应的DSL语句如下所示:
# 高亮 GET /hotel/_search { "query": { "match": { "all": "如家" } }, "highlight": { "fields": { "brand": { "pre_tags": "<em>", "post_tags": "</em>", "require_field_match": "false" } } } }
对应的Java 代码如下所示:
@Test void testHighlighterSort() throws IOException { // 1. 准备 request SearchRequest request = new SearchRequest("hotel"); // 2. 准备DSL // 2.1 query request.source().query(QueryBuilders.matchQuery("all","如家")); // 2.2 highlighter request.source().highlighter(new HighlightBuilder() .field("brand") .requireFieldMatch(false) .preTags("<em>") .postTags("</em>") ); // 3. 发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 解析结果 handlerResponse(response); }
在上一小节的内容中,我们学习了ES 对搜索结果的处理。在下一小节,我们将具体总结Rest Client 中查询以及对查询结果处理的相关步骤
三、Rest Client 使用总结
-
查询的基本步骤是:
a) 准备SearchRequest 对象;b) 准备DSL: Request.source()
① QueryBuilders 构建查询条件
② 传入Request.source() 的 query() 方法c) 发送请求,得到结果
d) 解析结果
-
结果解析样例代码
通常结果解析就是参考JSON结果,从外到内,逐层分析) 。private void handlerResponse(SearchResponse response) { // 4. 解析结果 SearchHits searchHits = response.getHits(); // 4.1 获取总条数 long total = searchHits.getTotalHits().value; // 4.2 获取所有的结果 SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { String json = hit.getSourceAsString(); // 反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); // 4.3 获取高亮结果 Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if(!CollectionUtils.isEmpty(highlightFields)){ HighlightField highlightField = highlightFields.get("name"); if(highlightField != null){ String name = highlightField.getFragments()[0].toString(); hotelDoc.setName(name); } System.out.println("hotelDoc: " + hotelDoc); } } System.out.println(response); }
以上就为本篇文章的全部内容啦!
如果对你有帮助的话,请多多点赞支持一下呗!