es区域查找底层原理

Api如何调用

  • 映射
PUT /test
{"mappings":{"properties":{"name":{"type":"text"},"location":{"type":"geo_point"}}}
}
  • 插入数据
PUT /test/_doc/1
## 字符串形式
{"name":"NetEase","location":"40.715,74.011"
}
## 对象形式
{"name":"Sina","location":{"lat":40.722,"lon":73.989}
}
## 数组形式
{"name":"Baidu","location":[73.983,40.719]
}

字符串形式以半角逗号分割,如 “lat,lon”
对象形式显式命名为 lat 和 lon
数组形式表示为 [lon,lat]

  • 查询
过滤器 作用
geo_bounding_box 找出落在指定矩形框中的点
geo_distance 找出与指定位置在给定距离内的点
geo_distance_range 找出与指定点距离在给定最小距离和最大距离之间的点
geo_polygon 找出落在多边形中的点。
  • 常用查询
  1. geo_bounding_box

指定一个矩形的顶部 ,底部 , 左边界和右边界,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间

{"query":{"bool":{"must":{"match_all":{}},"filter":{"geo_bounding_box":{"location":{"top_left":{"lat":40.73,"lon":71.12},"bottom_right":{"lat":40.01,"lon":74.1}}}}}}
}
  1. geo_distance

过滤仅包含与地理位置相距特定距离内的匹配的文档。

{"query":{"bool":{"must":{"match_all":{}},"filter":{"geo_distance":{"distance":"200km","location":{"lat":40,"lon":70}}}}}
}

原理分析

kd树思想

什么是kd树

  • k-dimensional(维度)树
  • 分割k维数据空间的数据结构
  • 是一颗二叉树
  • 切分维度上,左子数值小于右子树

kd树是怎么构建的

地点 经度 维度
自如 3 199
贝壳 2 132
链家 5 87
德祐 65 12
望京soho 127 14
将台 114 3
步骤一:
数据维度k=2
划分维度= 0
1.对划分维度的数据进行排序
[2,132],[3,199],[5,87],[65,12],[114,3],[127,14]
2.取中位数
index =  6/2 = 3
[65,12]

步骤二:
数据维度k=2
划分维度=(0+1%2)=1

步骤三:
数据维度k=2
划分维度=(1+1%2)=0

换种方式理解kd树的构建

在一个二维的平面当中分布着若干个点。

我们首先选择一个维度将这些数据一分为二,比如我们选择x轴。我们对所有数据按照x轴的值排序,选出其中的中点进行一分为二。

在这根线左右两侧的点被分成了两棵子树,对于这两个部分的数据来说,我们更换一个维度,也就是选择y轴进行划分。一样,我们先排序,然后找到中间的点,再次一分为二。我们可以得到:

我们重复上述过程,一直将点分到不能分为止

转成树就变成了这样

kd树怎么搜索的


以查询(2.1,3.1)为例:

  1. 二叉树搜索:先从(7,2)点开始进行二叉查找,然后到达(5,4),最后到达(2,3),此时搜索路径中的节点为<(7,2),(5,4),(2,3)>,首先以(2,3)作为当前最近邻点,计算其到查询点(2.1,3.1)的距离为0.1414,
  2. 回溯查找:在得到(2,3)为查询点的最近点之后,回溯到其父节点(5,4),并判断在该父节点的其他子节点空间中是否有距离查询点更近的数据点。以(2.1,3.1)为圆心,以0.1414为半径画圆,如下图所示。发现该圆并不和超平面y = 4交割,因此不用进入(5,4)节点右子空间中(图中灰色区域)去搜索;
  3. 最后,再回溯到(7,2),以(2.1,3.1)为圆心,以0.1414为半径的圆更不会与x = 7超平面交割,因此不用进入(7,2)右子空间进行查找。至此,搜索路径中的节点已经全部回溯完,结束整个搜索,返回最近邻点(2,3),最近距离为0.1414。


