Use GEOSearch to realize the function of nearby merchants

Article directory

    • 1 Basic usage of GEO data structure
    • 2 Import store data to GEO
    • 3 Realize the function of nearby merchants

1 Basic usage of GEO data structure

GEO is the abbreviation of Geolocation, which stands for geographic coordinates. Redis added support for GEO in version 3.2, which allows storing geographic coordinate information and helping us retrieve data based on latitude and longitude. Common commands are:

  • GEOADD: Add a geospatial information, including: longitude (longitude), latitude (latitude), value (member)
  • GEODIST: Calculate the distance between the specified two points and return
  • GEOHASH: Convert the coordinates of the specified member into a hash string and return
  • GEOPOS: returns the coordinates of the specified member
  • GEORADIUS: Specify the center and radius of the circle, find all the members contained in the circle, sort them according to the distance from the center of the circle, and return them. 6. Obsolete in the future
  • GEOSEARCH: Search for members within the specified range, and return them sorted by distance from the specified point. Ranges can be circular or rectangular. 6.2. New features
  • GEOSEARCHSTORE: The function is the same as GEOSEARCH, but the result can be stored in a specified key. 6.2. New features

2 Import store data to GEO

Specific scene description:

1653822036941

When we click on the food, a series of merchants will appear. The merchants can be sorted in a variety of ways. At this time, we are concerned about the distance. This place needs to use our GEO to pass the address collected by the current app to the background ( We have hardcoded it here), take the current coordinates as the center of the circle, bind the same store type type, and paging information at the same time, pass these conditions into the background, query the corresponding data in the background, and then return.

1653822021827

What we have to do is: import the data in the database table into redis, GEO in redis, GEO in redis is just a menber and a latitude and longitude, we pass the x and y axes to the latitude and longitude position made by redis , but we can’t put all the data into menber. After all, as redis is a memory-level database, if there is a huge amount of data, redis is still powerless, so we can store his id here.

But there is still a problem at this time, that is, the type is not stored in redis, so we cannot filter the data according to the type, so we can group according to the merchant type, and the merchants of the same type are the same group, and the typeId is used as the key to store into the same GEO set

the code

HmDianPingApplicationTests

@Test
void loadShopData() {<!-- -->
    // 1. Query store information
    List<Shop> list = shopService. list();
    // 2. Group the stores according to typeId, and put the same typeId into a collection
    Map<Long, List<Shop>> map = list. stream(). collect(Collectors. groupingBy(Shop::getTypeId));
    // 3. Write to Redis in batches
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {<!-- -->
        // 3.1. Get type id
        Long typeId = entry. getKey();
        String key = SHOP_GEO_KEY + typeId;
        // 3.2. Get the collection of stores of the same type
        List<Shop> value = entry. getValue();
        List<RedisGeoCommands. GeoLocation<String>> locations = new ArrayList<>(value. size());
        // 3.3. Write redis GEOADD key longitude latitude member
        for (Shop shop : value) {<!-- -->
            // stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
            locations.add(new RedisGeoCommands.GeoLocation<>(
                    shop. getId(). toString(),
                    new Point(shop. getX(), shop. getY())
            ));
        }
        stringRedisTemplate.opsForGeo().add(key, locations);
    }
}

3 Realize the function of nearby merchants

The 2.3.9 version of SpringDataRedis does not support the GEOSEARCH command provided by Redis 6.2, so we need to upgrade its version and modify our POM

Step 1: import 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>

Step two:

ShopController

@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);
}

ShopServiceImpl

 @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {<!-- -->
        // 1. Determine whether to query based on coordinates
        if (x == null || y == null) {<!-- -->
            // No need for coordinate query, query by database
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // return data
            return Result.ok(page.getRecords());
        }

        // 2. Calculate paging parameters
        int from = (current - 1) * SystemConstants. DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants. DEFAULT_PAGE_SIZE;

        // 3. Query redis, sort by distance, and paginate. Result: shopId, distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference. fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
                );
        // 4. Parse the id
        if (results == null) {<!-- -->
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands. GeoLocation<String>>> list = results. getContent();
        if (list. size() <= from) {<!-- -->
            // There is no next page, end
            return Result.ok(Collections.emptyList());
        }
        // 4.1. Intercept the part from ~ end
        List<Long> ids = new ArrayList<>(list. size());
        Map<String, Distance> distanceMap = new HashMap<>(list. size());
        list.stream().skip(from).forEach(result -> {<!-- -->
            // 4.2. Get the store id
            String shopIdStr = result. getContent(). getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3. Get the distance
            Distance distance = result. getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5. Query Shop by id
        String idStr = StrUtil. join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {<!-- -->
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6. return
        return Result.ok(shops);
    }