Redis GEO

来源:
三产
最后修订:
2017年06月20日 16:36:34
 202

简介

Redis 3.2 版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大音。GEO功能是 Redis 的另一位作者...

简介

Redis 3.2 版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大音。GEO功能是 Redis 的另一位作者Matt Stancliff 借鉴 NoSQL 数据库 Ardb 实现的,Ardb 的作者来自中国,它提供了优秀的GEO功能。

相关命令

增加地理位置信息

GEOADD

自3.2.0可用。

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

语法:GEOADD key longitude latitude member [longitude latitude member …]
说明:

longitudelatitudemember 分别是该地理位置的经度、纬度、成员(就是名称)。

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

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

  • 有效的经度介于 -180 度至 180 度之间。
  • 有效的纬度介于 -85.05112878 度至 85.05112878 度之间。

当用户尝试输入一个超出范围的经度或者纬度时, GEOADD 命令将返回一个错误。

返回值:

新添加到键里面的空间元素数量, 不包括那些已经存在但是被更新的元素。

示例:
# http://api.map.baidu.com/lbsapi/getpoint/index.html 这里可以获取坐标,我们填入 天安门 香山 两个位置的坐标
 coderknock> GEOADD tour 116.404412 39.915046 TianAnMen 116.20003 40.002428 XiangShan
(integer) 2
# 添加北京电影学院、毛主席纪念堂,更新 天安门坐标(返回值不包括更新的个数)
 coderknock> GEOADD tour 116.362444 39.977552 BeijingFilmAcademy 116.404269 39.909179 ChairmanMaoZedongMemorialHall 116.404412 39.915046 TianAnMen
(integer) 2

获取地理位置

GEOPOS

自3.2.0可用。

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

语法:GEOPOS key member [member …]
说明:

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

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

返回值:

GEOPOS 命令返回一个数组, 数组中的每个项都由两个元素组成: 第一个元素为给定位置元素的经度, 而第二个元素则为给定位置元素的纬度。

当给定的位置元素不存在时, 对应的数组项为空值。

示例:
# key 不存在
 coderknock> GEOPOS nonKey
(empty list or set)
# 查询天安门、毛主席纪念堂的位置信息
 coderknock> GEOPOS tour TianAnMen ChairmanMaoZedongMemorialHall
1) 1) "116.40441387891769"
   2) "39.915046196472751"
2) 1) "116.40426903963089"
   2) "39.909178316988886"

获取两个地理位置的距离

GEOPOS

自3.2.0可用。

时间复杂度:O(log(N))。

语法:GEOPOS key member [member …]
说明:

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

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

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

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

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

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

返回值:

计算出的距离会以双精度浮点数的形式被返回。 如果给定的位置元素不存在, 那么命令返回空值。

示例:
# 查询天安门 到 毛主席纪念堂的距离,使用默认单位 m
 coderknock> GEODIST tour TianAnMen ChairmanMaoZedongMemorialHall
"652.7795"
# 查询天安门 到 毛主席纪念堂的距离,使用单位 km 
 coderknock>  GEODIST tour TianAnMen ChairmanMaoZedongMemorialHall km
"0.6528"
# 查询的第二个元素不存在,返回 nil
 coderknock>  GEODIST tour TianAnMen non
(nil)

GEOHASH

GEOHASH

自3.2.0可用。

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

语法:GEOHASH key member [member …]
说明:

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

返回值:

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

示例:
# GEOHash 通过算法可以转成 经纬度格式
 coderknock> GEOHASH tour TianAnMen
1) "wx4g0f71gr0"

GEOHash 有如下特点:

  • GEO的数据类型为 zset,Redis 将所有地理位置信息的 GEOHash 存放在 zset 中。

    coderknock> TYPE tour
    zset
    
  • 字符串越长,表示的位置更精确,例如 GEOHash 长度为9时,精度在2米左右。

GEOHash 长度 精度(km)
1 2500
2 630
3 78
4 20
5 2.4
6 0.61
7 0.076
8 0.019
9 0.002

  • 两个字符串越相似,它们之间的距离越近,Redis 利用字符串前缀匹配算法实现相关的命令。

  • GEOHash 编码和经纬度是可以相互转换的。