一个复杂点了例子如查找点为(2,4.5),具体步骤依次如下:

  1. 同样先进行二叉查找,先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,由于查找点为y值为4.5,因此进入右子空间查找到(4,7),形成搜索路径<(7,2),(5,4),(4,7)>,但(4,7)与目标查找点的距离为3.202,而(5,4)与查找点之间的距离为3.041,所以(5,4)为查询点的最近点;
  2. 以(2,4.5)为圆心,以3.041为半径作圆,如下图所示。可见该圆和y = 4超平面交割,所以需要进入(5,4)左子空间进行查找,也就是将(2,3)节点加入搜索路径中得<(7,2),(2,3)>;于是接着搜索至(2,3)叶子节点,(2,3)距离(2,4.5)比(5,4)要近,所以最近邻点更新为(2,3),最近距离更新为1.5;
  3. 回溯查找至(5,4),直到最后回溯到根结点(7,2)的时候,以(2,4.5)为圆心1.5为半径作圆,并不和x = 7分割超平面交割,如下图所示。至此,搜索路径回溯完,返回最近邻点(2,3),最近距离1.5。
  • 再举个例子,思考下这种情况。寻找s最近的点,是个怎样的过程。

Redis是如何实现区域查找的?

Redis区域查找相关API

  • geoadd:添加经纬度坐标和对应地理位置名称。
  • geopos:获取地理位置的经纬度坐标。
  • geodist:计算两个地理位置的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:计算一个或者多个经纬度坐标点的geohash值。

Api案例

geoadd

geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
geoadd 语法格式如下:

GEOADD key longitude latitude member [longitude latitude member …]

redis> GEOADD Sicily 13.361389 38.115556 "BeiJing" 15.087269 37.502669 "ShangHai"
(integer) 2
redis> GEODIST Sicily BeiJing ShangHai
"166274.1516"
redis> GEORADIUS Sicily 15 37 100 km
1) "BeiJing"
redis> GEORADIUS Sicily 15 37 200 km
1) "BeiJing"
2) "ShangHai"
redis>

geopos
geopos 用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
geopos 语法格式如下:

GEOPOS key member [member …]

redis> GEOADD Sicily 13.361389 38.115556 "BeiJing" 15.087269 37.502669 "ShangHai"
(integer) 2
redis> GEOPOS Sicily BeiJing ShangHai NonExisting
1) 1) "13.36138933897018433"2) "38.11555639549629859"
2) 1) "15.08726745843887329"2) "37.50266842333162032"
3) (nil)
redis>

geodist
geodist 用于返回两个给定位置之间的距离。
geodist 语法格式如下:

GEODIST key member1 member2 [m|km|ft|mi]
member1 member2 为两个地理位置。

最后一个距离单位参数说明:

m :米,默认单位。
km :千米。
mi :英里。
ft :英尺

redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEODIST Sicily Palermo Catania
"166274.1516"
redis> GEODIST Sicily Palermo Catania km
"166.2742"
redis> GEODIST Sicily Palermo Catania mi
"103.3182"
redis> GEODIST Sicily Foo Bar
(nil)
redis>

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

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

georadius 与 georadiusbymember 语法格式如下:

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

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

redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUS Sicily 15 37 200 km WITHDIST
1) 1) "Palermo"2) "190.4424"
2) 1) "Catania"2) "56.4413"
redis> GEORADIUS Sicily 15 37 200 km WITHCOORD
1) 1) "Palermo"2) 1) "13.36138933897018433"2) "38.11555639549629859"
2) 1) "Catania"2) 1) "15.08726745843887329"2) "37.50266842333162032"
redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"2) "190.4424"3) 1) "13.36138933897018433"2) "38.11555639549629859"
2) 1) "Catania"2) "56.4413"3) 1) "15.08726745843887329"2) "37.50266842333162032"
redis>

参数说明:

m :米,默认单位。
km :千米。
mi :英里。
ft :英尺。
WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
WITHCOORD: 将位置元素的经度和维度也一并返回。
WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
COUNT 限定返回的记录数。
ASC: 查找结果根据距离从近到远排序。
DESC: 查找结果根据从远到近排序。

redis> GEOADD Sicily 13.583333 37.316667 "Agrigento"
(integer) 1
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km
1) "Agrigento"
2) "Palermo"
redis>

