4 kinds of LBS “nearby people” implementation scheme, everyone can understand

Time:2020-11-13

This article is included in personal blog: http://www.chengxy-nds.top , technical data sharing and learning together

Yesterday, a buddies of the official account and I discussed an interview question. Personally, it was more meaningful. I arranged it here to share it with you. The interview question is relatively simple: “let you realize the function of a nearby person, what plan do you have?” In fact, this question is mainly to investigate the breadth of technology. This paper introduces several schemes to give you some ideas, so as to avoid affecting the interview result due to the language jam in the interview process. If there is any laxity, please kindly point it out!

"People in the neighborhood"It is more commonly used in functional life, such as restaurants near takeout app and sharing cars near bike app. Since the probability of being asked in a common interview is very high, the following analysis is based onMysql databaseRedisMongoDBThe function of “people nearby” is realized.
4 kinds of LBS
The popular science: to mark a position in the world, the general practice is to use longitude and latitude. The range of longitude is (- 180, 180), and the range of latitude is (- 90, 90). The latitude is bounded by the equator, north is positive and south is negative, and the longitude is bounded by the prime meridian (Greenwich Observatory), positive in the East and negative in the West. For example, the longitude and latitude (116.49141, 40.01229) of the Motorola building in Wangjing are all positive numbers, because China is located in the Northeast hemisphere.


1、 “People nearby” principle

"People in the neighborhood"That is often saidLBSBased on the current location services, it provides value-added services for users based on their current location.

The core idea of “people nearby” is as follows:

  1. Search nearby users with “I” as the center
  2. Calculate the distance between others and me based on my current geographical location
  3. According to the distance between “I” and others, the nearest user or store is selected

4 kinds of LBS

2、 What is geohash algorithm?

I’m talking about it"People in the neighborhood"Before the specific implementation of the function, let’s have a look at itGeoHashAlgorithm, because it will always be dealt with later. The best way to locate a position is to useLongitude and latitudeIdentification, butLongitude and latitudeIt is two-dimensional, and it is very troublesome to calculate the positionLongitude and latitudeIt’s much easier to compare data into one-dimensional data, soGeoHashThe algorithm came into being.

GeoHashThe algorithm converts the two-dimensional longitude and latitude into a string, for example: 9 in the figure belowGeoHashThe string represents nine regions, and each string represents a rectangular region. The other points (longitude and latitude) in this rectangular area use the same oneGeoHashString representation.

4 kinds of LBS

such asWX4ERDue to the user’s data within this area of the user’s searchGeoHashAll strings areWX4ERTherefore, we can put theWX4ERAskey, restaurant information asvalueIf you do not use theGeoHashIn this algorithm, users in the region request restaurant data, and the longitude and latitude of users are different, so caching is not only troublesome, but also a huge amount of data.

GeoHashThe longer the string is, the more accurate the position is. The longer the string is, the smaller the error in distance is. BelowgeohashCode precision table:

Length, width and height of geohash codes
——– | —– | —– |
1 | 5,009.4km | 4,992.6km
2 |1,252.3km |624.1km
3 |156.5km |156km
4 |39.1km |19.5km
5 |4.9km |4.9km
6 |1.2km |609.4m
7 |152.9m |152.4m
8 |38.2m| 19m
9 |4.8m |4.8m
10 |1.2m |59.5cm
11 |14.9cm |14.9cm
12 |3.7cm |1.9cm

Moreover, the more similar the string is, the closer the distance is, and the more string prefix matches, the closer the distance is. For example, the longitude and latitude below represent three restaurants close to each other.

Merchant | latitude and longitude | geohash string
——– | —– | —– |
Chuanchuanxiang | 116.402843,39.999375 | wx4er9v
Hot pot | 116.3967,39.99932 | wx4rtk
Barbecue | 116.40382,39.918118 | wx4erfe

Let’s have a simple understanding of what isGeoHashEasy to expand the content,GeoHashThe content of the algorithm is relatively high and deep, and the interested partners can dig deeply by themselves. It doesn’t take too much space here (in fact, I know too shallow, cry and haw ~).

3、 Based on MySQL

This approach is purely based onmysqlImplemented, not usedGeoHashAlgorithm.

1. Design ideas

