05-ElasticSearch高级搜索
2、ElasticSearch高级搜索
- Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型如下所示
- ①、查询所有
- 查询出所有数据,一般测试用;例如
match_all
- 如下图所示
- 查询出所有数据,一般测试用;例如
- ②、全文检索(full text)查询
- 利用分词器对用户输入内容分词,然后去倒排索引库中匹配,例如
match_query
multi_match_query
- 利用分词器对用户输入内容分词,然后去倒排索引库中匹配,例如
- ③、精确查询
- 根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型的字段,例如
ids
range
term
- 根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型的字段,例如
- ④、地理(geo)查询
- 根据经纬度查询,例如
geo_distance
geo_bounding_box
- 根据经纬度查询,例如
- ⑤、复合(compound)查询
- 复合查询可以将上述各种查询条件组合起来,合并查询条件,例如
bool
function_score
- ①、查询所有
2.1、全文检索查询
2.1.1、使用场景
- 全文检索查询的基本流程如下所示
- ①、对用户搜索的内容做分词,得到词条
- ②、根据词条去倒排索引库中匹配,得到文档id
- ③、根据文档id找到文档,把所有匹配结果以并集或交集返回给用户
- 比较常用的场景包括
- 商城的输入框搜索
- 百度搜索框搜索
- 因为是拿着词条去匹配,因此参与搜索的字段也必须是可分词的text类型的字段
2.1.2、DSL语句格式
-
常见的全文检索查询包括
match
查询:单字段查询multi_match
查询:多字段查询,任意一个字段符合条件就算符合查询条件
-
match
查询语法如下所示-
GET /indexName/_search { "query": { "match":{ "FIELD": "TEXT" } } }
-
-
match_all
查询语法如下-
GET /indexName/_search { "query": { "multi_match": { "query": "TEXT", "fileds": ["FILED1", "FILED2"] } } }
-
2.1.3、match查询DSL语句示例&&RestAPI示例
①、DSL语句
-
比如要搜索
name
字段中存在如家酒店
,DSL语句如下所示-
GET hotel/_search { "query": { "match": { "name": "如家酒店" } }, "size": 2 # size的意思是只显示n条数据 }
-
-
搜索结果如下所示
-
结果分析
-
因为
name
字段是类型是text
,搜索的时候会对这个字段进行分词 -
如搜索
如家酒店
,那么就会分词称为如家
,酒店
,相当于会搜索三次,并取这三次搜索的并集(ES默认的是并集),所以搜索的命中率才会如此之高- 通俗的来说
- 并集就相当于搜索到
name like %如家%
算一条数据,搜索到酒店
也算一条数据 - 那么交集就跟它相反,必须是
name like %如家酒店%
才能算是一条数据
- 并集就相当于搜索到
- 通俗的来说
-
那么如何取交集呢?,如下所示
-
DSL
-
# 取交集,并集是or GET hotel/_search { "query": { "match": { "name": { "query": "如家酒店", "operator": "and" } } } }
-
-
运行结果
-
-
②、RestAPI
math_all
-
代码如下所示
-
package com.coolman.hotel.test; import com.coolman.hotel.pojo.HotelDoc; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.lucene.search.TotalHits; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; @SpringBootTest public class FullTextSearchDemo { // 注入 RestHighLevelClient对象 @Autowired private RestHighLevelClient restHighLevelClient;
// jackson private final ObjectMapper objectMapper = new ObjectMapper(); /** * 查询所有测试 */ @Test public void testMatchAll() throws IOException { // 1. 创建一个查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 指定索引 // 2. 添加查询的类型 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); searchRequest.source().query(matchAllQueryBuilder); // source就相当于{} searchRequest.source().size(100); // RestAPI默认返回的是10条数据,可以更改size的属性,即可自定义返回的数据量 // 3. 发出查询的请求,得到响应结果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应的结果 handlerResponse(response); } /** * 用来处理响应数据(相当于解析返回的JSON数据) * @param response */ private void handlerResponse(SearchResponse response) throws JsonProcessingException { // 1. 得到命中的数量(即总记录数量) SearchHits hits = response.getHits(); long totalCount = hits.getTotalHits().value;// 总记录数 System.out.println("总记录数量为:" + totalCount); // 2. 获取本次查询出来的列表数据 SearchHit[] hitsArray = hits.getHits(); for (SearchHit hit : hitsArray) { // 得到json字符串 String json = hit.getSourceAsString(); // 将json字符串转换为实体类对象 HotelDoc hotelDoc = objectMapper.readValue(json, HotelDoc.class); System.out.println(hotelDoc); } }
}
~~~
-
match
-
代码如下所示
-
/** * 单字段查询 */ @Test public void testMatch() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查询的类型 MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "如家酒店"); searchRequest.source().query(matchQueryBuilder); // 3. 发出查询请求,得到响应数据 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应的结果 handlerResponse(response); }
-
自行运行查看结果即可
2.1.4、multi_match查询DSL语句示例&&RestAPI示例
DSL语句
-
比如搜索
name
和brand
字段中出现如家酒店
的数据-
DSL语句如下所示
-
GET hotel/_search { "query": { "multi_match": { "query": "如家酒店", "fields": ["name", "brand"] } } }
-
-
运行结果如下所示
-
不过多字段查询的使用很少,因为多字段查询会使得查询效率变慢
-
一般都会在创建映射的时候,使用
copy_to
将指定字段的值拷贝到另一个字段,如自定义的all
字段 -
这样子就可以使用单字段查询,提高查询效率
-
RestAPI
跟单字段查询差不多,只不过使用QueryBuilders创建的对象略有不同罢了
-
代码如下所示
-
/** * 多字段查询 */ @Test public void testMultiMatch() throws IOException { // 1. 创建查询请求球体对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查询的字段 // MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("如家酒店", "name", "brand", "bussiness"); // searchRequest.source().query(multiMatchQueryBuilder); // 因为在创建映射的时候使用了copy_to,索引上面的多字段查询等价于下面的单字段查询 MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("all", "如家酒店"); searchRequest.source().query(matchQueryBuilder); // 3. 执行查询操作,得到响应对象 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应对象 handlerResponse(response); }
-
2.2、精准查询
2.2.1、使用场景
- 精确查询一般是查找
keyword
、数值、日期、boolean
等类型的字段,所以不会对搜索条件分词,常见的有如下term
- 根据词条精确值查询,相当于
equals
、=
- 根据词条精确值查询,相当于
range
- 根据值的范围查询,相当于
>=
、<=
、between
、and
- 根据值的范围查询,相当于
2.2.2、DSL语句格式
①、term
查询
-
因为精确查询的字段搜索的是不分词的字段,因此查询的条件也必须是不分词的词条
-
查询的时候,用户输入的内容跟自动值完全匹配的时候才认为符合条件
-
如果用户输入的内容过多,反而搜索不到数据
-
语法说明
-
# term 精确查询 GET /indexName/_search { "query": { "term": { "FILED": { "value": "VALUE" } } } }
-
-
示例
- 输入精确词条
- 输入非精确词条
- 输入精确词条
②、range
查询
-
范围查询,一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤
-
基本语法
-
# range 精确查询 # gte表示大于等于;gt表示大于 # lte表示小于等于;lt表示小于 GET /indexName/_search { "query": { "range": { "FIELD": { "gte": 10, "lte": 20 } } } }
-
-
示例
- 查询
price
大于等于200,小于等于500的酒店
- 查询
2.2.3、RestAPI
-
term
查询-
代码如下所示
-
/** * term 精确查询 */ @Test public void testTermQuery() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查询的字段 TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brand", "如家"); searchRequest.source().query(termQueryBuilder); // 3. 发出查询的请求,获取响应结果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应的结果 handlerResponse(response); }
-
-
range
查询-
代码如下所示
-
/** * range 精确查询 */ @Test public void testRangeQuery() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查询的字段 RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price"); rangeQuery.gte(300); // 大于等于300 rangeQuery.lte(500); // 小于等于500 searchRequest.source().query(rangeQuery); // 3. 执行查询操作,获取响应结果 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应结果 handlerResponse(response); }
-
2.3、地理坐标查询
2.3.1、使用场景
- 所谓的地理坐标查询,其实就是根据经纬度查询
- 官方文档
- https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
- 常见的使用场景如下所示
- 携程:搜索附近的酒店
- 滴滴:搜索附近的出租车
- 微信:搜索附近的人
2.3.2、DSL语句格式
①、矩形范围查询
-
矩形范围查询,也就是
geo_bounding_box
查询,查询坐标落在某个矩形范围的所有文档 -
查询的时候,需要指定矩形的左上、右下两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点,如下所示
-
语法如下所示
-
# 地理位置查询(矩形查询) GET hotel/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 31.1, "lon": 121.5 }, "bottom_right": { "lat": 30.9, "lon": 121.7 } } } } }
-
-
示例
②、附近查询
-
附近查询,也叫做距离查询(geo_distance)
- 查询到指定中心小于某个距离值的所有文档
-
换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件,如下所示
-
语法如下所示
-
GET hotel/_search { "query": { "geo_distance": { "distance": "15km", "location": "31.21,121.5" } } }
-
-
示例
2.3.3、RestAPI
①、矩形范围查询
-
代码如下所示
-
/** * 地理坐标矩形查询 */ @Test public void testGeoBoundingBoxSearch() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查询的字段 // 指定要查询的字段为 location GeoBoundingBoxQueryBuilder geoBoundingBoxQueryBuilder = QueryBuilders.geoBoundingBoxQuery("location"); // 指定 topLeft的坐标 geoBoundingBoxQueryBuilder.topLeft().resetLat(31.1); geoBoundingBoxQueryBuilder.topLeft().resetLon(121.5); // 指定 bottom_right的坐标 geoBoundingBoxQueryBuilder.bottomRight().resetLat(30.9); geoBoundingBoxQueryBuilder.bottomRight().resetLon(121.7); searchRequest.source().query(geoBoundingBoxQueryBuilder); // 3. 发起请求 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理返回的数据 handlerResponse(response); }
-
②、附近查询
-
代码如下所示
-
/** * 地理坐标附近查询(圆形) */ @Test public void testGeoDistanceSearch() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查询的字段 // 指定要查询的字段是 location GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery("location"); // 指定中心点坐标 geoDistanceQueryBuilder.point(new GeoPoint(31.21, 121.5)); // 指定要查询的范围距离 geoDistanceQueryBuilder.distance("15km"); searchRequest.source().query(geoDistanceQueryBuilder); // 3. 发起查询请求 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理返回的数据 handlerResponse(response); }
-
2.4、复合查询之布尔查询
2.4.1、使用场景
- 布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询,子查询的组合方式有如下几种
- ①、
must
:必须匹配每个子查询,类似”与”(and),must
的条件参与算分 - ②、
should
:选择性匹配子查询,类似”或”(or) - ③、
must_not
:必须不匹配,不参与算分,类似”非”(not) - ④、
filter
:效果和must
一样,都是and。必须匹配,filter的条件不参与算分
- ①、
- 常见的应用场景
- 比如在搜索酒店的时候,除了关键字搜索以外,我们还可能根据品牌、价格、城市等字段过滤
- 每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须使用bool查询
- 比如在搜索酒店的时候,除了关键字搜索以外,我们还可能根据品牌、价格、城市等字段过滤
- 注意事项
- 不过需要注意的是,搜索的时候,参与算分的字段越多,查询的性能也越差;因此这种多条件查询的时候,可以按照如下类似方法解决
- 搜索框的关键字搜索,是全文检索查询,使用
must
查询,参与算分 - 其他过滤条件,采用
filter
查询,不参与算分
- 搜索框的关键字搜索,是全文检索查询,使用
- 不过需要注意的是,搜索的时候,参与算分的字段越多,查询的性能也越差;因此这种多条件查询的时候,可以按照如下类似方法解决
2.4.2、DSL语句格式
-
DSL语句如下所示
-
GET hotel/_search { "query": { "bool": { "must": [ { "term": { "city": { "value": "上海" } } } ], "should": [ { "term": { "brand": { "value": "皇冠假日" } } }, { "term": { "brand": { "value": "华美达" } } } ], "must_not": [ { "range": { "price": { "lte": 500 } } } ], "filter": { "range": { "score": { "gte": 45 } } } } } }
-
这个DSL语句的意思通俗来说就是
- ①、城市必须是上海
- ②、品牌可以是皇冠假日或者华美达
- ③、价格必须小于等于500
- ④、得分必须大于等于45
-
-
示例
-
需求如下所示
- 搜索名字包含”如家酒店”,价格不高于400,在坐标31.21,121.5,周围10km范围的酒店
-
分析
- ①、名称搜索,属于全文检索查询,应该参与算分
- ②、价格不高于400,用
range
过滤查询,不参与算分(可以放到must_not
中,当然也可以放到filter
中,使用lte
表示小于等于400) - ③、周围10km范围内,用
geo_distance
查询,属于过滤条件,不参与算分,放到filter
中
-
DSL语句如下所示
-
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 } } } } } }
-
PS:在kibana中编写
filter
中的坐标信息的时候自动补全有些bug,kibana会报错distacne_unit
和location
不能共存,所以应该把这个单位删除,然后在distance
字段上添加双引号的同时带上单位
-
-
2.4.3、RestAPI
-
代码如下所示
-
/** * 复合查询之布尔查询 */ @Test public void testBooleanQuery() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加要查询的字段 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // must MatchQueryBuilder brand = QueryBuilders.matchQuery("name", "如家酒店"); boolQueryBuilder.must(brand); // must_not RangeQueryBuilder price = QueryBuilders.rangeQuery("price").gt(400); boolQueryBuilder.mustNot(price); // filter GeoDistanceQueryBuilder location = QueryBuilders.geoDistanceQuery("location").point(new GeoPoint(31.21, 121.5)).distance("10km"); boolQueryBuilder.filter(location); searchRequest.source().query(boolQueryBuilder); // 3. 执行查询,得到响应数据 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应数据 handlerResponse(response); }
-
2.5、复合查询之算分函数查询
2.5.1、使用场景
- 当我们使用
match
查询的时候,文档结果会根据搜索词条的关联度打分(_score
),返回结果时按照分值降序排序- 可以自行查询查看验证
- 在Elasticsearch中,早期使用的打分算法是TF-IDF算法,公式如下
- TF(词条频率):描述某一词在一篇文档中出现的频繁程度。出现越多,分值越高,反之,分值月底
- IDF(逆文档频率):通过公式可以看到,词条出现的文档数量越多,分值越低,反之越高
- 在后来的5.1版本升级后,Elasticsearch将算法改进为BM25算法,公式如下
- TF-IDF算法有一个缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算法有一个上限,曲线更加平滑
- 根据相关度打分是比较合理的需求,但合理的不一定是产品经理需要的
- 以某度为例,在搜索的结果中,并不是相关度越高,排名越靠前;而是谁掏的钱多,排名就越靠前
- 要想人为控制相关性算分,就需要利用Elasticsearch中的function score查询
2.5.2、DSL语句格式
- 可以通过下图来理解算分函数查询的DSL语句基本格式
- function score 查询中包含四部分内容
- ①、原始查询条件
- query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
- ②、过滤条件
- filter部分,符合该条件的文档才会重新算分
- ③、算分函数
- 符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
weight
:函数结果是常量field_value_factor
:以文档中的某个字段值作为函数结果random_score
:以随机数作为函数结果script_score
:自定义算分函数算法
- 符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
- ④、运算模式
- 算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括
multiply
:相乘replace
:用function score替换query score- 其它,例如:
sum
、avg
、max
、min
- 算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括
- ①、原始查询条件
- function score 的运行流程如下所示
- a. 根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
- b. 根据过滤条件,过滤文档
- c. 符合过滤条件的文档,基于算分函数的运算,得到函数算分(function score)
- d. 将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分
- 因此,其中的关键点是
- 过滤条件:决定哪些文档的算分被修改
- 算分函数:决定函数算分的算法
- 运算模式:决定最终算分结果
需求
-
让”如家”这个品牌的酒店排名靠前一点
-
这个需求很简单,可以理解为如下几部分
- ①、原始条件:不确定,可以任意变化
- ②、过滤条件:
brand = "如家"
- ③、算分函数:可以简单粗暴,直接使用
weight
给固定的算分结果 - ④、运算模式:比如求和
-
因此DSL语句如下所示
-
GET hotel/_search { "query": { "function_score": { "query": { "match": { "name": "酒店" } }, "functions": [ { "filter": { "term": { "brand": "如家" } }, "weight": 10 } ], "boost_mode": "sum" } } }
-
-
结果如下所示
-
原始搜索结果如下所示
2.5.3、RestAPI
-
代码如下所示,可以对照着DSL语句进行编写
-
/** * 复合查询之算分函数查询 */ @Test public void testFunctionScoreQuery() throws IOException { // 1. 创建查询请求对象 SearchRequest searchRequest = new SearchRequest("hotel"); // 2. 添加查询的请求体 searchRequest.source().query( // query QueryBuilders.functionScoreQuery( // function_score QueryBuilders.matchQuery("name", "酒店"), // match new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // functions new FunctionScoreQueryBuilder.FilterFunctionBuilder( // filter QueryBuilders.termQuery("brand", "如家"), // term ScoreFunctionBuilders.weightFactorFunction(10) // weight ) } ).boostMode(CombineFunction.SUM) // boost_mode ); // 3. 执行查询,获取响应数据 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 4. 处理响应数据 handlerResponse(response); }
-