黑马点评项目-附近商铺

一、GEO 数据结构的基本用法

1.1 GEO 数据结构

GEO 就是 Geolocation 的简写形式,代表地理坐标。Redis 在 3.2 版本中加入了对 GEO 的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:
GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
GEODIST:计算指定的两个点之间的距离并返回
GEOHASH:将指定 member 的坐标转为 hash 字符串形式并返回
GEOPOS:返回指定 member 的坐标
GEORADIUS:指定圆心、半径,找到该圆内包含的所有 member,并按照与圆心之间的距离排序后返回。6.2 以后已废弃
GEOSEARCH:在指定范围内搜索 member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2 新功能
GEOSEARCHSTORE:与 GEOSEARCH 功能一致,不过可以把结果存储到一个指定的 key。6.2 新功能

1.2 练习 Redis 的 GEO 功能

需求:
1、添加下面几条数据:

  • 北京南站(116.378248 39.865275)
  • 北京站(116.42803 39.903738)
  • 北京西站(116.322287 39.893729)

2、计算北京西站到北京站的距离
3、搜索天安门(116.397904 39.909005)附近 10km 内的所有火车站,并按照距离升序排序

GEOADD 命令
在这里插入图片描述
添加北京南站、北京站、北京西站的坐标数据
在这里插入图片描述
使用 RPM 查看存入的数据
在这里插入图片描述
可以看出 GEO 底层是基于 SortedSet 实现的,Redis 将地理坐标转换成了 score 值存入到了 SortedSet 中。

GEODIST 命令
在这里插入图片描述
计算北京西站到北京站的距离
在这里插入图片描述
GEORADIUS 命令(已废弃)
在这里插入图片描述
GEOSEARCH 命令
在这里插入图片描述
返回使用 GEOADD 向 SortedSet 中添加的 member,这些 member 位于给定形状指定的区域的边界内。该命令对GEORADIUS命令进行了扩展,除了支持在圆形区域内搜索外,它还支持在矩形区域内搜索。

应使用此命令代替已弃用的GEORADIUS和GEORADIUSBYMEMBER命令。

查询的中心点由以下强制选项之一提供:

FROMMEMBER: 使用给定的且存在于SortedSet中的 member 的位置。
FROMLONLAT:使用给定的 longitude 和 latitude 位置。

查询的形状由以下强制选项之一提供:
BYRADIUS: 类似GEORADIUS,根据给定的圆形区域内搜索 radius。
BYBOX:在轴对齐的矩形内搜索,由 height 和 width 确定。

该命令可以选择使用以下选项返回附加信息:
WITHDIST: 返回匹配项到指定中心点的距离。返回的距离单位与半径或高度和宽度参数指定的单位相同。
WITHCOORD: 返回匹配项的经度和纬度。
WITHHASH:以 52 位无符号整数的形式返回项目的原始 geohash 编码排序集分数。

默认情况下,匹配项未排序返回。要对它们进行排序,请使用以下两个选项之一:
ASC:相对于中心点,从最近到最远对返回的项目进行排序。
DESC:相对于中心点,从最远到最近对返回的项目进行排序。

默认返回所有匹配项。如果想要将结果限制为前 N 个匹配项,可以使用COUNT选项。使用ANY选项时,只要找到足够的匹配项,该命令就会返回。

搜索天安门(116.397904 39.909005)附近 10km 内的所有火车站,并按照距离升序排序
在这里插入图片描述
GEOPOS 和 GEOHASH 命令:
在这里插入图片描述

二、导入店铺数据到 GEO

在这里插入图片描述
按照商户类型做分组,类型相同的商户作为同一组,以 typeId 作为 key 存入同一个 GEO 集合中。
在这里插入图片描述
HmDianPingApplicationTests

@SpringBootTest
class HmDianPingApplicationTests {

    @Autowired
    private ShopServiceImpl shopService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void loadShopData(){
        // 1、查询店铺信息
        List<Shop> list = shopService.list();
        // 2、把店铺分组,按照 typeId 分组,typeId 一致的放到一个集合中
        Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        // 3、分批完成写入 Redis
        for (Map.Entry<Long, List<Shop>> longListEntry : map.entrySet()) {
            Long typeId = longListEntry.getKey();
            List<Shop> value = longListEntry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
            for (Shop shop : value) {
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(), shop.getY())
                ));
            }
            stringRedisTemplate.opsForGeo().add(RedisConstants.SHOP_GEO_KEY + typeId, locations);
        }

    }
}

三、实现附近商户功能

SpringDataRedis 的 2.3.9 版本并不支持 Redis6.2 提供的 GEOSEARCH 命令,因此我们需要提示版本,修改自己的 POM 文件。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-data-redis</artifactId>
            <groupId>org.springframework.data</groupId>
        </exclusion>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.6.2</version>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.1.6.RELEASE</version>
</dependency>

ShopController

@RestController
@RequestMapping("/shop")
public class ShopController {

    @Resource
    public IShopService shopService;

	/**
     * 根据商铺类型分页查询商铺信息
     * @param typeId 商铺类型
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/type")
    public Result queryShopByType(
            @RequestParam("typeId") Integer typeId,
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "x", required = false) Double x,
            @RequestParam(value = "y", required = false) Double y
    ) {
        return shopService.queryShopByType(typeId, current, x, y);
    }
}

IShopService

public interface IShopService extends IService<Shop> {
    Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}

ShopServiceImpl

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

	@Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 判断是否需要根据坐标查询
        if(x == null || y == null){
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        // 计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 查询 Redis,按照距离排序、分页。
        GeoResults<RedisGeoCommands.GeoLocation<String>> search = stringRedisTemplate.opsForGeo().
                search(RedisConstants.SHOP_GEO_KEY + typeId,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));

        if(search == null){
            return Result.ok(Collections.emptyList());
        }

        // 查询 Redis,按照距离排序、分页
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = search.getContent();
        if(from >= content.size()){
            return Result.ok(Collections.emptyList());
        }

        List<Long> ids = new ArrayList<>(content.size());
        Map<String, Distance> distanceMap = new HashMap<>(content.size());
        // 截取 from ~ end 的部分
        content.stream().skip(from).forEach(result -> {
            // 获取店铺 id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        String join = StrUtil.join(",", ids);
        // 根据 id 查询 shop
        List<Shop> shopList = query().in("id", ids).last("order by field(" + join + ")").list();

        for (Shop shop : shopList) {
           shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        
        return Result.ok(shopList);
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值