Redis——GeoHash

GeoHash算法

Redis 在 3.2 版本以后增加了地理位置 GEO 模块。Redis 也使用 GeoHash 算 法。GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一 条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附 近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行 了。

那这个映射算法具体是怎样的呢?它将整个地球看成一个二维平面,然后划分成了一系 列正方形的方格,就好比围棋棋盘。所有的地图元素坐标都将放置于唯一的方格中。方格越 小,坐标越精确。然后对这些方格进行整数编码,越是靠近的方格编码越是接近。那如何编 码呢?一个最简单的方案就是切蛋糕法。设想一个正方形的蛋糕摆在你面前,二刀下去均分 分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对 每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数 予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会 越来越高。

上面的例子中使用的是二刀法,真实算法中还会有很多其它刀法,最终编码出来的整数 数字也都不一样。

编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐 标,整数越长,还原出来的坐标值的损失程度就越小。对于「附近的人」这个功能而言,损 失的一点精确度可以忽略不计。

GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变 成一个字符串。在 Redis 里面,经纬度使用 52 位的整数进行编码,放进了 zset 里面,zset 的 value 是元素的 key,score 是 GeoHash 的 52 位整数值。zset 的 score 虽然是浮点数, 但是对于 52 位的整数值,它可以无损存储。

在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个 zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一 些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。

我们知道,经度范围是东经180到西经180,纬度范围是南纬90到北纬90,我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

如果纬度范围[-90°, 0°)用二进制0代表,(0°, 90°]用二进制1代表,经度范围[-180°, 0°)用二进制0代表,(0°, 180°]用二进制1代表,那么地球可以分成如下4个部分
在这里插入图片描述

继续划分:
在这里插入图片描述

Geohash算法一共有三步:

1.首先将经纬度变成二进制:
比如这样一个点(39.923201, 116.390705)
纬度的范围是(-90,90),其中间值为0。对于纬度39.923201,在区间(0,90)中,因此得到一个1;(0,90)区间的中间值为45度,纬度39.923201小于45,因此得到一个0,依次计算下去,即可得到纬度的二进制表示,如下表:
在这里插入图片描述

最后得到纬度的二进制表示为:

10111000110001111001

同理可以得到经度116.390705的二进制表示为:

11010010110001000100

2.第二步,就是将经纬度合并:
经度占偶数位,纬度占奇数位,注意,0也是偶数位。

 11100 11101 00100 01111 00000 01101 01011 00001

3. 第三步,按照Base32进行编码:
Base32编码表的其中一种如下,是用0-9、b-z(去掉a, i, l, o)这32个字母进行编码。具体操作是先将上一步得到的合并后二进制转换为10进制数据,然后对应生成Base32码。需要注意的是,将5个二进制位转换成一个base32码。上例最终得到的值为

wx4g0ec1

Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

  • GeoHash用一个字符串表示经度和纬度两个坐标。在数据库中可以实现在一列上应用索引(某些情况下无法在两列上同时应用索引)
  • GeoHash表示的并不是一个点,而是一个矩形区域
  • GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索

编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离。

在这里插入图片描述

GEO数据结构

在这里插入图片描述

GEOADD

1、概念

将给定的空间元素(纬度、经度、名字)添加到指定的键里面。 这些数据会以有序集合的形式被储存在键里面, 从而使得像GEORADIUSGEORADIUSBYMEMBER 这样的命令可以在之后通过位置查询取得这些元素。

GEOADD 命令以标准的 x,y 格式接受参数, 所以用户必须先输入经度, 然后再输入纬度。 GEOADD 能够记录的坐标是有限的: 非常接近两极的区域是无法被索引的。 精确的坐标限制由 EPSG:900913 / EPSG:3785 / OSGEO:41001 等坐标系统定义, 具体如下:

  • 有效的经度介于 -180 度至 180 度之间。
  • 有效的纬度介于 -85.05112878 度至 85.05112878 度之间。
  • 当用户尝试输入一个超出范围的经度或者纬度时, GEOADD 命令将返回一个错误。