下面是 Java 版本的算法:

import jodd.util.Base32;

import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;

/**
 * <p> 本代码来自 http://www.cnblogs.com/gaopeng527/p/5066983.html </p>
 *
 * @author 三产
 * @version 1.0
 * @date 2017-06-20
 * @QQGroup 213732117
 * @website http://www.coderknock.com
 * @copyright Copyright 2017 拿客 coderknock.com  All rights reserved.
 * @since JDK 1.8
 */
public class GeoHash {
    private static int numbits = 6 * 5; //经纬度单独编码长度
    //32位编码对应字符
    final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
    //定义编码映射关系
    final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();

    //初始化编码映射内容
    static {
        int i = 0;
        for (char c : digits)
            lookup.put(c, i++);
    }

    /**
     * 对编码后的字符串解码
     *
     * @param geohash
     * @return
     */
    public double[] decode(String geohash) {
        StringBuilder buffer = new StringBuilder();
        for (char c : geohash.toCharArray()) {

            int i = lookup.get(c) + 32;
            buffer.append(Integer.toString(i, 2).substring(1));
        }

        BitSet lonset = new BitSet();
        BitSet latset = new BitSet();

        //偶数位,经度
        int j = 0;
        for (int i = 0; i < numbits * 2; i += 2) {
            boolean isSet = false;
            if (i < buffer.length())
                isSet = buffer.charAt(i) == '1';
            lonset.set(j++, isSet);
        }

        //奇数位,纬度
        j = 0;
        for (int i = 1; i < numbits * 2; i += 2) {
            boolean isSet = false;
            if (i < buffer.length())
                isSet = buffer.charAt(i) == '1';
            latset.set(j++, isSet);
        }

        double lon = decode(lonset, -180, 180);
        double lat = decode(latset, -90, 90);

        return new double[]{lat, lon};
    }

    /**
     * 根据二进制和范围解码
     *
     * @param bs
     * @param floor
     * @param ceiling
     * @return
     */
    private double decode(BitSet bs, double floor, double ceiling) {
        double mid = 0;
        for (int i = 0; i < bs.length(); i++) {
            mid = (floor + ceiling) / 2;
            if (bs.get(i))
                floor = mid;
            else
                ceiling = mid;
        }
        return mid;
    }

    /**
     * 对经纬度进行编码
     *
     * @param lat
     * @param lon
     * @return
     */
    public String encode(double lat, double lon) {
        BitSet latbits = getBits(lat, -90, 90);
        BitSet lonbits = getBits(lon, -180, 180);
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < numbits; i++) {
            buffer.append((lonbits.get(i)) ? '1' : '0');
            buffer.append((latbits.get(i)) ? '1' : '0');
        }
        return base32(Long.parseLong(buffer.toString(), 2));
    }

    /**
     * 根据经纬度和范围,获取对应二进制
     *
     * @param lat
     * @param floor
     * @param ceiling
     * @return
     */
    private BitSet getBits(double lat, double floor, double ceiling) {
        BitSet buffer = new BitSet(numbits);
        for (int i = 0; i < numbits; i++) {
            double mid = (floor + ceiling) / 2;
            if (lat >= mid) {
                buffer.set(i);
                floor = mid;
            } else {
                ceiling = mid;
            }
        }
        return buffer;
    }

    /**
     * 将经纬度合并后的二进制进行指定的32位编码
     *
     * @param i
     * @return
     */
    private String base32(long i) {
        char[] buf = new char[65];
        int charPos = 64;
        boolean negative = (i < 0);
        if (!negative)
            i = -i;
        while (i <= -32) {
            buf[charPos--] = digits[(int) (-(i % 32))];
            i /= 32;
        }
        buf[charPos] = digits[(int) (-i)];

        if (negative)
            buf[--charPos] = '-';
        return new String(buf, charPos, (65 - charPos));
    }

    public static void main(String[] args) throws Exception {
        GeoHash geohash = new GeoHash();
        String s = geohash.encode(39.9150413274765, 116.40440583229065);
        System.out.println(s);
        double[] geo = geohash.decode(s);
        System.out.println(geo[0] + " " + geo[1]);
        System.out.println(Arrays.toString(geohash.decode("wx4g0f71gr0")));
    }
}

