Redis系列四 GEO附近的人

夫大人者,与天地合其德,与日月合其明,与四时合其序,与鬼神合其吉凶。(《周易·䷀乾·文言》)

GEO算法

GeoHash是一种地址编码方法。将二维的空间经纬度数据编码成一个字符串

地球上的经度范围:[-180, 180],纬度范围:[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。

我们先将平面切割成四个正方形,然后用简单的 01 编码来标识这个四个正方形,最后按照编码的大小将四个正方形连接起来,这样整个平面就转换成了一条Z曲线,变成了一维。
我们递归对每个正方形做同样的操作,递归的层次越深,整个平面就逐渐被Z曲线填充。我们的点也会落在每个小正方形里面,小正方形越小,精度就越高。如下图所示:

第一步: 经纬度转二进制

比如这样一个点(39.923201, 116.390705)

在区间内就是1,否则就是0
依次计算得到二进制数:

39.923201 => 10111000110001111001
116.390705 => 11010010110001000100

第二步: 经纬度合并

经度占偶数位,纬度占奇数位,注意,0也是偶数位。

11100 11101 00100 01111 00000 01101 01011 00001

第三步: Base32编码

二进制=>十进制=>进行编码即可
wx4g0ec1ebpf

可以在这个网址互相转换,http://geohash.co/;

注意

geohash

  1. GeoHash表示的并不是一个点,而是一个矩形区域
  2. GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索
  3. 编码越长,表示的范围越小,位置也越精确

边缘问题

如图,如果车在红点位置,区域内还有一个黄点。相邻区域内的绿点明显离红点更近。但因为黄点的编码和红点一样,最终找到的将是黄点。这就有问题了。

要解决这个问题,很简单,只要再查找周边8个区域内的点,对比距离即可

曲线突变问题

其中0111和1000两个编码非常相近,但它们的实际距离确很远。所以编码相近的两个单位,并不一定真实距离很近,这需要实际计算两个点的距离才行。

iordis代码实现


import * as Redis from "ioredis";

const redis = new Redis();

(async function(){
const key = 'geo:zhubo';
// 1. geoadd 添加 ABC三元素&对应的经纬度
// @ts-ignore
await redis.geoadd(key, [116.48, 39.9, 'A', 116.9, 39.95, 'B', 116.83, 39.01, 'C']);
// @ts-ignore
const dist = await redis.geodist(key, 'A', 'B', 'km');
console.log(`AB之间的距离为${dist}km`);
// @ts-ignore
const pos = await redis.geopos(key, 'A');
console.log(`A的经纬度为${pos}`);
// @ts-ignore
const hash = await redis.geohash(key, 'A');
console.log(`A的hash为${hash}`);
// @ts-ignore
const GEORADIUSBYMEMBER = await redis.georadiusbymember(key, 'A', 300, 'km', 'COUNT', 4, 'asc')
// @ts-ignore
const GEORADIUSBYMEMBER1 = await redis.georadiusbymember(key, 'A', 300, 'km', 'withcoord', 'withdist', 'withhash', 'COUNT', 4, 'asc')
console.log(`A的附近300km, 距离由近到远为${GEORADIUSBYMEMBER}`);
console.log(`A的附近300km, 距离由近到远为${GEORADIUSBYMEMBER1}`);
// @ts-ignore
const GEORADIUS = await redis.georadius(key, 116.67, 39.5, 50, 'km', 'withdist', 'count', 3, 'asc');
console.log(`距离「116.67, 39.5」附近「50」km内, 距离由近到远前3位为${GEORADIUS}`);
process.exit(1);
})()
4附近的人 git:(main) ✗ ts-node index.ts
AB之间的距离为36.2543km
A的经纬度为116.47999852895736694,39.90000009167092543
A的hash为wx4ffxd4ke0
A的附近300km, 距离由近到远为A,B,C
A的附近300km, 距离由近到远为A,0.0000,4069885894809634,116.47999852895736694,39.90000009167092543,B,36.2543,4069982235196126,116.90000027418136597,39.9500000012600438,C,103.4539,4069174206137433,116.82999998331069946,39.01000119404034905
距离「116.67, 39.5」附近「50」km内, 距离由近到远前3位为A,47.3686
# GEOADD: 添加位置 hset结构 
127.0.0.1:6379> GEOADD zhubo 116.48 39.9 A
(integer) 1
127.0.0.1:6379> GEOADD zhubo 116.9 39.95 B
(integer) 1
127.0.0.1:6379> GEOADD zhubo 116.83 39.01 C
(integer) 1
127.0.0.1:6379> GEOADD zhubo 116.67 39.44 D
(integer) 1
# GEODIST 两个key 之间的距离 km m
127.0.0.1:6379> GEODIST zhubo A B km
"36.2543"
127.0.0.1:6379> GEODIST zhubo A A km
"0.0000"
# GEOPOS 输出某个key的信息
127.0.0.1:6379> GEOPOS zhubo A
1) 1) "116.47999852895736694"
2) "39.90000009167092543"
127.0.0.1:6379> GEOPOS zhubo A B
1) 1) "116.47999852895736694"
2) "39.90000009167092543"
2) 1) "116.90000027418136597"
2) "39.9500000012600438"
# GEOHASH 输出对应的hash值
127.0.0.1:6379> GEOHASH zhubo A
1) "wx4ffxd4ke0"

# GEORADIUSBYMEMBER 距离某个key xxkm,限制number个元素 生序排列
127.0.0.1:6379> GEORADIUSBYMEMBER zhubo A 300 km count 4 asc
1) "A"
2) "B"
3) "D"
4) "C"
127.0.0.1:6379> GEORADIUSBYMEMBER zhubo A 300 km count 4 desc
1) "C"
2) "D"
3) "B"
4) "A"
# GEORADIUSBYMEMBER 附加 withcoord withdist withhash
127.0.0.1:6379> GEORADIUSBYMEMBER zhubo A 300 km withcoord withdist withhash count 4 asc
1) 1) "A"
2) "0.0000"
3) (integer) 4069885894809634
4) 1) "116.47999852895736694"
2) "39.90000009167092543"
2) 1) "B"
2) "36.2543"
3) (integer) 4069982235196126
4) 1) "116.90000027418136597"
2) "39.9500000012600438"
3) 1) "D"
2) "53.6879"
3) (integer) 4069136689844544
4) 1) "116.67000085115432739"
2) "39.43999889567408701"
4) 1) "C"
2) "103.4539"
3) (integer) 4069174206137433
4) 1) "116.82999998331069946"
2) "39.01000119404034905"
# GEORADIUS 距离某个点,附近信息
127.0.0.1:6379> GEORADIUS zhubo 116.67 39.5 50 km withdist count 3 asc
1) 1) "D"
2) "6.6737"
2) 1) "A"
2) "47.3686"

参考

Geohash算法原理及实现