淘先锋技术网

首页 1 2 3 4 5 6 7

一、前言

继上一节学习了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条:
在这里插入图片描述