获取指定位置范围内的地理信息位置集合

GEORADIUS

自3.2.0可用。

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

语法:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD][WITHDIST][WITHHASH][ASC|DESC][COUNT count][STORE key][STOREDIST key]
说明:

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

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

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

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

  • WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD : 将位置元素的经度和维度也一并返回。
  • WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。

命令默认返回未排序的位置元素。 通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

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

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

  • STORE key :将返回结果的地理位置信息保存到指定键。
  • STOREDIST key :将返回结果离中心节点的距离保存到指定键。
返回值:

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

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

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

  1. 以浮点数格式返回的中心与位置元素之间的距离, 单位与用户指定范围时的单位一致。
  2. geohash 整数。
  3. 由两个元素组成的坐标,分别为经度和纬度。
示例:
# 查看 tour 中距离北京南站 100 km 以内的景点
 coderknock> GEORADIUS tour 116.385871 39.871977 100 km
1) "XiangShan"
2) "BeijingFilmAcademy"
3) "ChairmanMaoZedongMemorialHall"
4) "TianAnMen"
# 查看 tour 中距离北京南站 10 km 以内的景点同时显示其经纬度
 coderknock> GEORADIUS tour 116.385871 39.871977 10 km WITHCOORD

1) 1) "ChairmanMaoZedongMemorialHall"
   2) 1) "116.40426903963089"
      2) "39.909178316988886"
2) 1) "TianAnMen"
   2) 1) "116.40441387891769"
      2) "39.915046196472751"
# 查看 tour 中距离北京南站 20 km 以内的景点同时显示其经纬度以及距离北京南站的距离(这里的单位是 km)
 coderknock> GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST
1) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) 1) "116.36244267225266"
      2) "39.97755242026205"
2) 1) "ChairmanMaoZedongMemorialHall"
   2) "4.4256"
   3) 1) "116.40426903963089"
      2) "39.909178316988886"
3) 1) "TianAnMen"
   2) "5.0450"
   3) 1) "116.40441387891769"
      2) "39.915046196472751"
# 查看 tour 中距离北京南站 20 km 以内的景点同时显示其经纬度以及距离北京南站的距离(这里的单位是 km)并且按从近到远排序 
 coderknock> GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST ASC
1) 1) "ChairmanMaoZedongMemorialHall"
   2) "4.4256"
   3) 1) "116.40426903963089"
      2) "39.909178316988886"
2) 1) "TianAnMen"
   2) "5.0450"
   3) 1) "116.40441387891769"
      2) "39.915046196472751"
3) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) 1) "116.36244267225266"
      2) "39.97755242026205" 
# 查看 tour 中距离北京南站 20 km 以内的景点同时显示其经纬度以及距离北京南站的距离(这里的单位是 km)并且按从远到近排序       
 coderknock>  GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST DESC
1) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) 1) "116.36244267225266"
      2) "39.97755242026205"
2) 1) "TianAnMen"
   2) "5.0450"
   3) 1) "116.40441387891769"
      2) "39.915046196472751"
3) 1) "ChairmanMaoZedongMemorialHall"
   2) "4.4256"
   3) 1) "116.40426903963089"
      2) "39.909178316988886"     
# 查看 tour 中距离北京南站 20 km 以内的景点同时显示其经纬度以及距离北京南站的距离(这里的单位是 km)并且按从远到近排序 并且显示 geohash 整数
 coderknock> GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST DESC WITHHASH
1) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) (integer) 4069880657711258
   4) 1) "116.36244267225266"
      2) "39.97755242026205"
2) 1) "TianAnMen"
   2) "5.0450"
   3) (integer) 4069885555136394
   4) 1) "116.40441387891769"
      2) "39.915046196472751"
