基于Solr的空间搜索学习笔记
在Solr中基于空間地址查詢主要圍繞2個概念實現:
(1) Cartesian Tiers 笛卡爾層
Cartesian Tiers是通過將一個平面地圖的根據設定的層次數,將每層的分解成若干個網格,如下圖所示:
?
??? 每層以2的評方遞增,所以第一層為4個網格,第二層為16 個,所以整個地圖的經緯度將在每層的網格中體現:
?
?
笛卡爾層在Lucene中對空間地理位置查詢最大的用處在查找周邊地址的時候有效的減少查詢量,即將查詢量可以控制在分層后最小的網格中的若干docId。
那么如何構建這樣的索引結構呢,其實很簡單,只需要對應笛卡爾層的層數來構建域即可。
也即是tiers0->field_0,tiers1->field_1,tiers2-field_2,……,tiers19->field_19。(一般20層即可)。每個對應笛卡爾層次的域將根據當前這條記錄的經緯度通過笛卡爾算法計算出歸屬于當前層的網格,然后將gridId(網格唯一標示)以term的方式存入索引。這樣每條記錄關于笛卡爾0-19的域將都會有一個gridId對應起來。但是查詢的時候一般是需要查周邊的地址,那么可能周邊的范圍超過一個網格的范圍,那么實際操作過程是根據經緯度和一個距離確定出需要涉及查詢的從19-0(從高往低查,留給讀者思考)若干層對應的若干網格的數據(關于代碼實現在后面的文章內容闡述)。那么一個經緯度周邊地址的查詢只需要如下圖圓圈內的數據:
所以通過這樣的數據過濾,將極大的減少計算量。
(2) GeoHash算法
在Lucene索引中將經緯度的二維坐標通過geohash,變成一個一維的字符串base32的坐標,例如,經緯度對應一個base32的坐標為DRT2Y,那這個base32的字符串什么意思呢,
其實編碼中每個字符都是代表一個區域,并且前面的字符是后面字符的父區域,即R是D區域內的子區域,T又為D區域的子區域,大家可以從如下圖片獲得base32的層級關系(以下圖片均來自互聯網):
?
進入D區域,則看到又分為若干區域,而R為其子區域:
?
繼續進入R區域,可以繼續看到有子區域T區域:
?
而2Y也是基于以上的關系類推,所以一個base32的編碼是標示一個區域,而編碼過程中會根據經緯度的精度來確定這個區域大小。從上面的解釋大家肯定會想到編碼的前綴是表示更大的區域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內的更大區域。所以根據這個特點,利用模糊查詢是可以達到一種附近地點的查詢。
geohash算法實現其實非常簡單,網上有很多例子,在這里借用下這些例子再加上比較詳細的說明。基本算法流程是基于多輪的收斂,以達到滿足精度要求為止。具體流程以(39.92324 緯度, 116.3906 經度)為例,首先將緯度的范圍(-90, 90)平分成兩個區間(-90, 0)、(0, 90),如果目標緯度位在(-90,0),則編碼為0,在(0,90)則編碼為1。由于上面的例子中維度39.92324是屬于(0, 90),所以第一輪獲得的編碼位取1。接下來再將(0, 90)分成 (0, 45), (45, 90)兩個區間,而39.92324位于(0, 45),所以編碼為0。以此類推,直到精度符合要求為止,如下圖所示:
?
所以通過16輪的計算后得到經度39.92324的編碼為:1011 1000 1100 0111 1001
經度也用同樣的算法,對(-180, 180)多輪的依次細分計算:
?
得到經度116.3906的編碼為1101 0010 1100 0100 0100
經緯度的編碼都計算完畢后,接下來就需要合并經緯度的編碼,規則是以經度開始,依次每次取一位合并成5位的新編碼,如上圖紅色字標示順序所示:
?
完成合并編碼后就需要將該編碼和base32編碼表對應起來,做法是每5位為一個十進制數,以11100為例,它的十進制數是28,所以對應的base32編碼表示W,如下圖所示:
?
其他的五位編碼依次從表中找到對應位置后,(39.92324 緯度, 116.3906 經度)的base32編碼為:wx4g0ec1
解碼算法與編碼算法相反,先進行base32解碼,然后分離出經緯度,最后根據二進制編碼對經緯度范圍進行細分即可,這里不再贅述。不過由于geohash表示的是區間,編碼越長越精確,但不可能解碼出完全一致的地址
關于Solr+Lucene使用Cartesian Tiers 笛卡爾層和GeoHash的構建索引和查詢的細節介紹將在新的Blog中闡述。
在Solr中其實支持很多默認距離函數,但是基于坐標構建索引和查詢的主要會基于2種方案:
(1)GeoHash
(2)Cartesian Tiers+GeoHash
而這塊的源碼實現都在lucene-spatial.jar中可以找到。接下來我將根據這2種方案展開關于構建索引和查詢細節進行闡述,都是代碼分析,感興趣的看官可以繼續往下看。
GeoHash
?構建索引階段
定義geohash域,在schema.xml中定義:
<fieldtype name="geohash"?class="solr.GeoHashField"/>
接下來再構建索引的時候使用到lucene-spatial.jar的GeoHashUtils類:
String geoHash = GeoHashUtils.encode(latitude, longitude);//通過geoHash算法將經緯度變成base32的編碼
document.addField("geohash", geoHash); //將經緯度對應的bash32編碼存入索引。
查詢階段
在solrconfig.xml中配置好QP,該QP將對用戶的請求Query進行QParser,查詢語法規范是
{!spatial? sfield=geofield pt= latitude, longitude d=xx, sphere_radius=xx }
sfield:geohash對應的域名
pt:經緯度字符串
d=球面距離
sphere_radius:圓周半徑
接下來看看QP是如何解析上述查詢語句,然后生成基于GeoHash的Query的,見如下代碼,代碼來源SpatialFilterQParser的parse()方法:
Java代碼 ??
?
?
?
?
?
?
?
?
?
?
其中最核心的方法便是GeoHashField的createSpatialQuery(),該方法負責生成基于geoHash的查詢Query,展開看該方法:
?
?
Java代碼 ??
?
?
???? 從源碼中可以看到代碼作者有標示TODO:optimize this,筆者從源碼中看到這塊的實現,也覺得確實有疑惑,整個大體實現流程是基于Lucene的Filter的方式來過濾命中docId,但是其過濾的范圍讓筆者看起來覺得性能會出現問題,可能也是源碼中有TODO:optimize this的緣故吧。
?接下來繼續講下核心處理流程,Lucene的查詢規則是Query->Weight->Scorer,而主要負責查詢遍歷結果集合的就是Scorer,該例子也不例外,同樣是:
SolrConstantScoreQueryà ConstantWeightà ConstantScorer,通過Query生成Weight,Weight生成Scorer,熟悉Lucene的讀者應該很清楚了,這里不再累述,
其中ConstantScorer的通過docIdSetIterator遍歷獲取滿足條件的docId
而docIdSetIterator便是前面源碼中的ValueSourceRangeFilter,該Filter將會過濾掉不在一個指定球面距離范圍內的數據,而ValueSourceRangeFilter并不是實際工作的類,它又將過濾交給了GeohashHaversineFunction,見ValueSourceRangeFilter如下代碼:
?
Java代碼 ??
那么繼續看GeohashHaversineFunction,首先看其?getRangeScorer()方法,最核心的部分為:
Java代碼 ??
所以再看看計算球面距離的GeohashHaversineFunction.floatVal()方法,可以從該方法最終調用的是distance()方法,如下所示:
Java代碼 ??
所以整個查詢流程是將索引中的所有docId從第一個docId 0開始,對應的經度緯度和查詢經緯度的球面距離是否在查詢給定的distance之內,滿足著將該docId返回,不滿足則過濾。
大家可能看到是所有docId,這也是筆者覺得該過濾范圍實現不靠譜的地方,也許是作者說需要進一步優化的地方。大家如果對怎么是所有docId進行過濾有疑惑,可以查看ValueSourceScorer的nextDoc() advance()方法,相信看過之后就明白了。到此Solr基于GeoHash的查詢實現介紹完畢了。
總結
以上是生活随笔為你收集整理的基于Solr的空间搜索学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 商品搜索引擎---推荐系统设计
- 下一篇: Solr4.7实现LBS(地理位置搜索)