如果需要對帶經緯度的數據進行檢索,比如查找當前所在位置附近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 |
指定權重字段:qf=store_name^10+poi_location_p^5 |
score |
排序字段根據qf定義的字段defType定義的方式計算得到score排序輸出 |
其中有幾種常見的Solr支持的幾何操作:
WITHIN:在內部
CONTAINS:包含關系
DISJOINT:不相交
Intersects:相交(存在交集)
1)點查詢
測試代碼:查詢距離某個點pt距離為d的集合
SolrQuery params = new SolrQuery(); params.set("q", "*:*"); params.set("fq", "{!geofilt}"); //距離過濾函數 params.set("pt", "118.227985 39.410722"); //當前經緯度 params.set("sfield", "station_position"); //經緯度的字段 params.set("d", "50"); //就近 d km的所有數據 //params.set("score", "kilometers"); params.set("sort", "geodist() asc"); //根據距離排序:由近到遠 params.set("start", "0"); //記錄開始位置 params.set("rows", "100"); //查詢的行數 params.set("fl", "*,_dist_:geodist(),score");//查詢的結果中添加距離和score
返回結果集:
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配置文件:
<field name="station_position" type="location_jts" indexed="true" stored="true"/> <fieldType name="location_jts" class="solr.SpatialRecursivePrefixTreeFieldType" spatialContextFactory="com.spatial4j.core.context.jts.JtsSpatialContextFactory" distErrPct="0.025" maxDistErr="0.000009" units="degrees"/>
JtsSpatialContextFactory
當有Polygon多邊形時會使用jts(需要把jts.jar放到solr webapp服務的lib下)。基本形狀使用SpatialContext (spatial4j的類)。
Jts下載:http://sourceforge.net/projects/jts-topo-suite/
測試代碼:
SolrQuery params = new SolrQuery(); //q=geo:"Intersects(POLYGON((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30)))" params.set("q", "station_position:\"Intersects(POLYGON((118 40, 118.5 40, 118.5 38, 118.3 35, 118 38,118 40)))\""); params.set("start", "0"); //記錄開始位置 params.set("rows", "100"); //查詢的行數 params.set("fl", "*");
返回在這個POLYGON內的所有結果集。
3) 地址分詞搜索
在“點查詢”的基礎上加上一些地址信息,就可以做一些地理位置+地址信息的LBS應用。
Solr分詞配置
這里使用了mmseg4j分詞器:https://github.com/chenlb/mmseg4j-solr
Schema.xml配置:
<field name="station_address" type="textComplex" indexed="true" stored="true" multiValued="true"/> <fieldtype name="textComplex" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="complex" dicPath="dic"/> </analyzer> </fieldtype> <fieldtype name="textMaxWord" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="max-word" /> </analyzer> </fieldtype> <fieldtype name="textSimple" class="solr.TextField" positionIncrementGap="100"> <analyzer> <tokenizer class="com.chenlb.mmseg4j.solr.MMSegTokenizerFactory" mode="simple" dicPath="D:/my_dic" /> </analyzer> </fieldtype>
這里對“station_address”這個字段進行中文分詞。
下載mmseg4j-core-1.10.0.jar和mmseg4j-solr-2.2.0.jar放到solr webapp服務的lib下。
測試代碼:
public static SolrQuery getPointAddressQuery(String address){ SolrQuery params = new SolrQuery(); String q_params = "station_address:"+address; params.set("q", q_params); params.set("fq", "{!geofilt}"); //距離過濾函數 //params.set("fq","{!bbox}"); //距離過濾函數:圓的外接矩形 params.set("pt", "118.227985 39.410722"); //當前經緯度 params.set("sfield", "station_position"); //經緯度的字段 params.set("d", "50"); //就近 d km的所有數據 //params.set("score", "distance"); params.set("sort", "geodist() asc"); //根據距離排序:由近到遠 params.set("start", "0"); //記錄開始位置 params.set("rows", "100"); //查詢的行數 params.set("fl", "*,_dist_:geodist(),score"); return params; } public static void main(String[] args) { BaseStationSearch baseStationSearch = new BaseStationSearch(); baseStationSearch.IndexBaseStation(); //執行一次索引 //SolrQuery params = getPointQuery(); //SolrQuery params = getPolygonQuery(); SolrQuery params = getPointAddressQuery("鼓樓"); baseStationSearch.getAndPrintResult(params); }
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}
上面是測試的結果。
代碼托管在GitHub上:https://github.com/luxiaoxun/Code4Java
參考:
http://wiki.apache.org/solr/SpatialSearch
https://cwiki.apache.org/confluence/display/solr/Spatial+Search
http://tech.meituan.com/solr-spatial-search.html