2、GEOADD命令

时间复杂度: 每添加一个元素的复杂度为 O(log(N)) , 其中 N 为键里面包含的位置元素数量。

命令demo: 
GEOADD key longitude latitude member [longitude latitude member ...]

命令描述:将指定的地理空间位置(纬度、经度、名称)添加到指定的key中。

返回值:添加到sorted set元素的数目,但不包括已更新score的元素。
3、示例
127.0.0.1:6379> geoadd beijing 116.182881 39.72877 良乡大学城北
(integer) 1
127.0.0.1:6379> geoadd beijing 116.182881 39.72877 良乡大学城南
(integer) 1
127.0.0.1:6379> geoadd beijing 116.318427 39.986771 新东方大厦 116.319873 39.987953 中钢大厦
(integer) 2

GEODIST

1、概念

返回两个给定位置之间的距离。

如果两个位置之间的其中一个不存在, 那么命令返回空值。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

GEODIST 命令在计算距离时会假设地球为完美的球形, 在极限情况下, 这一假设最大会造成 0.5% 的误差。

2、命令
复杂度: O(log(N))

命令demo: 
GEODIST key member1 member2 [unit]
3、示例
127.0.0.1:6379> geodist beijing 新东方大厦 良乡大学城北 m
"30942.4262"
127.0.0.1:6379> geodist beijing 新东方大厦 良乡大学城北 km
"30.9424"

GEOPOS

1、概念

从键里面返回所有给定位置元素的位置(经度和纬度)。

因为 GEOPOS 命令接受可变数量的位置元素作为输入, 所以即使用户只给定了一个位置元素, 命令也会返回数组回复。

GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。 当给定的位置元素不存在时, 对应的数组项为空值。

2、命令
时间复杂度: 获取每个位置元素的复杂度为 O(log(N)) , 其中 N 为键里面包含的位置元素数量。

命令demo: 
GEOPOS key member [member ...]
3、示例
127.0.0.1:6379> geopos beijing 良乡大学城北
1) 1) "116.18287950754165649"
   2) "39.7287695384729389"
127.0.0.1:6379> geopos beijing 菜市场
1) (nil)
GEOHASH
1、概念

返回一个或多个位置元素的 Geohash 表示。

返回值:一个数组, 数组的每个项都是一个 geohash 。 命令返回的 geohash 的位置与用户给定的位置元素的位置一一对应。

2、命令
时间复杂度: 寻找每个位置元素的复杂度为 O(log(N)) , 其中 N 为给定键包含的位置元素数量。

命令demo: 
GEOHASH key member [member ...]

命令描述:返回一个或多个位置元素的 Geohash 表示。通常使用表示位置的元素使用不同的技术,使用Geohash位置52点整数编码。由于编码和解码过程中所使用的初始最小和最大坐标不同,编码的编码也不同于标准。此命令返回一个标准的Geohash
3、示例
127.0.0.1:6379> geohash beijing 良乡大学城南
1) "wx4d5b3uc70"
127.0.0.1:6379> geohash beijing 中刚大厦
1) (nil)
127.0.0.1:6379> geohash beijing 中钢大厦
1) "wx4eqztd9q0"

GEORADIUS

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。

  • WITHCOORD : 将位置元素的经度和维度也一并返回。

  • WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
    命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

  • ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。

  • DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。

在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT 选项去获取前 N 个匹配元素, 但是因为命令在内部可能会需要对所有被匹配的元素进行处理, 所以在对一个非常大的区域进行搜索时, 即使只使用 COUNT 选项去获取少量元素, 命令的执行速度也可能会非常慢。 但是从另一方面来说, 使用 COUNT 选项去减少需要返回的元素数量, 对于减少带宽来说仍然是非常有用的。

GEORADIUS 命令返回一个数组, 具体来说:

  • 在没有给定任何 WITH 选项的情况下, 命令只会返回一个像 [“New York”,“Milan”,“Paris”] 这样的线性(linear)列表。
  • 在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下, 命令返回一个二层嵌套数组, 内层的每个子数组就表示一个元素。