GeoHash

geohash是2008年Gustavo Niemeye发明用来编码经纬度信息的一种编码方式,比如北京市中心的经纬度坐标是116.404844,39.912279,通过12位geohash编码后就变成了wx4g0cg3vknd,它究竟是如何实现的?其实原理非常简单,就是二分,整个编码过程可以分为如下几步

  1. 转二进制

上过初中地理的我们都知道,地球上如何一个点就可以标识为某个经纬度坐标,经度的取值范围是东经0-180度和西经0-180度,维度的取值范围是北纬0到90和南纬0-90度。去掉东西南北,可以分别认为经度和维度的取值范围为[-180,180]和[-90,90]。
我们先来看经度,[-180,180]可以简单分成两个部分[-180,0]和[0,180],对于给定的一个具体值,我们用一个bit来标识是在[-180,0]还是[0,180]区间里。然后我们可以对这两个子区间继续细分,用更多的bit来标识是这个值是在哪个子区间里。就好比用二分查找,记录下每次查找的路径,往左就是0往右是1,查找完后我们就会得到一个0101的串,这个串就可以用来标识这个经度值。
同理维度也是一样,只不过他的取值返回变成了[-90,90]而已。通过这两种方式编码完成后,任意经纬度我们都可以得到两个由0和1组成的串。
比如还是北京市中心的经纬度坐标 116.404844,39.912279,我们先对116.404844做编码,得到其二进制为:

11010010110001101101

然后我们对维度39.912279编码得到二进制为

10111000110000111001

  1. 经纬度二进制合并

接下来我们只需要将上述二进制交错合并成一个即可,这里注意经度占偶数位,纬度占奇数位,得到最终的二进制。

1101101110000200111100000001111011010011

  1. 将合并后的二进制做base32编码

最后我们将合并后的二进制做base32编码,将连续5位转化为一个0-31的十进制数,然后用对应的字符代替,将所有二进制位处理完后我们就完成了base32编码。编码表如下:

最终得到geohash值wx4g0cg3vknd。

geohash是将空间不断的二分,然后将二分的路径转化为base32编码,最后保存下来,从原理可以看出,geohash表示的是一个区间,而不是一个点,geohash值越长,这个区间就越小,标识的位置也就越精确,下图是维基百科中不同长度geohash下的经纬度误差(lat:维度,lng:经度)

geohash的用途及问题

geohash成功的将一个二维信息编码成了一个一维信息,这样编码我觉得有两个好处:1. 编码后数据长度变短,利于节省存储。2. 利于使用前缀检索。我们来详细说下第二点。

从上文中geohash的实现来看,只要两个坐标点的geohash有共同的前缀,你们我们就可以肯定这两个点在同一个区域内 (区域大小取决于共同前缀的长度)。这种特性给我们带来的好处就是,我们可以把所有坐标点按geohash做增序索引,然后查找的时候按前缀筛选,大幅提升检索的性能。

举个例子,假设我要找北京国贸附近3公里内的餐馆,已知国贸的geohash是wx4g41,那我也很轻易就可以计算出来我需要扫描哪些区域内的点。但有个点需要注意,上文我已经提到过,geohash值实际上是代表一个区域,而不是一个点,找到一批候选点之后还需要遍历一次计算下精确距离。

Redis Geo源码

geoadd

/* GEOADD key [CH] [NX|XX] long lat name [long2 lat2 name2 ... longN latN nameN] */
void geoaddCommand(client *c) {int xx = 0, nx = 0, longidx = 2;int i;/* 解析可选参数 */while (longidx < c->argc) {char *opt = c->argv[longidx]->ptr;if (!strcasecmp(opt,"nx")) nx = 1;else if (!strcasecmp(opt,"xx")) xx = 1;else if (!strcasecmp(opt,"ch")) {}else break;longidx++;}if ((c->argc - longidx) % 3 || (xx && nx)) {/* 解析所有的经纬度值和member,并对其个数做校验 */addReplyErrorObject(c,shared.syntaxerr);return;}/* 构建zadd的参数数组 */int elements = (c->argc - longidx) / 3;int argc = longidx+elements*2; /* ZADD key [CH] [NX|XX] score ele ... */robj **argv = zcalloc(argc*sizeof(robj*));argv[0] = createRawStringObject("zadd",4);for (i = 1; i < longidx; i++) {argv[i] = c->argv[i];incrRefCount(argv[i]);}/* 以3个参数为一组,将所有的经纬度和member信息从参数列表里解析出来,并放到zadd的参数数组中 */for (i = 0; i < elements; i++) {double xy[2];if (extractLongLatOrReply(c, (c->argv+longidx)+(i*3),xy) == C_ERR) {for (i = 0; i < argc; i++)if (argv[i]) decrRefCount(argv[i]);zfree(argv);return;}/* 将经纬度坐标转化成score信息 */GeoHashBits hash;geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);GeoHashFix52Bits bits = geohashAlign52Bits(hash);robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));robj *val = c->argv[longidx + i * 3 + 2];argv[longidx+i*2] = score;argv[longidx+1+i*2] = val;incrRefCount(val);}/* 转化成zadd命令所需要的参数格式*/replaceClientCommandVector(c,argc,argv);zaddCommand(c);
}

georadius

void georadiusGeneric(client *c, int srcKeyIndex, int flags) {robj *storekey = NULL;int storedist = 0; /* 0 for STORE, 1 for STOREDIST. *//* 根据key找找到对应的zojb */robj *zobj = NULL;if ((zobj = lookupKeyReadOrReply(c, c->argv[srcKeyIndex], shared.emptyarray)) == NULL ||checkType(c, zobj, OBJ_ZSET)) {return;}/* 解析请求中的经纬度值 */int base_args;GeoShape shape = {0};if (flags & RADIUS_COORDS) {/** 各种必选参数的解析,省略细节代码,主要是解析坐标点信息和半径   */ }/* 解析所有的可选参数. */int withdist = 0, withhash = 0, withcoords = 0;int frommember = 0, fromloc = 0, byradius = 0, bybox = 0;int sort = SORT_NONE;int any = 0; /* any=1 means a limited search, stop as soon as enough results were found. */long long count = 0;  /* Max number of results to return. 0 means unlimited. */if (c->argc > base_args) {/** 各种可选参数的解析,省略细节代码   */ }/* Get all neighbor geohash boxes for our radius search* 获取到要查找范围内所有的9个geo邻域 */GeoHashRadius georadius = geohashCalculateAreasByShapeWGS84(&shape);/* 创建geoArray存储结果列表 */geoArray *ga = geoArrayCreate();/* 扫描9个区域中是否有满足条的点,有就放到geoArray中 */membersOfAllNeighbors(zobj, georadius, &shape, ga, any ? count : 0);/* 如果没有匹配结果,返回空对象 */if (ga->used == 0 && storekey == NULL) {addReply(c,shared.emptyarray);geoArrayFree(ga);return;}long result_length = ga->used;long returned_items = (count == 0 || result_length < count) ?result_length : count;long option_length = 0;/* * 后续一些参数逻辑,比如处理排序,存储……*/// 释放geoArray占用的空间 geoArrayFree(ga);
}

Redis Geo流程总结

  1. 解析请求参数。
  2. 计算目标坐标所在的geohash和8个邻居。
  3. 在zset中查找这9个区域中满足距离限制的所有点集。
  4. 处理排序等后续逻辑。
  5. 清理临时存储空间。

