如果需要對帶經緯度的資料進行檢索,比如尋找當前所在位置附近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配置:
<field name="station_id" type="long" indexed="true" stored="true" required="true" multiValued="false" /><field name="station_address" type="text_general" indexed="true" stored="true"/><field name="station_position" type="location_rpt" indexed="true" stored="true"/><uniqueKey>station_id</uniqueKey>
這裡重點是station_position,它的type是location_rpt,它在Solr中的定義如下:
<!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. --><fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/><!-- An alternative geospatial field type new to Solr 4. It supports multiValued and polygon shapes. For more information about this and other Spatial fields new to Solr 4, see: http://wiki.apache.org/solr/SolrAdaptersForLuceneSpatial4 --><fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType"geo="true" distErrPct="0.025" maxDistErr="0.000009" units="degrees" /><!-- Spatial rectangle (bounding box) field. It supports most spatial predicates, and has special relevancy modes: score=overlapRatio|area|area2D (local-param to the query). DocValues is required for relevancy. --><fieldType name="bbox" class="solr.BBoxField" geo="true" units="degrees" numberType="_bbox_coord" /><fieldType name="_bbox_coord" class="solr.TrieDoubleField" precisionStep="8" docValues="true" stored="false"/>
對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來建立索引:
//Index some base station data for test public void IndexBaseStation(){ BaseStationDb baseStationDb = new BaseStationDb(); List<BaseStation> stations = baseStationDb.getAllBaseStations(); Collection<SolrInputDocument> docList = new ArrayList<SolrInputDocument>(); for (BaseStation baseStation : stations) { //添加基站資料到Solr索引中 SolrInputDocument doc = new SolrInputDocument(); doc.addField("station_id", baseStation.getBaseStationId()); doc.addField("station_address", baseStation.getAddress()); String posString = baseStation.getLongitude()+" "+baseStation.getLatitude() ; doc.addField("station_position", posString); docList.add(doc); } try { server.add(docList); server.commit(); } catch (SolrServerException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println("Index base station data done!"); }
這裡使用“經度 緯度”這樣的字串格式將經緯度索引到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 |