如果需要對帶經緯度的資料進行檢索,比如查找目前所在位置附近1000米的酒店,一種簡單的方法就是:擷取資料庫中的所有酒店資料,按經緯度計算距離,傳回距離小于1000米的資料。
這種方式在資料量小的時候比較有效,但是當資料量大的時候,檢索的效率是很低的,本文介紹使用solr的spatial query進行空間搜尋。
空間搜尋原理
空間搜尋,又名spatial search(spatial query),基于空間搜尋技術,可以做到:
1)對point(經緯度)和其他的幾何圖形建索引
2)根據距離排序
3)根據矩形,圓形或者其他的幾何形狀過濾搜尋結果
在solr中,空間搜尋主要基于geohash和cartesian tiers 2個概念來實作:
geohash算法
通過geohash算法,可以将經緯度的二維坐标變成一個可排序、可比較的的字元串編碼。
在編碼中的每個字元代表一個區域,并且前面的字元是後面字元的父區域。其算法的過程如下:
根據經緯度計算geohash二進制編碼
地球緯度區間是[-90,90], 如某緯度是39.92324,可以通過下面算法對39.92324進行逼近編碼:
1)區間[-90,90]進行二分為[-90,0),[0,90],稱為左右區間,可以确定39.92324屬于右區間[0,90],給标記為1;
2)接着将區間[0,90]進行二分為 [0,45),[45,90],可以确定39.92324屬于左區間 [0,45),給标記為0;
3)遞歸上述過程39.92324總是屬于某個區間[a,b]。随着每次疊代區間[a,b]總在縮小,并越來越逼近39.928167;
4)如果給定的緯度(39.92324)屬于左區間,則記錄0,如果屬于右區間則記錄1,這樣随着算法的進行會産生一個序列1011 1000 1100 0111 1001,序列的長度跟給定的區間劃分次數有關。