3) 1) "ChairmanMaoZedongMemorialHall"
   2) "4.4256"
   3) (integer) 4069885371962385
   4) 1) "116.40426903963089"
      2) "39.909178316988886"  
# 查看 tour 中距离北京南站 20 km 以内的景点同时显示其经纬度以及距离北京南站的距离(这里的单位是 km)并且按从远到近排序 并且显示 geohash 的前两个元素       
 coderknock>  GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST DESC WITHHASH COUNT 2
1) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) (integer) 4069880657711258
   4) 1) "116.36244267225266"
      2) "39.97755242026205"
2) 1) "TianAnMen"
   2) "5.0450"
   3) (integer) 4069885555136394
   4) 1) "116.40441387891769"
      2) "39.915046196472751"
# 使用 STORE 或者 STOREDIST 不允许使用 出 COUNT 以为的其他选项       
 coderknock>  GEORADIUS tour 116.385871 39.871977 20 km WITHCOORD WITHDIST DESC WITHHASH COUNT 2 STORE posKey STOREDIST distKey
(error) ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options
# 存储
 coderknock> GEORADIUS tour 116.385871 39.871977 20 km  STORE posKey STOREDIST distKey COUNT 2
(integer) 2      
 coderknock> ZRANGE distKey 0 -1 WITHSCORES
1) "ChairmanMaoZedongMemorialHall"
2) "4.4256428962580543"
3) "TianAnMen"
4) "5.0450138622984122"
 coderknock> TYPE posKey
none
# 同时使用 STORE STOREDIST 只有最后一个选项生效
 coderknock> GEORADIUS tour 116.385871 39.871977 20 km  STORE posKey
(integer) 3
 coderknock> TYPE posKey
zset
# 这里位置信息存储的是 geohash 整数
 coderknock> ZRANGE posKey 0 -1 WITHSCORES
1) "BeijingFilmAcademy"
2) "4069880657711258"
3) "ChairmanMaoZedongMemorialHall"
4) "4069885371962385"
5) "TianAnMen"
6) "4069885555136394"
# 上面存储的位置信息可以直接使用
 coderknock> GEORADIUS posKey 116.385871 39.871977 20 km WITHCOORD WITHDIST DESC WITHHASH COUNT 2
1) 1) "BeijingFilmAcademy"
   2) "11.9116"
   3) (integer) 4069880657711258
   4) 1) "116.36244267225266"
      2) "39.97755242026205"
2) 1) "TianAnMen"
   2) "5.0450"
   3) (integer) 4069885555136394
   4) 1) "116.40441387891769"
      2) "39.915046196472751"

GEORADIUSBYMEMBER

自3.2.0可用。

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

语法:GEORADIUSBYMEMBER key member radius m|km|ft|mi[WITHCOORD][WITHDIST][WITHHASH][COUNT count][ASC|DESC][STORE key][STOREDIST key]
说明:

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

关于 GEORADIUSBYMEMBER 命令的更多信息, 请参考 GEORADIUS 命令的文档。

返回值:

一个数组, 数组中的每个项表示一个范围之内的位置元素。

示例:
 coderknock>  GEORADIUSBYMEMBER tour TianAnMen  100 km
1) "XiangShan"
2) "BeijingFilmAcademy"
3) "ChairmanMaoZedongMemorialHall"
4) "TianAnMen"

删除地理位置信息

GEO 没有提供删除成员的命令,但是因为 GEO 的底层实现是 zset ,所以可以借用 ZREM 命令实现对地理位置信息的删除。

查询全部 GEO

GEO 没有提供查询全部成员的命令,但是因为 GEO 的底层实现是 zset ,所以可以借用 ZRANGE 等命令实现对地理位置信息的查询。

 coderknock> ZRANGE tour 0 -1 WITHSCORES
1) "XiangShan"
2) "4069880147829102"
3) "BeijingFilmAcademy"
4) "4069880657711258"
5) "ChairmanMaoZedongMemorialHall"
6) "4069885371962385"
7) "TianAnMen"
8) "4069885555136394"