Taking the user as the center, assuming a distance of 500 meters as the radius, draw a circle. All users in this circular area are “people nearby” who meet the user’s requirements. However, there is a problem that the circle has radian. It is too difficult to search the circular area directly. It is impossible to search directly with longitude and latitude.

But if there is a square on the circular jacket, it is easy to search the user information in the square by obtaining the maximum and minimum values of user longitude and latitude (longitude, latitude + distance) and then using the maximum and minimum values as the filtering criteria.

So here comes the question,Does more come out area swollen do?

Let’s analyze. The distance from the user in this part of the area to the circle must be larger than the radius of the circle. Then we calculate the distance between the user’s center point and all users in the square, and screen out all users whose distance is less than or equal to the radius, and all users in the circular area meet the requirements"People in the neighborhood"

4 kinds of LBS

2. Analysis of advantages and disadvantages

Purely based onmysqlrealization"People in the neighborhood"The advantages are obvious, that is, it is simple, as long as you create a table to store the longitude and latitude information of users. The disadvantage is also obvious, which requires a lot of calculation of the distance between two points, which greatly affects the performance.

3. Implementation

Create a simple table to store the latitude and longitude attributes of users.

CREATE TABLE `nearby_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `Name ` varchar (255) default null comment 'name',
  `Longitude ` double default null comment 'longitude',
  `Latitude ` double default null comment 'latitude',
  `create_ time` datetime DEFAULT NULL ON UPDATE CURRENT_ Timestamp comment 'creation time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

To calculate the distance between two points, a tripartite library is used. After all, the wheel made by ourselves is not particularly round, and it may be square, ha ha ha ha~

<dependency>
     <groupId>com.spatial4j</groupId>
     <artifactId>spatial4j</artifactId>
     <version>0.5</version>
</dependency>

After the outer square is obtained, the users in the square area are searched by the maximum and minimum longitude and latitude values of the square, and then the users who exceed the specified distance are eliminatedpeople nearby

private SpatialContext spatialContext = SpatialContext.GEO;    
    
    /**
     *Get people in the neighborhood x meters
     *
     *@ param distance search range unit: km
     *@ param userlng longitude of current user
     *@ param userlat latitude of current user
     */
    @GetMapping("/nearby")
    public String nearBySearch(@RequestParam("distance") double distance,
                               @RequestParam("userLng") double userLng,
                               @RequestParam("userLat") double userLat) {
        //1. Get the circumscribed square
        Rectangle rectangle = getRectangle(distance, userLng, userLat);
        //2. Get all users in the square
        List<User> users = userMapper.selectUser(rectangle.getMinX(), rectangle.getMaxX(), rectangle.getMinY(), rectangle.getMaxY());
        //3. Remove redundant users whose radius exceeds the specified distance
        users = users.stream()
            .filter(a -> getDistance(a.getLongitude(), a.getLatitude(), userLng, userLat) <= distance)
            .collect(Collectors.toList());
        return JSON.toJSONString(users);
    }
    
    private Rectangle getRectangle(double distance, double userLng, double userLat) {
        return spatialContext.getDistCalc()
            .calcBoxByDistFromPt(spatialContext.makePoint(userLng, userLat), 
                                 distance * DistanceUtils.KM_TO_DEG, spatialContext, null);
    }

Because the sorting of distances between users is implemented in business code, you can see that SQL statements are also very simple.

    <select id="selectUser" resultMap="BaseResultMap">
        SELECT * FROM user
        WHERE 1=1
        and (longitude BETWEEN ${minlng} AND ${maxlng})
        and (latitude BETWEEN ${minlat} AND ${maxlat})
    </select>

4、 MySQL + geohash

1. Design ideas

The design idea of this method is simpler. When storing the location information of users, the corresponding values are calculated according to the user’s longitude and latitude attributesgeohashcharacter string.be careful: in calculationgeohashString, you need to specifygeohashThe precision of the string, that isgeohashThe length of the string,See abovegeohashPrecision table

When you need to getpeople nearby, only the current user is requiredgeohashString, database throughWHERE geohash Like 'geocode%‘To find outgeohashThen calculate the distance between the current user and the searched user, and filter out all the users whose distance is less than or equal to the specified distance (500 meters nearby), i.epeople nearby

2. Analysis of advantages and disadvantages

utilize GeoHashAlgorithm implementation"People in the neighborhood"There is a problem becausegeohashThe algorithm divides the map into rectangles and encodes each rectanglegeohashcharacter string. But my current point is very close to the adjacent point, but we are in two areas respectively. The point clearly in front of me cannot be found. It is dark under the light.

How to solve this problem?

In order to avoid similar two adjacent points in different regions, we need to obtain the current point at the same time(WX4G0)Near the area8RegionalgeohashCode, screening and comparison.
4 kinds of LBS

3. Implementation

We also need to design a table to store the longitude and latitude information of users, but the difference is to add one moregeo_codeField to store the geohash string. This field is calculated by the user’s latitude and longitude attributes. Frequently used fields are recommended to be indexed.

CREATE TABLE `nearby_user_geohash` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `Name ` varchar (255) default null comment 'name',
  `Longitude ` double default null comment 'longitude',
  `Latitude ` double default null comment 'latitude',
  `geo_ Code ` varchar (64) default null comment 'geohash code calculated by latitude and longitude',
  `create_ time` datetime DEFAULT NULL ON UPDATE CURRENT_ Timestamp comment 'creation time',
  PRIMARY KEY (`id`),
  KEY `index_geo_hash` (`geo_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Firstly, according to the user’s latitude and longitude information, the user’s coordinates are calculated after the specified precisiongeoHashCode, and then get 8 directions around the usergeoHashCode searches for users in the database, and finally filters out users beyond the given distance (within 500 meters).

