一、前言
继上一节学习了ES的搜索的查询全部和term搜索后,此节将把搜索匹配功能剩余的2个学习完,分别是range搜索和exists搜索
二、range范围搜索
range查询用于范围查询,一般是对数值型和日期型数据的查询。使用range进行范围查询时,用户可以按照需求中是否包含边界数值进行选项设置,可供组合的选项如下:
- gt:大于;
- lt 小于;
- gte 大于等于;
- lte 小于等于;
其请求形式如下:
GET /hotel/_search
{
"query": {
"range": {
"FIELD": { //需要范围查询的列
"gte": "${VALUE1}", //大于等于value1
"lte": "${VALUE2}" //小于等于value2
}
}
}
}
以下是数值类型的查询示例,查询住宿价格在500~600(包含边界值)元的酒店:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": "500",
"lte": "600"
}
}
}
}
ES返回的数据如下:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "001",
"_score" : 1.0,
"_source" : {
"title" : "文雅酒店",
"city" : "北京",
"price" : "558.00",
"create_time" : "2020-03-29 21:00:00",
"amenities" : "浴池,普通停车场/充电停车场",
"full_room" : true,
"location" : {
"lat" : 36.940243,
"lon" : 120.394
},
"praise" : 10
}
},
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "006",
"_score" : 1.0,
"_source" : {
"title" : "京盛集团精选酒店",
"city" : "上海",
"price" : "500.00",
"create_time" : "2022-01-29 22:50:00",
"full_room" : true,
"location" : {
"lat" : 40.918229,
"lon" : 118.422011
},
"praise" : 20
}
}
]
}
}
如果我需要查询大于500元(不包含边界值)的酒店:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gt": "500"
}
}
}
}
注意,使用range查询时,查询值必须符合该字段在mappings中设置的规范。例如,在酒店索引中,price字段是double类型,则range应该使用数值型或者数值类型的字符串形式,不能使用其他形式。以下示例将导致ES返回错误:
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gt": "abc"
}
}
}
}
执行上述DSL后,ES返回信息如下:
{
"error" : {
"root_cause" : [
{
"type" : "query_shard_exception",
"reason" : "failed to create query: For input string: \"abc\"",
"index_uuid" : "az-MqIf9QM6asEIfivIBLQ",
"index" : "hotel"
}
],
"type" : "search_phase_execution_exception", //range查询解析异常
"reason" : "all shards failed",
"phase" : "query",
"grouped" : true,
"failed_shards" : [
{
"shard" : 0,
"index" : "hotel",
"node" : "ER773I31Sx-wJuJwJCh7Ng",
"reason" : {
"type" : "query_shard_exception",
//构建range查询时出现异常
"reason" : "failed to create query: For input string: \"abc\"",
"index_uuid" : "az-MqIf9QM6asEIfivIBLQ",
"index" : "hotel",
"caused_by" : {
//字符串类型不能转换为range查询对应的数值型数据
"type" : "number_format_exception",
"reason" : "For input string: \"abc\""
}
}
}
]
},
"status" : 400
}
和term查询类似,查询日期型的字段时,需要遵循该字段在mappings中定义的格式进行查询。例如create_time使用的格式为"yyyy-MM-dd HH:mm:ss",则range查询应该使用如下方式:
GET /hotel/_search
{
"query": {
"range": {
"create_time": {
"gte": "2021-02-27 22:00:00",
"lte": "2024-02-27 22:00:00"
}
}
}
}
在Java客户端上构建range请求是使用QueryBuilders.rangeQuery()
方法实现的,该方法的参数为字段名称,然后再调用对应的方法即可构建相应的查询范围。可以调用gt()、lt()、gte()、lte()
等方法分别实现大于、小于、大于等于、小于等于等查询范围。在使用时支持链式编程,可以连着使用"."操作符,这样不用拆分语句,也比较容易理解,一下通过range查询完成一个createTime大于等于输入的createTimeStart和小于等于createEnd的一个范围查询
Service层:
public List<Hotel> rangeQuery(HotelDocRequest hotelDocRequest) throws IOException {
//新建搜索请求
String indexName = hotelDocRequest.getIndexName();
if (CharSequenceUtil.isBlank(indexName)) {
throw new SearchException("索引名不能为空");
}
SearchRequest searchRequest = new SearchRequest(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
Date createTimeStart = hotelDocRequest.getCreateTimeStart();
String createTimeStartToSearch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(createTimeStart);
Date createTimeEnd = hotelDocRequest.getCreateTimeEnd();
String createTimeEndToSearch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(createTimeEnd);
searchSourceBuilder.query(QueryBuilders.rangeQuery("create_time").gte(createTimeStartToSearch).lte(createTimeEndToSearch));
searchRequest.source(searchSourceBuilder);
return getQueryResult(searchRequest);
}
Controller层:
@PostMapping("/query/range")
public FoundationResponse<List<Hotel>> rangeQuery(@RequestBody HotelDocRequest hotelDocRequest) {
try {
List<Hotel> hotelList = esQueryService.rangeQuery(hotelDocRequest);
if (CollUtil.isNotEmpty(hotelList)) {
return FoundationResponse.success(hotelList);
} else {
return FoundationResponse.error(100,"no data");
}
} catch (IOException e) {
log.warn("搜索发生异常,原因为:{}", e.getMessage());
return FoundationResponse.error(100, e.getMessage());
} catch (Exception e) {
log.error("服务发生异常,原因为:{}", e.getMessage());
return FoundationResponse.error(100, e.getMessage());
}
}
postman调用该接口:
三、exists查询
在某些场景下,我们希望找到某个字段不为空的文档,则可以用exists搜索。字段不为空的条件有:
- 值存在且不是null
- 值不是空数组
- 值是数组,但不是[null]
为方便测试,给索引hotel增加tag字段,DSL如下:
POST /hotel/_mapping
{
"properties": {
"tag":{
"type": "keyword"
}
}
}
下面向该索引中分别写入3条字段为空的数据。
添加tag字段值为null的文档,DSL如下:
POST /hotel/_create/020
{
"title":"环球酒店",
"tag":null
}
添加tag字段是空数组的文档,DSL如下:
POST /hotel/_create/021
{
"title":"环球酒店2",
"tag":[]
}
添加tag为数组,其中只有一个元素,且该元素为null的文档,DSL如下:
POST /hotel/_create/022
{
"title":"环球酒店3",
"tag":[null]
}
上面3种情况的数据使用exists查询都不命中,查询的DSL如下:
GET /hotel/_search
{
"query": {
"exists": {
"field": "tag"
}
}
}
返回结果如下:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0, //命中的文档个数为0
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ] //命中的文档集合为空
}
}
在java客户端中进行查询时,可以调用QueryBuilders.existsQuery(String name)
方法新建一个exists查询,传递的name参数是目标字段名称。以下是使用Java客户端构建exists查询的示例:
service层:
public List<Hotel> existQuery(HotelDocRequest hotelDocRequest) throws IOException {
//新建搜索请求
String indexName = hotelDocRequest.getIndexName();
String propertiesName = hotelDocRequest.getPropertiesName();
if (CharSequenceUtil.isBlank(indexName)) {
throw new SearchException("索引名不能为空");
}
if (CharSequenceUtil.isBlank(propertiesName)) {
throw new SearchException("想要查询的字段名不能为空");
}
SearchRequest searchRequest = new SearchRequest(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.from(hotelDocRequest.getOffset());
searchSourceBuilder.size(hotelDocRequest.getLimit());
searchSourceBuilder.query(QueryBuilders.existsQuery(propertiesName));
searchRequest.source(searchSourceBuilder);
return getQueryResult(searchRequest);
}
controller层:
@PostMapping("/query/exist")
public FoundationResponse<List<Hotel>> existQuery(@RequestBody HotelDocRequest hotelDocRequest) {
try {
List<Hotel> hotelList = esQueryService.existQuery(hotelDocRequest);
if (CollUtil.isNotEmpty(hotelList)) {
return FoundationResponse.success(hotelList);
} else {
return FoundationResponse.error(100,"no data");
}
} catch (IOException e) {
log.warn("搜索发生异常,原因为:{}", e.getMessage());
return FoundationResponse.error(100, e.getMessage());
} catch (Exception e) {
log.error("服务发生异常,原因为:{}", e.getMessage());
return FoundationResponse.error(100, e.getMessage());
}
}
postman调用即可,比如搜索之前tag字段:
没有搜到,所以报了no data
如果搜索title,则会将title不为null的值全部搜索出来,由于title不为空的比较多,我这边只查前3条: