src/types/redis_geo.cc (284 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ #include "redis_geo.h" #include <algorithm> namespace redis { rocksdb::Status Geo::Add(engine::Context &ctx, const Slice &user_key, std::vector<GeoPoint> *geo_points, uint64_t *added_cnt) { std::vector<MemberScore> member_scores; for (const auto &geo_point : *geo_points) { /* Turn the coordinates into the score of the element. */ GeoHashBits hash; GeohashEncodeWGS84(geo_point.longitude, geo_point.latitude, GEO_STEP_MAX, &hash); GeoHashFix52Bits bits = GeoHashHelper::Align52Bits(hash); member_scores.emplace_back(MemberScore{geo_point.member, static_cast<double>(bits)}); } return ZSet::Add(ctx, user_key, ZAddFlags::Default(), &member_scores, added_cnt); } rocksdb::Status Geo::Dist(engine::Context &ctx, const Slice &user_key, const Slice &member_1, const Slice &member_2, double *dist) { std::map<std::string, GeoPoint> geo_points; auto s = MGet(ctx, user_key, {member_1, member_2}, &geo_points); if (!s.ok()) return s; for (const auto &member : {member_1, member_2}) { auto iter = geo_points.find(member.ToString()); if (iter == geo_points.end()) { return rocksdb::Status::NotFound(); } } *dist = GeoHashHelper::GetDistance(geo_points[member_1.ToString()].longitude, geo_points[member_1.ToString()].latitude, geo_points[member_2.ToString()].longitude, geo_points[member_2.ToString()].latitude); return rocksdb::Status::OK(); } rocksdb::Status Geo::Hash(engine::Context &ctx, const Slice &user_key, const std::vector<Slice> &members, std::vector<std::string> *geo_hashes) { std::map<std::string, GeoPoint> geo_points; auto s = MGet(ctx, user_key, members, &geo_points); if (!s.ok()) return s; for (const auto &member : members) { auto iter = geo_points.find(member.ToString()); if (iter == geo_points.end()) { geo_hashes->emplace_back(std::string()); continue; } geo_hashes->emplace_back(EncodeGeoHash(iter->second.longitude, iter->second.latitude)); } return rocksdb::Status::OK(); } rocksdb::Status Geo::Pos(engine::Context &ctx, const Slice &user_key, const std::vector<Slice> &members, std::map<std::string, GeoPoint> *geo_points) { return MGet(ctx, user_key, members, geo_points); } rocksdb::Status Geo::Radius(engine::Context &ctx, const Slice &user_key, double longitude, double latitude, double radius_meters, int count, DistanceSort sort, const std::string &store_key, bool store_distance, double unit_conversion, std::vector<GeoPoint> *geo_points) { GeoShape geo_shape; geo_shape.type = kGeoShapeTypeCircular; geo_shape.xy[0] = longitude; geo_shape.xy[1] = latitude; geo_shape.radius = radius_meters; geo_shape.conversion = 1; std::string dummy_member; return SearchStore(ctx, user_key, geo_shape, kLongLat, dummy_member, count, sort, store_key, store_distance, unit_conversion, geo_points); } rocksdb::Status Geo::RadiusByMember(engine::Context &ctx, const Slice &user_key, const Slice &member, double radius_meters, int count, DistanceSort sort, const std::string &store_key, bool store_distance, double unit_conversion, std::vector<GeoPoint> *geo_points) { GeoPoint geo_point; auto s = Get(ctx, user_key, member, &geo_point); if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s; return Radius(ctx, user_key, geo_point.longitude, geo_point.latitude, radius_meters, count, sort, store_key, store_distance, unit_conversion, geo_points); } rocksdb::Status Geo::Search(engine::Context &ctx, const Slice &user_key, GeoShape geo_shape, OriginPointType point_type, std::string &member, int count, DistanceSort sort, bool store_distance, double unit_conversion, std::vector<GeoPoint> *geo_points) { return SearchStore(ctx, user_key, geo_shape, point_type, member, count, sort, "", store_distance, unit_conversion, geo_points); } rocksdb::Status Geo::SearchStore(engine::Context &ctx, const Slice &user_key, GeoShape geo_shape, OriginPointType point_type, std::string &member, int count, DistanceSort sort, const std::string &store_key, bool store_distance, double unit_conversion, std::vector<GeoPoint> *geo_points) { if (point_type == kMember) { GeoPoint geo_point; auto s = Get(ctx, user_key, member, &geo_point); // store key is not empty, try to remove it before returning. if (!s.ok() && s.IsNotFound() && !store_key.empty()) { auto del_s = ZSet::Del(ctx, store_key); if (!del_s.ok()) return del_s; } if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s; geo_shape.xy[0] = geo_point.longitude; geo_shape.xy[1] = geo_point.latitude; } std::string ns_key = AppendNamespacePrefix(user_key); ZSetMetadata metadata(false); rocksdb::Status s = ZSet::GetMetadata(ctx, ns_key, &metadata); // store key is not empty, try to remove it before returning. if (!s.ok() && s.IsNotFound() && !store_key.empty()) { auto del_s = ZSet::Del(ctx, store_key); if (!del_s.ok()) return del_s; } if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s; // Get neighbor geohash boxes for radius search GeoHashRadius georadius = GeoHashHelper::GetAreasByShapeWGS84(geo_shape); // Get zset for all matching points membersOfAllNeighbors(ctx, user_key, georadius, geo_shape, geo_points); // if no matching results, give empty reply if (geo_points->empty()) { // store key is not empty, try to remove it before returning. if (!store_key.empty()) { auto del_s = ZSet::Del(ctx, store_key); if (!del_s.ok()) return del_s; } return rocksdb::Status::OK(); } // process [optional] sorting if (sort == kSortASC) { std::sort(geo_points->begin(), geo_points->end(), sortGeoPointASC); } else if (sort == kSortDESC) { std::sort(geo_points->begin(), geo_points->end(), sortGeoPointDESC); } // storing if (!store_key.empty()) { auto result_length = static_cast<int64_t>(geo_points->size()); int64_t returned_items_count = (count == 0 || result_length < count) ? result_length : count; if (returned_items_count == 0) { auto s = ZSet::Del(ctx, user_key); if (!s.ok()) return s; } else { std::vector<MemberScore> member_scores; for (const auto &geo_point : *geo_points) { if (returned_items_count-- <= 0) { break; } double score = store_distance ? geo_point.dist / unit_conversion : geo_point.score; member_scores.emplace_back(MemberScore{geo_point.member, score}); } ZSet::Overwrite(ctx, store_key, member_scores); } } return rocksdb::Status::OK(); } rocksdb::Status Geo::Get(engine::Context &ctx, const Slice &user_key, const Slice &member, GeoPoint *geo_point) { std::map<std::string, GeoPoint> geo_points; auto s = MGet(ctx, user_key, {member}, &geo_points); if (!s.ok()) return s; auto iter = geo_points.find(member.ToString()); if (iter == geo_points.end()) { return rocksdb::Status::InvalidArgument("could not decode requested zset member"); } *geo_point = iter->second; return rocksdb::Status::OK(); } rocksdb::Status Geo::MGet(engine::Context &ctx, const Slice &user_key, const std::vector<Slice> &members, std::map<std::string, GeoPoint> *geo_points) { std::map<std::string, double> member_scores; auto s = ZSet::MGet(ctx, user_key, members, &member_scores); if (!s.ok()) return s; for (const auto &member : members) { auto iter = member_scores.find(member.ToString()); if (iter == member_scores.end()) { continue; } double xy[2]; if (!decodeGeoHash(iter->second, xy)) { continue; } (*geo_points)[member.ToString()] = GeoPoint{xy[0], xy[1], member.ToString()}; } return rocksdb::Status::OK(); } std::string Geo::EncodeGeoHash(double longitude, double latitude) { const std::string geoalphabet = "0123456789bcdefghjkmnpqrstuvwxyz"; /* The internal format we use for geocoding is a bit different * than the standard, since we use as initial latitude range * -85,85, while the normal geohashing algorithm uses -90,90. * So we have to decode our position and re-encode using the * standard ranges in order to output a valid geohash string. */ /* Re-encode */ GeoHashRange r[2]; GeoHashBits hash; r[0].min = -180; r[0].max = 180; r[1].min = -90; r[1].max = 90; GeohashEncode(&r[0], &r[1], longitude, latitude, 26, &hash); std::string geo_hash; for (int i = 0; i < 11; i++) { int idx = 0; if (i == 10) { /* We have just 52 bits, but the API used to output * an 11 bytes geohash. For compatibility we assume * zero. */ idx = 0; } else { idx = static_cast<int>((hash.bits >> (52 - ((i + 1) * 5))) & 0x1f); } geo_hash += geoalphabet[idx]; } return geo_hash; } int Geo::decodeGeoHash(double bits, double *xy) { GeoHashBits hash = {(uint64_t)bits, GEO_STEP_MAX}; return GeohashDecodeToLongLatWGS84(hash, xy); } /* Search all eight neighbors + self geohash box */ int Geo::membersOfAllNeighbors(engine::Context &ctx, const Slice &user_key, GeoHashRadius n, const GeoShape &geo_shape, std::vector<GeoPoint> *geo_points) { GeoHashBits neighbors[9]; unsigned int last_processed = 0; int count = 0; neighbors[0] = n.hash; neighbors[1] = n.neighbors.north; neighbors[2] = n.neighbors.south; neighbors[3] = n.neighbors.east; neighbors[4] = n.neighbors.west; neighbors[5] = n.neighbors.north_east; neighbors[6] = n.neighbors.north_west; neighbors[7] = n.neighbors.south_east; neighbors[8] = n.neighbors.south_west; /* For each neighbor (*and* our own hashbox), get all the matching * members and add them to the potential result list. */ for (unsigned int i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) { if (HASHISZERO(neighbors[i])) { continue; } /* When a huge Radius (in the 5000 km range or more) is used, * adjacent neighbors can be the same, leading to duplicated * elements. Skip every range which is the same as the one * processed previously. */ if (last_processed && neighbors[i].bits == neighbors[last_processed].bits && neighbors[i].step == neighbors[last_processed].step) { continue; } count += membersOfGeoHashBox(ctx, user_key, neighbors[i], geo_points, geo_shape); last_processed = i; } return count; } /* Obtain all members between the min/max of this geohash bounding box. * Populate a GeoArray of GeoPoints by calling getPointsInRange(). * Return the number of points added to the array. */ int Geo::membersOfGeoHashBox(engine::Context &ctx, const Slice &user_key, GeoHashBits hash, std::vector<GeoPoint> *geo_points, const GeoShape &geo_shape) { GeoHashFix52Bits min = 0, max = 0; scoresOfGeoHashBox(hash, &min, &max); return getPointsInRange(ctx, user_key, static_cast<double>(min), static_cast<double>(max), geo_shape, geo_points); } /* Compute the sorted set scores min (inclusive), max (exclusive) we should * query in order to retrieve all the elements inside the specified area * 'hash'. The two scores are returned by reference in *min and *max. */ void Geo::scoresOfGeoHashBox(GeoHashBits hash, GeoHashFix52Bits *min, GeoHashFix52Bits *max) { /* We want to compute the sorted set scores that will include all the * elements inside the specified GeoHash 'hash', which has as many * bits as specified by hash.step * 2. * * So if step is, for example, 3, and the hash value in binary * is 101010, since our score is 52 bits we want every element which * is in binary: 101010????????????????????????????????????????????? * Where ? can be 0 or 1. * * To get the min score we just use the initial hash value left * shifted enough to get the 52 bit value. Later we increment the * 6 bit prefis (see the hash.bits++ statement), and get the new * prefix: 101011, which we align again to 52 bits to get the maximum * value (which is excluded from the search). So we get everything * between the two following scores (represented in binary): * * 1010100000000000000000000000000000000000000000000000 (included) * and * 1010110000000000000000000000000000000000000000000000 (excluded). */ *min = GeoHashHelper::Align52Bits(hash); hash.bits++; *max = GeoHashHelper::Align52Bits(hash); } /* Query a Redis sorted set to extract all the elements between 'min' and * 'max', appending them into the array of GeoPoint structures 'gparray'. * The command returns the number of elements added to the array. * * Elements which are farthest than 'radius' from the specified 'x' and 'y' * coordinates are not included. * * The ability of this function to append to an existing set of points is * important for good performances because querying by radius is performed * using multiple queries to the sorted set, that we later need to sort * via qsort. Similarly we need to be able to reject points outside the search * radius area ASAP in order to allocate and process more points than needed. */ int Geo::getPointsInRange(engine::Context &ctx, const Slice &user_key, double min, double max, const GeoShape &geo_shape, std::vector<GeoPoint> *geo_points) { /* include min in range; exclude max in range */ /* That's: min <= val < max */ RangeScoreSpec spec; spec.min = min; spec.max = max; spec.maxex = true; uint64_t size = 0; std::vector<MemberScore> member_scores; rocksdb::Status s = ZSet::RangeByScore(ctx, user_key, spec, &member_scores, &size); if (!s.ok()) return 0; for (const auto &member_score : member_scores) { appendIfWithinShape(geo_points, geo_shape, member_score.score, member_score.member); } return 0; } /* Helper function for geoGetPointsInRange(): given a sorted set score * representing a point, and another point (the center of our search) and * a radius, appends this entry as a geoPoint into the specified geoArray * only if the point is within the search area. * * returns true if the point is included, or false if it is outside. */ bool Geo::appendIfWithinRadius(std::vector<GeoPoint> *geo_points, double lon, double lat, double radius, double score, const std::string &member) { double distance = NAN, xy[2]; if (!decodeGeoHash(score, xy)) return false; /* Can't decode. */ /* Note that geohashGetDistanceIfInRadiusWGS84() takes arguments in * reverse order: longitude first, latitude later. */ if (!GeoHashHelper::GetDistanceIfInRadiusWGS84(lon, lat, xy[0], xy[1], radius, &distance)) { return false; } /* Append the new element. */ GeoPoint geo_point; geo_point.longitude = xy[0]; geo_point.latitude = xy[1]; geo_point.dist = distance; geo_point.member = member; geo_point.score = score; geo_points->emplace_back(geo_point); return true; } bool Geo::appendIfWithinShape(std::vector<GeoPoint> *geo_points, const GeoShape &geo_shape, double score, const std::string &member) { double distance = NAN, xy[2]; if (!decodeGeoHash(score, xy)) return false; if (geo_shape.type == kGeoShapeTypeCircular) { if (!GeoHashHelper::GetDistanceIfInRadiusWGS84(geo_shape.xy[0], geo_shape.xy[1], xy[0], xy[1], geo_shape.radius * geo_shape.conversion, &distance)) { return false; } } else if (geo_shape.type == kGeoShapeTypeRectangular) { if (!GeoHashHelper::GetDistanceIfInBoxWGS84(geo_shape.bounds, geo_shape.xy[0], geo_shape.xy[1], xy[0], xy[1], &distance)) { return false; } } /* Append the new element. */ GeoPoint geo_point; geo_point.longitude = xy[0]; geo_point.latitude = xy[1]; geo_point.dist = distance; geo_point.member = member; geo_point.score = score; geo_points->emplace_back(geo_point); return true; } bool Geo::sortGeoPointASC(const GeoPoint &gp1, const GeoPoint &gp2) { return gp1.dist < gp2.dist; } bool Geo::sortGeoPointDESC(const GeoPoint &gp1, const GeoPoint &gp2) { return gp1.dist >= gp2.dist; } } // namespace redis