private SpatialContext spatialContext = SpatialContext.GEO;

    /***
     *Add user
     * @return
     */
    @PostMapping("/addUser")
    public boolean add(@RequestBody UserGeohash user) {
        //The default precision is 12 bits
        String geoHashCode = GeohashUtils.encodeLatLon(user.getLatitude(),user.getLongitude());
        return userGeohashService.save(user.setGeoCode(geoHashCode).setCreateTime(LocalDateTime.now()));
    }


/**
     *Get people in the specified range nearby
     *
     *@ param distance distance (distance of nearby users) unit km
     *The precision of @ param len geohash (several bit string)
     *@ param userlng longitude of current user
     *@ param userlat latitude of current user
     * @return json
     */
    @GetMapping("/nearby")
    public String nearBySearch(@RequestParam("distance") double distance,
                               @RequestParam("len") int len,
                               @RequestParam("userLng") double userLng,
                               @RequestParam("userLat") double userLat) {


        //1. According to the required range, determine the accuracy of geohash code, and obtain the geohash code of current user coordinates
        GeoHash geoHash = GeoHash.withCharacterPrecision(userLat, userLng, len);
        //2. Get the geohash codes of 8 directions around the user
        GeoHash[] adjacent = geoHash.getAdjacent();

        QueryWrapper<UserGeohash> queryWrapper = new QueryWrapper<UserGeohash>()
            .likeRight("geo_code",geoHash.toBase32());
        Stream.of(adjacent).forEach(a -> queryWrapper.or().likeRight("geo_code",a.toBase32()));

        //3. Match the geohash code with specified precision
        List<UserGeohash> users = userGeohashService.list(queryWrapper);
        //4. Filter those that exceed the distance
        users = users.stream()
                .filter(a ->getDistance(a.getLongitude(),a.getLatitude(),userLng,userLat)<= distance)
                .collect(Collectors.toList());
        return JSON.toJSONString(users);
    }

    
    /***
     *The distance between two points in a sphere
     *@ param longitude 1
     *@ param latitude 1
     *@ param userlng longitude 2
     *@ param userlat latitude 2
     *@ return return distance, in KM
     */
    private double getDistance(Double longitude, Double latitude, double userLng, double userLat) {
        return spatialContext.calcDistance(spatialContext.makePoint(userLng, userLat),
                spatialContext.makePoint(longitude, latitude)) * DistanceUtils.DEG_TO_KM;
    }

5、 Redis + geohash

Redis 3.2After version, based ongeohashAnd data structureZsetIt provides geographic location related functions. Through the top twomysqlThe implementation of the discovery,people nearbyThe function is obviously read more and write less scenes, so useredisPerformance will be greatly improved.

1. Design ideas

