首先声明下,这是一篇学习笔记,来自极客时间陈东老师《检索核心技术20讲》,讲的很不错,有些问题的处理算法常常让我感觉耳目一新,这次聊的空间搜索算法就是其中之一。
一 空间搜索的需求
我们在使用地图的时候,常常会使用地图搜索附近的酒店,附近的美食店,甚至附近的WC等功能。这些还好说,以为酒店这些的位置是固定的,只要拿搜索区域内酒店的经纬度和我们所在位置经纬度做计算,计算出距离,依次计算不同酒店的位置,然后再做个排序就得出了我们附近酒店排行TOP N。
我们在用微信的话,想必对附近的人的功能,并不陌生,那么我们如何计算我们附近的人,这里面和酒店的差别是,人员是流动的,计算起来很麻烦。如果拿在线的人和我们现在的人做一对一的位置计算,再进行排序,显然不够明知,虽然我们不知道附近到底是多远的距离叫附近,但是一般来说不同的城市不能算附近(当然边界上可能更近,后面再说),所以我们在计算距离的时候,只要计算同城的附近人就可以了,这样我们可以很大的减少计算量。
再深入一下,如果我们附近的范围更小点,我们可以减少到计算一个区的在线用户和我们位置的距离,其实像这种计算,我们只要取最近的top n就可以了,有时候没有必要严格排序。
有了缩小区域的思路,我们就来看下如何来求计算距离能更快速,更合适。
二 区域编码
为了计算距离,一般我们用经纬度表示位置,我们要把计算的空间划分不同的区域,每个区域用编码标识,比如我们把一个城市按照经纬度分为四个区域分别用编码:00,01,11,10来表示,如果继续划分如下:
区域编码
如果我们需要更精准的距离,可以进一步来划分,编码的位数变为4位了,也更精确了。这样编码的方式来划分,相同的区域的前缀编码是相同的,这有利于我们做区域查询,比如我们在一个区域内查不到的时候,可以把范围再扩大的一个区域。
2.1 解决附近查询误差的问题
前文有说,如果我们计算附近的人的时候,按照一个区域内的人去搜索,减少了计算的量,但是还有个问题,就是区域内的人,也可能不是离我们近的人,相临区域的人有可能距离更近,如下图:
附近人问题
说明,在上图中,绿色的三角符号表示本人位置,绿色的圆圈和绿色三角符号属于同一个区域,但是距离更近的反而是相邻的区域。
对于这个问题,假设我们认为的附近区域是10km,如果我们只在此区域搜索,显然可能会漏掉更近的距离。我们区域扩大一倍,从搜索1个区域到搜索了9个区域,多了8个附近的区域,这样就不会有遗漏了。
2.2 Geohash 编码
刚才是我们按照自己的理解去进行区域编码,如果我们将地球看作一个大的二维空间,经度显然是水平方向,维度就是垂直方向,地球的经度范围是[-180,180] ,维度的区间[-90,90]; 假设以(经度:104.07 纬度:30.67)这个位置来进行编码:
- 经度方向上,104.07 经度在0-180之间,我们将空间右半部分编码为1,在180度的范围继续一分为二,104.07在90度到180之间,继续编码为1,所以在经度上编码为:11。
- 维度方向上,30.67维度在0-90范围编码为1,继续划分,30.67在0-45范围,继续编码为0,故此在维度上的编码为:10
- 综合来看上述位置的编码为:1110 (先经度后维度)。
我们一共才用4位编码,所以显然粒度很粗,如果想精细化,用更多位数来表示,我们就通过这种编码的方式,将整个空间的二维的编码转成了一维的数字,计算操作起来更简单了。
如果我们将经度用15个bit来表示,维度位15个bit来表示,那么和起来就是30个bit,编码很长,我们最后使用用0-9、b-z(去掉a, i, l, o)这32个字母,对01串进行base32编码编码的方式,将5个bit转成一个字母和数字就形成类似:"w3qcef",这样30位编码就转成了6位字符串。这种用数字和字符来表示位置的编码方式叫GeoHash 编码。
base32编码 图片来自互联网
9位Geohash编码就是45bit,即可以精确到4.8米,10位可以精确到1米左右。网上有对应表格,可以根据我们的精度要求来选择Geohash编码的位数。
缺点:Geohash编码1个字符或数字就表示经维度,可能我们用的时候,比如我们需要精度为3米的时候,采用9位的话精度不够,采用10位的精度又太过精确用不到。这时候需要换编码方式或者直接采用原始的01串表示。
三 实践
常用的全文搜索引擎,比如Solr或ES都支持位置搜索,以Solr为例,solr支持多种距离字段定义,常用的配置如下:
<!-- solr4.0以上版本有默认的字段类型定义 可能有细微差别-->
<fieldType name="location" class="solr.LatLonPointSpatialField" docValues="true"/>
<fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType" geo="true" />
新建些测试文档:
<add>
<doc>
<field name = "id">001</field>
<field name = "city_s">成都</field>
<field name = "location_p">30.67,104.07</field>
</doc>
<doc>
<field name = "id">002</field>
<field name = "city_s">南京</field>
<field name = "location_p">32.07,118.78 </field>
</doc>
<doc>
<field name = "id">003</field>
<field name = "city_s">重庆</field>
<field name = "location_p">29.57,106.56</field>
</doc>
<doc>
<field name = "id">004</field>
<field name = "city_s">西安</field>
<field name = "location_p">34.27,108.93</field>
</doc>
</add>
此处采用动态字段形式,定义city_s即为string类型,location_p为location类型。
查询下结果如下:
结果验证
Solr支持两种查询分析器,其实差不多,分别为:geofit查询分析器和bbox查询分析器
3.1 Geofit查询分析器查询附近数据
geofit过滤器可以根据地理空间距离从给定点做一个圆形的过滤,查询5公里范围内的数据,命令如下:
fq={!geofilt sfield=location_p pt=30.67,104.05 d=1000}
sfield: 保存位置的字段名。
pt :初始位置
d: 附近距离,单位千米
结果展示:
查询结果
Geofit采用两步求结果:
- 按照精度要求,创建一个正方形边框,正方形的边长为搜索的距离,通过这个正方形过滤文档。
-
计算边框内的位置和中心的距离,然后进行排序,且要过滤掉以距离为圆的圆外数据。
但是如果数据量很大,那么精确的圆过滤没有必要,那就可以采用bbox查询器。
Geofit计算
3.2 bbox查询分析器查询附近数据
bbox过滤器与geofit非常相似,只要它使用要计算的圆的边界框,它采用与geofit相同的参数:
fq= {!bbox sfield=location_p pt=30.67,104.05 d=1000}
由于不做精确过滤,所以速度更快,结果我就不贴了。
3.3 距离排序
我们从刚才查询结果来看,离的近的排在前面,那么有没有办法计算这个距离的,solr中有相关函数的,且支持排序:
fl = id,city_s,distance:geodist(location_p,30.67,104.05) &
sort=geodist(location_p,30.67,104.05)asc,score desc
查询就且排序
就说到这里吧。
四 共享诗词
# 江城子·前瞻马耳九仙山
[宋] [苏轼]
前瞻马耳九仙山。
碧连天。晚云间。
城上高台,真个是超然。
莫使匆匆云雨散,今夜里,月婵娟。
小溪鸥鹭静联拳。去翩翩。点轻烟。
人事凄凉,回首便他年。
莫忘使君歌笑处,垂柳下,矮槐前。
网友评论