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