redisrealizationpeople nearbyThe function is mainly throughGeoSix commands for the module.

  • GEOADD: add the given location object (latitude, longitude, name) to the specified key;
  • GEOPOS: returns the location (longitude and latitude) of all the given location objects from the key;
  • GEODIST: returns the distance between two given positions;
  • GEOHASH: returns the geohash representation of one or more location objects;
  • GEORADIUS: return all position objects in the target set whose distance from the center does not exceed the given maximum distance with the given longitude and latitude as the center;
  • GEORADIUSBYMEMBER: returns all the position objects whose distance does not exceed the given maximum distance with the given position object as the center.

withGEOADDCommand andGEORADIUSSimple example of command:

GEOADD key longitude latitude member [longitude latitude member ...]

Among them,keyIs the collection name,memberIs the object corresponding to the latitude and longitude.

GEOADDAdd location information of “hot pot shop” of multiple merchants:

Geoadd Hotel 119.98866180732716 30.27465803229662 hot pot restaurant

GEORADIUSAccording to the given longitude and latitude as the center, all position objects in the target set whose distance from the center does not exceed the given maximum distance (within 500m) are obtained"People in the neighborhood"

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STORedisT key]

Scope unit:m | km | ft | mi–>Meters, kilometers, feet, miles.

  • WITHDIST: returns the distance between the position object and the center at the same time. The unit of distance is consistent with the range unit given by the user.
  • WITHCOORD: returns the longitude and dimension of the location object.
  • WITHHASH: returns the ordered set score of the location object in the form of a 52 bit signed integer. This option is mainly used for the underlying application or debugging, but it has little effect in practice.
  • ASC | DESC: return position object element from near to far | return position object element from far to near.
  • COUNT count: select the first n matching position object elements. (return all elements if not set)
  • STORE key: save the geographic location information of the returned result to the specified key.
  • STORedisT key: save the distance between the return result and the center point to the specified key.

For example, the following command: get all hotels within 500 meters of the current location.

GEORADIUS hotel 119.98866180732716    30.27465803229662 500 m WITHCOORD

RedisInternal use of ordered sets(zset)Save the user’s location information,zsetEach element in the is an object with a locationscoreThe value is 52 bits calculated by latitude and longitudegeohashValue.

2. Analysis of advantages and disadvantages

redisrealizationpeople nearbyIt has high efficiency, simple integration, and also supports distance sorting. However, there are some errors in the results. In order to make the results more accurate, it is necessary to manually calculate the distance between the user center position and other user locations, and then screen again.

3. Implementation

Here’s what it looks likeJava redisImplementation version, the code is very concise.

@Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    //Key used by geo related commands
    private final static String KEY = "user_info";

    public boolean save(User user) {
        Long flag = redisTemplate.opsForGeo().add(KEY, new RedisGeoCommands.GeoLocation<>(
                user.getName(), 
                new Point(user.getLongitude(), user.getLatitude()))
        );
        return flag != null && flag > 0;
    }

    /**
     *Get users in the specified range nearby according to the current location
     *@ param distance specifies the range unit km, which can be determined according to {@ link org.springframework.data . geo.Metrics }Set it up
     *@ param userlng user longitude
     *@ param userlat user latitude
     * @return
     */
    public String nearBySearch(double distance, double userLng, double userLat) {
        List<User> users = new ArrayList<>();
        //1. Georadius obtains information in the vicinity
        GeoResults<RedisGeoCommands.GeoLocation<Object>> reslut = 
            redisTemplate.opsForGeo().radius(KEY, 
                        new Circle(new Point(userLng, userLat), new Distance(distance, Metrics.KILOMETERS)),
                        RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                                .includeDistance()
                                .includeCoordinates().sortAscending());
        //2. Collect information and save it in the list
        List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> content = reslut.getContent();
        //3. Filter out the data beyond the distance
        content.forEach(a-> users.add(
                new User().setDistance(a.getDistance().getValue())
                .setLatitude(a.getContent().getPoint().getX())
                .setLongitude(a.getContent().getPoint().getY())));
        return JSON.toJSONString(users);
    }

6、 Mongodb + 2D index

1. Design ideas

MongoDBThe realization of nearby people is mainly through its two kinds of geospatial index2dsphereand2d。 The bottom layer of the two indexes is still based onGeohashTo build. But with the international commonGeohashThere are also some differences. Please refer to the official documents for details.