在返回嵌套数组时, 子数组的第一个元素总是位置元素的名字。 至于额外的信息, 则会作为子数组的后续元素, 按照以下顺序被返回:

  • 以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
  • geohash 整数。
  • 由两个元素组成的坐标,分别为经度和纬度。
2、命令
时间复杂度: O(N+log(M)), 其中 N 为指定半径范围内的位置元素数量, 而 M 则是被返回位置元素的数量。

命令demo: 
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
3、示例
127.0.0.1:6379> georadius beijing 116.31801992654800415 39.98575224321251653 10 km withcoord
1) 1) "\xe6\x96\xb0\xe4\xb8\x9c\xe6\x96\xb9\xe5\xa4\xa7\xe5\x8e\xa6"
   2) 1) "116.31842762231826782"
      2) "39.98677120111857164"
2) 1) "\xe4\xb8\xad\xe9\x92\xa2\xe5\xa4\xa7\xe5\x8e\xa6"
   2) 1) "116.31987065076828003"
      2) "39.98795238117882178"
GEORADIUSBYMEMBER
1、概念

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。

时间复杂度: O(log(N)+M), 其中 N 为指定范围之内的元素数量, 而 M 则是被返回的元素数量。

2、命令
命令: 
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
3、示例
127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin
(integer) 1
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader
(integer) 1
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
(integer) 1
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi
(integer) 2

# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
2) "juejin"
3) "meituan"

# 范围 20 公里以内最多 3 个元素按距离倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc
1) "jd"
2) "meituan"
3) "juejin"

# 三个可选参数 withcoord withdist withhash 用来携带附加参数
# withdist 很有用,它可以用来显示距离
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc
1) 1) "ireader"
   2) "0.0000"
   3) (integer) 4069886008361398
   4) 1) "116.5142020583152771"
      2) "39.90540918662494363"
2) 1) "juejin"
   2) "10.5501"
   3) (integer) 4069887154388167
   4) 1) "116.48104995489120483"
      2) "39.99679348858259686"
3) 1) "meituan"
   2) "11.5748"
   3) (integer) 4069887179083478
   4) 1) "116.48903220891952515"
      2) "40.00766997707732031"

Java测试

package com.example.demo.book;

import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.GeoRadiusParam;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author: acton_zhang
 * @Date: 2023/4/17 1:50 下午
 * @Version 1.0
 */
public class RedisGEOTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.auth("123456");

        String key = "company";

        jedis.geoadd(key, 116.48105, 39.996794, "juejin");
        jedis.geoadd(key, 116.514203, 39.905409, "ireader");
        jedis.geoadd(key, 116.489033, 40.007669, "meituan");

        GeoCoordinate coordinate1 = new GeoCoordinate(116.562108, 39.787602);
        GeoCoordinate coordinate2 = new GeoCoordinate(116.334255, 40.027400);
        Map<String, GeoCoordinate> param = new HashMap<>();
        param.put("jd", coordinate1);
        param.put("xiaomi", coordinate2);
        jedis.geoadd(key, param);

        List<GeoRadiusResponse> res = jedis.georadiusByMember(key, "ireader", 20d, GeoUnit.KM,
                GeoRadiusParam.geoRadiusParam().withCoord().withDist().withHash().count(3).sortAscending());
        for (GeoRadiusResponse response : res) {
            System.out.println(response.getMemberByString());
            System.out.println(response.getDistance());
            System.out.println(response.getRawScore());
            System.out.println(response.getCoordinate());
            System.out.println("*************************");
        }

    }
}

ireader
0.0
4069886008361398
(116.51420205831528,39.905409186624944)
*************************
juejin
10.5501
4069887154388167
(116.4810499548912,39.9967934885826)
*************************
meituan
11.5748
4069887179083478
(116.48903220891953,40.00766997707732)
*************************
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值