Es区域查找底层原理vsRedis Geo区域查找的实现区别相关推荐

  1. iOS之深入解析Runtime的objc_msgSend“慢速查找”底层原理

    CacheLookup 快速查找 objc_msgSend 通过汇编 快速查找方法缓存 ,如果能找到则调用 TailCallCachedImp 直接将方法缓存起来然后进行调用,如果查找不到就跳到 Ch ...

  2. iOS之深入解析Runtime的objc_msgSend“快速查找”底层原理

    Runtime 一.什么是 runtime ? Objective-C 语言将尽可能多的决策从 编译时和链接时 推迟到运行时.只要有可能,它就 动态 地做事情,这意味着该语言不仅需要一个编译器,还需要 ...

  3. 【java并发编程】底层原理——用户态和内核态的区别

    一.背景--线程状态切换的代价 java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都 ...

  4. elasticsearch原理_ElasticSearch读写底层原理及性能调优

    ES写入/查询底层原理 1. Elasticsearch写入数据流程 客户端随机选择一个ES集群中的节点,发送POST/PUT请求,被选择的节点为协调节点(coordinating node) 协调节 ...

  5. iOS之深入解析objc_msgSend消息转发机制的底层原理

    一.抛砖引玉 objc_msgSend() 消息发送的过程就是 通过 SEL 查找 IMP 的过程 . objc_msgSend() 是用 汇编语言 实现的,使用汇编实现的优势是: 消息发送的过程需要 ...

  6. elasticsearch最大节点数_ElasticSearch读写底层原理及性能调优

    ES写入/查询底层原理 1. Elasticsearch写入数据流程 客户端随机选择一个ES集群中的节点,发送POST/PUT请求,被选择的节点为协调节点(coordinating node) 协调节 ...

  7. 四种比较简单的图像显著性区域特征提取方法原理及实现

    四种比较简单的图像显著性区域特征提取方法原理及实现-----> AC/HC/LC/FT. laviewpbt  2014.8.4 编辑 Email:laviewpbt@sina.com   QQ ...

  8. 【ES】ElasticSearch搜索的底层原理?倒排索引和TF-IDF打分算法

    Elasticsearch搜索的底层原理 ES搜索是分词后,每个字可以利用FST高速找到倒排索引的位置,并迅速获取文档id列表,大大的提升了性能,减少磁盘IO. ES的搜索原理就是倒排索引 + TF- ...

  9. OpenCV后投影,利用阈值函数分割指定区域生成掩膜,通过直方图分布查找其他图像相同区域。

    一.API函数 void mixChannels(const Mat* src,int nsrc,Mat* dst ,int ndst,const int* fromTo,size_t npairs) ...

最新文章

  1. 十分钟成为 Contributor 系列 | 为 TiDB 重构 built-in 函数
  2. git用法小结(2)--git分支
  3. sql floor 取整函数
  4. Vue实现仿音乐播放器4-Vue-router实现音乐导航菜单切换
  5. Pandas 表格样式设置指南,看这一篇就够了!
  6. python画卡通人物用什么_干啥啥不行,吹牛第一名——Python头像动漫化,快来用女朋友照片生成一个动漫头像吧...
  7. C++一天一个程序(七)
  8. React开发(131):ant design学习指南之form中的resetFields
  9. Android官方开发文档Training系列课程中文版:构建第一款安卓应用之程序运行
  10. php chr 乱码,php chr() ord()中文截取乱码问题解决方法_PHP教程
  11. LeetCode-3. 无重复字符的最长子串
  12. spring boot 1.5.4 整合redis、拦截器、过滤器、监听器、静态资源配置(十六)
  13. Android 启动多个闹钟。
  14. X Lossless Decoder for mac(XLD音频无损解码器)
  15. makefile编写---:= ?= += =的区别
  16. HDFS原理(超详解)
  17. 微波工程(2)——传输线理论
  18. 靶场练习之hackinglab(鹰眼)-基础题
  19. FME中的栅格数据操作之十二——矢量数据栅格化
  20. 删除文件时提示:无法读源文件或磁盘之解决办法

热门文章

  1. 要不要去北京上海发展?
  2. python wpa_wpa_passphrase
  3. 被上司坑,怕不怕? 往死里整那种
  4. 电子烟TPD注册操作流程
  5. 波士顿矩阵(明星,金牛,问题,搜狗)
  6. 50万奖补!湖北省2022年大学生创业扶持项目申报要求以及申报流程
  7. 复盘——一年经历获得十年经验
  8. 一个招聘网站的详细部件设计
  9. ig夺冠后服务器不稳定,LOL官方为iG夺冠活动庆典道歉:服务器不稳导致奖励发布延迟...
  10. matex2怎么升级鸿蒙,Mate X2怎么升级鸿蒙系统 Mate X2升级鸿蒙系统步骤教程