一,GEO数据结构
GEO就是Geolocation的简写,代表地理坐标。Redis在3.2版本加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来搜索数据
1.1 GeoAdd
GeoAdd:添加一个地理空间信息,包含:经度(longitude),纬度(latitude),值(member)
-
语法
GEOADD key [NX|XX] [CH] longitude latitude member [ longitude latitude member...] -
key:存储地理空间数据的键。 -
longitude:地理位置的经度。 -
latitude:地理位置的纬度。 -
member:地理位置的成员名称。 -
[NX|XX]-
NX(Not Exists):只在成员不存在时才添加。如果指定了NX,只有当member不存在时,longitude和latitude才会被添加到key中。如果member已经存在,此操作不会做任何修改。 -
XX(Exists):只在成员已经存在时才添加。如果指定了XX,只有当member已经存在时,longitude和latitude才会被更新。如果member不存在,此操作不会做任何修改。 -
这两个选项是互斥的,即你不能同时指定
NX和XX。这些选项用于控制数据的添加策略,确保数据一致性或避免不必要的操作。
-
-
[CH]CH(Changed):返回添加或更新的成员数量。默认情况下,GEOADD命令只返回添加的成员数量。如果你指定了CH,Redis 将返回所有被修改的成员数量(包括添加和更新的成员)。
举例:
# 只在 "Palermo" 不存在时才添加
GEOADD cities NX 13.361389 38.115556 "Palermo"
# 只在 "Catania" 已经存在时才更新
GEOADD cities XX 15.087269 37.502669 "Catania"
# 返回添加或更新的成员数量
GEOADD cities CH 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
1.2 GeoDist
GeoDist:计算指定的两个点之间的距离并返回
-
语法:
GeoDist key member1 member2 [unit]key:存储地理空间数据的键。member1:第一个成员。member2:第二个成员。unit(可选):距离的单位,可以是以下几种:m:米(默认)km:千米ft:英尺mi:英里
举例:
#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
GEOADD cities 12.496366 41.902783 "Rome"
# 查看两个成员之间的距离
GEODIST cities "Palermo" "Catania"
1.3 GeoHash
GeoHash:将指定member的坐标转化为hash字符串并返回
-
语法
GeoHash key member [member ...]key:存储地理空间数据的键。member:成员。
举例
#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
# 查看member的坐标转化成的hash字符串
GeoHash cities "Palermo" "Catania"
1.4 GeoPos
GeoPos:返回指定member的坐标
-
语法
GeoPos key member [member ...]key:存储地理空间数据的键。member:成员。
举例
#添加信息
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
# 查看两个member的坐标
GeoPos cities "Palermo" "Catania"
1.5 GeoRadius
GeoRadius:指定圆心,半径,找到该园内所有member,并按照与圆心之间的距离排序后返回(6.2之后已废弃)
-
语法
GeoRadius key longitude latitude radius unit [WithCoord] [WithDist] [WithHash] [Count count [Any] ] [ASC|DSC] [Store key] [StoreDist key]key:存储地理空间数据的键。longitude:中心点的经度。latitude:中心点的纬度。radius:查询的半径大小。unit:距离的单位,可以是以下几种:m:米km:千米ft:英尺mi:英里
WithCoord:返回成员的经纬度。WithDist:返回成员到中心点的距离。WithHash:返回成员的 Geohash 编码。Countcount [ANY]:限制返回的结果数量。ANY表示可以返回任意数量的结果,用于提高查询效率。ASC|DESC:结果按距离的升序或降序排序。Storekey:将结果成员的名字存储到指定的键。StoreDistkey:将结果成员的名字和距离存储到指定的键。
举例
GEOADD cities 13.361389 38.115556 "Palermo"
GEOADD cities 15.087269 37.502669 "Catania"
GEOADD cities 12.496366 41.902783 "Rome"
# 查询以指定经纬度为中心,半径为100公里的成员,并返回成员的经纬度和距离
GEORADIUS cities 15.087269 37.502669 100 km WITHCOORD WITHDIST
# 查询以指定经纬度为中心,半径为100公里的成员,按距离升序排序
GEORADIUS cities 15.087269 37.502669 100 km WITHCOORD WITHDIST ASC
# 查询以指定经纬度为中心,半径为100公里的成员,并将结果存储到指定的键
GEORADIUS cities 15.087269 37.502669 100 km STORE nearby_cities
# 查询以指定经纬度为中心,半径为100公里的成员,并将结果和距离存储到指定的键
GEORADIUS cities 15.087269 37.502669 100 km STOREDIST distances
1.6 GeoSearch
GeoSearch:在指定搜索范围内搜索member,并按照与指定点之间的距离排序后返回,范围可以是圆形,矩形。(6.2新功能)
-
语法
GeoSearch [FromMember member] [FromLonlat longitude latitude] [ByRadius radius unit] [ByBox with height unit] [ASC|DESC] [Count count[Any] ] [WithCoord] [WithDist] [WithHash]-
key:存储地理空间数据的键 -
中心点选项(必选一个)
FROMMEMBER member:以指定的成员为中心。FROMLONLAT longitude latitude:以指定的经纬度为中心。
-
搜索范围选项(必选一个)
-
BYRADIUS radius unit:指定搜索半径和单位。-
radius:搜索半径。 -
unit:距离单位,可以是m(米)、km(千米)、ft(英尺)、mi(英里)。
-
-
BYBOX width height unit:指定搜索矩形的宽度、高度和单位。width:矩形的宽度。height:矩形的高度。unit:距离单位,可以是m(米)、km(千米)、ft(英尺)、mi(英里)。
-
-
ASC:结果按距离升序排序。 -
DESC:结果按距离降序排序。 -
COUNT count [ANY]:限制返回结果的数量。ANY表示可以返回任意数量的结果,以提高查询效率。 -
WITHCOORD:返回成员的经纬度。 -
WITHDIST:返回成员到中心点的距离。 -
WITHHASH:返回成员的 Geohash 编码。
-
举例
# 使用成员作为中心点,按半径进行搜索
GEOSEARCH cities FROMMEMBER "Catania" BYRADIUS 100 km WITHCOORD WITHDIST
# 使用经纬度作为中心点,按半径进行搜索
GEOSEARCH cities FROMLONLAT 15.087269 37.502669 BYRADIUS 100 km WITHCOORD WITHDIST
# 使用成员作为中心点,按矩形边界进行搜索
GEOSEARCH cities FROMMEMBER "Catania" BYBOX 200 200 km WITHCOORD WITHDIST
1.7 GeoSearchStore
GeoSearchStore:与GeoSearch功能一样,不过可以把结果存储到一个指定的key(6.2新功能)
-
语法
GeoSearchStore destination source [FromMember member] [FromLonlat longitude latitude] [ByRadius radius unit] [ByBox with height unit] [ASC|DESC] [Count count[Any] ] [WithCoord] [WithDist] [WithHash]destination:存储搜索结果的键。source:存储地理空间数据的键。
举例
# 使用成员作为中心点,按半径进行搜索并存储结果
GEOSEARCHSTORE nearby_cities cities FROMMEMBER "Catania" BYRADIUS 100 km
# 使用经纬度作为中心点,按半径进行搜索并存储结果
GEOSEARCHSTORE nearby_cities cities FROMLONLAT 15.087269 37.502669 BYRADIUS 100 km
二,附件商户搜索
2.1 导入店铺经纬度数据到GEO
void loadShopData(){
//1.查询店铺信息
List<Shop> list=shopService.list();
//2.把店铺信息按照type_id分组
Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupBy(Shop::getTypeId));
//3.分批完成写入Redis
for(Map.Entry<Long,List<Shop>> entry : map.entrySet()){
//获取类型id
Long typeId=entry.getKey();
String key="shop:geo:"+typeId;
//获取同类型店铺集合
List<Shop> value=entry.getValue();
//写入redis GeoAdd 经度 纬度 Member(这种写法很低效,只是写出来了解)
for(Shop shop: value){
stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY(),shop.getId().toString() ));
}
//一次性写入(将Shop转化为Localtion)
List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();
for(Shop shop:value){
RedisGeoCommands.GeoLocation<String> location = new RedisGeoCommands.GeoLocation<>();
location.setName(shop.getId().toString());
location.setPoint(new Point(shop.getX(),shop.getY()) );
locations.add(location);
}
stringRedisTemplate.opsForGeo().add(key,locations);
}
}
2.2 搜索附近商户
控制层接口
@GetMapping("/of/type")
public Result queryShopByType(
@RequestParam(value = "typeId") Integer typeId,
@RequestParam(value = "current",defaultValue = "1") Integer current,
@RequestParam(value = "x",required = false) Integer x,
@RequestParam(value = "y",required = false) Integer y){
return shopService.queryShopByType(typeId,current,x,y);
}
Service层实现
public Result queryShopByType(Integer typeId,Integer current,Double x,Double y){
//1.判断是否需要根据坐标查询
if(x ==null || y==null){
//不需要按照坐标查询,按照数据库查询
Page<Shop> page=query()
.eq("type_id",typeId)
.page(new Page<>(current,10));
return Result.ok(page.getRecords());
}
//2.计算分页参数
int from=(current-1)*10;
int end=current*10;
//3.查询Redis,按照距离排序,分页 结果:shopId,distance
//GeoSearch key ByLonLat x y ByRadius 5000 WithDistance Count end
String key="shop:geo:"+typeId;
GeoResults<RedisGeoCommands.GeoLocations<String> > results= stringRedisTemplage.opsForGeo()
.search(
key,
GeoReference.fromCoordinate(x,y),
new Distance(5000),
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance()
.limit(end)//代表从第一条开始到end条,需要我们手动截取
);
//4.解析出id
if(results == null){
return Result.ok(Collections.emptyList());
}
List<RedisGeoCommands.GeoLocations<String>> list=results.getContent();
if(list.size() <= from){
//没有下一页,结束
return Result.ok(Collections.emptyList());
}
//截取from~end的部分
List<Long> ids=new ArrayList<>();
Map<String,Distance> distanceMap=new HashMap<>();
list.skip(from).forEach(result ->{
//获取店铺id
String shopIdStr=result.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
//获取距离
Distance distance= result.getDistance();
distanceMap.put(shopIdStr,distance);
});
//5.根据id查询出店铺信息
String idStr=StrUtil.join(",",ids);
List<Shop> shop = query().in("id",ids).last("ORDER BY FIELD(id,+"idStr"+)").list();
for(Shop shop: shops){
Distance dis = distanceMap.get(shopId().toString()).getValue();
shop.setDistance(dis);
}
//6.返回
return Result.ok(shops);
}