2dsphereThe index only supports geometry query of spherical surface.

2dThe index supports flat geometry and some spherical queries. although2dIndexes support some spherical queries, but2dErrors may occur when indexing these spherical queries. Therefore, spherical query should be selected as much as possible2dsphereIndexes.

Although the methods of the two indexes are different, as long as the coordinate span is not too large, the distance difference between the two indexes can be ignored.

2. Implementation

First insert a batch of location data into theMongoDBcollectionName ithotel, equivalent toMySQLTable name of. Two fieldsnamename,locationIt is a pair of longitude and latitude data.

db.hotel.insertMany([
 {'name':'hotel1',  location:[115.993121,28.676436]},
 {'name':'hotel2',  location:[116.000093,28.679402]},
 {'name':'hotel3',  location:[115.999967,28.679743]},
 {'name':'hotel4',  location:[115.995593,28.681632]},
 {'name':'hotel5',  location:[115.975543,28.679509]},
 {'name':'hotel6',  location:[115.968428,28.669368]},
 {'name':'hotel7',  location:[116.035262,28.677037]},
 {'name':'hotel8',  location:[116.024770,28.68667]},
 {'name':'hotel9',  location:[116.002384,28.683865]},
 {'name':'hotel10', location:[116.000821,28.68129]},
])

Next we givelocationField creates a2dIndex, index accuracy throughbitsTo specify,bitsThe larger the index, the more accurate the index is.

db.coll.createIndex({'location':"2d"}, {"bits":11111})

usegeoNear Order a test,nearCurrent coordinates (longitude, latitude),sphericalWhether to calculate the spherical distance,distanceMultiplierRadius of the earth. The unit is meters. The default value is 6378137,maxDistanceFilter conditions (users within the specified distance), open radian must be divided intodistanceMultiplierdistanceFieldThe calculated distance between two points, field alias (random name).

db.hotel.aggregate({
    $geoNear:{
        Near: [115.999567,28.681813], // current coordinates
        Spherical: true, // calculate spherical distance
        Distancemultiplier: 6378137, // the radius of the earth in meters, then the division record is also meter
        Maxdistance: 2000 / 6378137, // within 2000m, radian is required
        Distancefield: "distance" // distance field alias
    }
})

You can see that there is qualified data in the result, and there is one more fielddistanceThe alias you just set represents the distance between two points.

{ "_id" : ObjectId("5e96a5c91b8d4ce765381e58"), "name" : "hotel10", "location" : [ 116.000821, 28.68129 ], "distance" : 135.60095397487655 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e51"), "name" : "hotel3", "location" : [ 115.999967, 28.679743 ], "distance" : 233.71915803517447 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e50"), "name" : "hotel2", "location" : [ 116.000093, 28.679402 ], "distance" : 273.26317035334176 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e57"), "name" : "hotel9", "location" : [ 116.002384, 28.683865 ], "distance" : 357.5791936927476 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e52"), "name" : "hotel4", "location" : [ 115.995593, 28.681632 ], "distance" : 388.62555058249967 }
{ "_id" : ObjectId("5e96a5c91b8d4ce765381e4f"), "name" : "hotel1", "location" : [ 115.993121, 28.676436 ], "distance" : 868.6740526419927 }

summary

The focus of this paper is not on the specific implementation, but to provide some design ideas. During the interview, you may not have a deep understanding of a certain technology, but if you have a wide range of knowledge, you can say a variety of design ideas from many aspects, and you can talk with a lot, then you will give the interviewers great favor and the probability of getting an offer will be much higher. and"People in the neighborhood"There are many scenarios for the use of functions, especially for e-commerce platforms. Therefore, students who want to enter large factories should have some knowledge about this kind of knowledge.


Code implementation refers to an open source project of a big guy. Here are demos of the first three implementation methods. Interested partners can learn about GitHub address:https://github.com/larscheng/larscheng-learning-demo/tree/master/NearbySearch,。

Recommended Today

Summary of recent use of gin

Recently, a new project is developed by using gin. Some problems are encountered in the process. To sum up, as a note, I hope it can help you. Cross domain problems Middleware: func Cors() gin.HandlerFunc { return func(c *gin.Context) { //Here you can use * or the domain name you specify c.Header(“Access-Control-Allow-Origin”, “*”) //Allow header […]