同理,地球經度區間是[-180,180],對經度116.3906進行編碼的過程也類似:
組碼
通過上述計算,緯度産生的編碼為1011 1000 1100 0111 1001,經度産生的編碼為1101 0010 1100 0100 0100。偶數位放經度,奇數位放緯度,把2串編碼組合生成新串:11100 11101 00100 01111 00000 01101 01011 00001。
最後使用用0-9、b-z(去掉a, i, l, o)這32個字母進行base32編碼,首先将11100 11101 00100 01111 00000 01101 01011 00001轉成十進制 28,29,4,15,0,13,11,1,十進制對應的編碼就是wx4g0ec1。同理,将編碼轉換成經緯度的解碼算法與之相反,具體不再贅述。
由上可知,字元串越長,表示的範圍越精确。當geohash base32編碼長度為8時,精度在19米左右,而當編碼長度為9時,精度在2米左右,編碼長度需要根據資料情況進行選擇。不過從geohash的編碼算法中可以看出它的一個缺點,位于邊界兩側的兩點,雖然十分接近,但編碼會完全不同。實際應用中,可以同時搜尋該點所在區域的其他八個區域的點,即可解決這個問題。
cartesian tiers 笛卡爾層
笛卡爾分層模型的思想是将經緯度轉換成更大粒度的分層網格,該模型建立了很多的地理層,每一層在前一層的基礎上細化切分粒度,每一個網格被配置設定一個id,代表一個地理位置。
每層以2的平方遞增,是以第一層為4個網格,第二層為16 個,是以整個地圖的經緯度将在每層的網格中展現:
那麼如何建構這樣的索引結構呢,其實很簡單,隻需要對應笛卡爾層的層數來建構域即可,一個域或坐标對應多個tiers層次。也即是tiers0->field_0,tiers1->field_1,tiers2->field_2,……,tiers19->field_19。(一般20層即可)。每個對應笛卡爾層次的域将根據目前這條記錄的經緯度通過笛卡爾算法計算出歸屬于目前層的網格,然後将gridid(網格唯一标示)以term的方式存入索引。這樣每條記錄關于笛卡爾0-19的域将都會有一個gridid對應起來。但是查詢的時候一般是需要查周邊的位址,那麼可能周邊的範圍超過一個網格的範圍,那麼實際操作過程是根據經緯度和一個距離确定出需要涉及查詢的從19-0(從高往低查)若幹層對應的若幹網格的資料。那麼一個經緯度周邊位址的查詢隻需要如下圖圓圈内的資料:
由上可知,基于cartesian tier的搜尋步驟為:
1、根據cartesian tier層獲得坐标點的地理位置gridid
2、與系統索引gridid比對計算
3、計算結果集與目标坐标點的距離傳回特定範圍内的結果集合
使用笛卡爾層,能有效縮減少過濾範圍,快速定位坐标點。
基于solr的空間搜尋實戰
solr已經提供了3種filedtype來進行空間搜尋:
1) latlontype(用于平面坐标,而不是大地坐标)
2) spatialrecursiveprefixtreefieldtype(縮寫為rpt)
3) bboxfield(用于邊界索引查詢)
本文重點介紹使用spatialrecursiveprefixtreefieldtype,不僅可以用點,也可以用于多邊形的查詢。
1、配置solr
首先看下資料:
solr的schema.xml配置:
這裡重點是station_position,它的type是location_rpt,它在solr中的定義如下:
對solr.spatialrecursiveprefixtreefieldtype的配置說明:
spatialrecursiveprefixtreefieldtype
用于深度周遊字首樹的fieldtype,主要用于獲得基于lucene中的recursiveprefixtreestrategy。
geo
預設為true,值為true的情況下坐标基于球面坐标系,采用geohash的方式;值為false的情況下坐标基于2d平面的坐标系,采用euclidean/cartesian的方式。
disterrpct
定義非point圖形的精度,範圍在0-0.5之間。該值決定了非point的圖形索引或查詢時的level(如geohash模式時就是geohash編碼的長度)。當為0時取maxlevels,即精度最大,精度越大将花費更多的空間和時間去建索引。
maxdisterr/maxlevels:maxdisterr
定義了索引資料的最高層maxlevels,上述定義為0.000009,根據geohashutils.lookuphashlenforwidthheight(0.000009, 0.000009)算出編碼長度為11位,精度在1米左右,直接決定了point索引的term數。maxlevels優先級高于maxdisterr,即有maxlevels的話maxdisterr失效。詳見spatialprefixtreefactory.init()方法。不過一般使用maxdisterr。
units
機關是degrees。
worldbounds
世界坐标值:”minx miny maxx maxy”。 geo=true即geohash模式時,該值預設為”-180 -90 180 90”。geo=false即quad時,該值為java double類型的正負邊界,此時需要指定該值,設定成”-180 -90 180 90”。
2、建立索引
這裡使用solrj來建立索引:
這裡使用“經度 緯度”這樣的字元串格式将經緯度索引到station_position字段中。
3、查詢
查詢文法示例:
q={!geofilt pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q={!bbox pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q=poi_location_p:"intersects(-74.093 41.042 -69.347 44.558)" //a bounding box (not in wkt)
q=poi_location_p:"intersects(polygon((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30)))" //a wkt example
涉及到的字段說明:
字段
含義
q
查詢條件,如 q=poi_id:134567
fq
過濾條件,如 fq=store_name:農業
fl
傳回字段,如fl=poi_id,store_name
pt
坐标點,如pt=54.729696,-98.525391
d
搜尋半徑,如 d=10表示10km範圍内
sfield
指定坐标索引字段,如sfield=geo
deftype
指定查詢類型可以取 dismax和edismax,edismax支援boost函數相乘作用,dismax是通過累加方式計算最後的score.
qf
指定權重字段:qf=store_name^10+poi_location_p^5
score
排序字段根據qf定義的字段deftype定義的方式計算得到score排序輸出
其中有幾種常見的solr支援的幾何操作:
within:在内部
contains:包含關系
disjoint:不相交
intersects:相交(存在交集)
1)點查詢
測試代碼:查詢距離某個點pt距離為d的集合
傳回結果集:
solrdocument{station_id=12003, station_address=江蘇南京1, station_position=118.227996 39.410733, _version_=1499776366043725838, _dist_=0.001559071, score=1.0}
solrdocument{station_id=12004, station_address=江蘇南京2, station_position=118.228996 39.411733, _version_=1499776366044774400, _dist_=0.14214091, score=1.0}
solrdocument{station_id=12005, station_address=江蘇南京3, station_position=118.238996 39.421733, _version_=1499776366044774401, _dist_=1.5471642, score=1.0}
solrdocument{station_id=7583, station_address=河北省唐山市于唐線, station_position=118.399614 39.269098, _version_=1499776365690355717, _dist_=21.583544, score=1.0}
從這部分結果集中可以看出,前3條資料是離目标點"118.227985 39.410722"最近的(這3條資料是我僞造的,僅僅用于測試)。
2)多邊形查詢:
修改schema.xml配置檔案:
jtsspatialcontextfactory
當有polygon多邊形時會使用jts(需要把jts.jar放到solr webapp服務的lib下)。基本形狀使用spatialcontext (spatial4j的類)。
測試代碼:
傳回在這個polygon内的所有結果集。
3) 位址分詞搜尋
在“點查詢”的基礎上加上一些位址資訊,就可以做一些地理位置+位址資訊的lbs應用。
solr分詞配置
這裡使用了mmseg4j分詞器:https://github.com/chenlb/mmseg4j-solr
schema.xml配置:
這裡對“station_address”這個字段進行中文分詞。
下載下傳mmseg4j-core-1.10.0.jar和mmseg4j-solr-2.2.0.jar放到solr webapp服務的lib下。
search results count: 2
solrdocument{station_id=12003, station_address=[江蘇南京鼓樓東南大學], station_position=[118.227996 39.410733], _version_=1500226229258682377, _dist_=0.001559071, score=4.0452886}
solrdocument{station_id=12004, station_address=[江蘇南京鼓樓南京大學], station_position=[118.228996 39.411733], _version_=1500226229258682378, _dist_=0.14214091, score=4.0452886}
上面是測試的結果。
參考:
http://wiki.apache.org/solr/spatialsearch
https://cwiki.apache.org/confluence/display/solr/spatial+search
http://tech.meituan.com/solr-spatial-search.html
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。