文章版權由作者李曉暉和博客園共有,若轉載請於明顯處標明出處:http://www.cnblogs.com/naaoveGIS/
1.背景
多個項目中實現范圍(圓)搜索的方案為:依賴庫表中的X和Y字段構造一個矩形查詢范圍,再通過幾何計算范圍中的數據到指定坐標的距離是否在閾值半徑中,最后返回閾值中的數據。
該方案有幾個優點:
- 無需對數據預處理,僅通過sql就可以實現,實現方式簡單。
- 數據庫環境中,通過數字搜索比通過字符串搜索效率更高,占用的CPU更少。
但是,該方案在表數據量龐大的情況下,通過X和Y兩個字段,並且有四個查詢條件,對性能有一定損耗。
在之前我寫過一篇關於Geohash編碼研究的文章WebGIS中GeoHash編碼的研究和擴展,這里提到了一種將X和Y以哈夫曼原理編碼成一維字符串的方案。那么這里如果我們使用geohash編碼方案來優化查詢效率是否有用?
2.基於GeoHash編碼的范圍查詢
2.1需要解決的點
- 基於GeoHash編碼原理,將編碼對象從經緯度數據擴展到也支持平面坐標數據
- 由於編碼值對應的是一個范圍,如果查詢坐標落入在范圍的角落,僅通過相同字符串匹配可能導致查詢結果不全,這里需要重構查詢范圍
- 根據查詢的容差范圍,可以計算出該范圍所對應的geohash字符串位數
2.2解決思路
- 針對平面坐標:將編碼范圍改變成該地圖平面坐標真實范圍,基於哈夫曼編碼規則進行計算,最后使用base32編碼成字符串。
- 針對查詢范圍:以查詢點為中心通過查詢范圍構造出查詢范圍矩形,利用目前查詢范圍所對應的hash編碼長度所對應的精度,利用該精度將矩形進行切割,然后對格網分別編碼。
- geohash長度所對應的真實精度:基於編碼規律,經度的bit長度可以為奇偶,但是緯度的bit長度必須是偶數,反算出經度和緯度的bit長度。然后根據經緯對范圍,結合各方向的二分法次數(bit長度),即可算出經緯度此時的精度。
2.3方案實現
這里重點給出查詢搜索代碼,即通過hash長度對應的精度、查詢范圍參數,進行網格切分和編碼。
/*** * 通過傳入指定范圍、指定坐標、查詢范圍和geohash長度,返回查詢范圍中對應的所有geohash編碼 * @param minX * @param minY * @param maxX * @param maxY * @param X * @param Y * @param geohashLength geohash字符串編碼長度 * @param searchRange 查詢范圍,如果是平面坐標系100M則傳入100,經緯度坐標系0.0001度則傳入0.0001 * @return */ public static List<String> GeoHashSearch(double minX, double minY, double maxX, double maxY, double X, double Y, int geohashLength,double searchRange){ List<Integer> latLngLength = SetHashLength(geohashLength); double boundMinX = X - searchRange; double boundMaxX = X + searchRange; double boundMinY = Y - searchRange; double boundMaxY = Y + searchRange; List<Double> range = GetGoeHashRange(minX, minY, maxX, maxY, latLngLength.get(0), latLngLength.get(1)); List<String> searchResult= new ArrayList<String>(); double xrange = range.get(0); double yrange = range.get(1); double value = 0.5; for (int i = 0; boundMinX + (i - value) * xrange <= boundMaxX; i++) { for (int j = 0; boundMinY + (j - value) * yrange <= boundMaxY; j++) { String geohashCode = Encode(minX, minY, maxX, maxY, boundMinX + i* xrange, boundMinY + j * yrange, geohashLength); if (!searchResult.contains(geohashCode)) { searchResult.add(geohashCode); } } } return searchResult; }
2.4優缺點探討
2.4.1優點
- geohash編碼通過不斷的二分,如果有必要可以直接將精度編碼至厘米或毫米級別,並且對應的編碼長度不會特別長。比如,當經緯度坐標系下,即使坐標范圍用全球范圍(-90到90,-180到180),其厘米級的編碼長度也不長。以下是此時的長度精確表:
2.4.2缺點
- 高精度編碼沒法使用:雖然精度到厘米編碼長度也不長,但是當查詢范圍是1Km例如,此時編碼長度只需要到2位,而查詢卻必須使用like去匹配,此時查詢效率反而太低。
- 不同編碼長度間跨越的精度太大:比如,查詢1000M和查詢2000M范圍所對應的編碼長度可能都是2,這樣導致查詢的結果的個數(格網切分)可能特別多。那么此時即使對編碼字段做了索引,也不一定會產生實際效果(如果使用In則索引無效,而使用OR,查詢條件又過多影響sql解析等)。
- 編碼為字符串影響查詢效率:geohash編碼的結果是基於Base32規范進行結果編碼,為字符串,影響數據庫查詢效率。
2.5 換一種思路
geohash編碼由於隨着地圖范圍不同各編碼長度精度無法確定、編碼只能以字符串存儲等問題,在我們的業務場景上無法使用。那么,如果我們讓編碼精度確定、編碼可以用數字替代,是否就可以達到業務場景的需要呢?
3.基於格網編碼的范圍查詢
3.1算法介紹
格網划分算是GIS算法中的萬金油。以前博客中寫過的空間索引、地理插值、影像金字塔、矢量切片等等均可以基於格網的思路去探索。這里,同樣可以利用格網算法來進行編碼。
3.1.1基本算法
- 將地圖的左上角坐標當做原點,設定好格網的長度(X方向和Y方向)
- 傳入坐標,計算坐標分別在X方向和Y方向離坐標原點的格網個數,分別為xNum、yNum
/*** * 通過傳入地圖起始點,待編碼坐標,編碼的X和Y方向精確度,獲取網格編碼字符串 * @param minX 地圖起始點X坐標 * @param minY 地圖起始點Y坐標 * @param X * @param Y * @param gridXSize X方向精確度。平面坐標為M,經緯度坐標為度 * @param gridYSize Y方向精確度。平面坐標為M,經緯度坐標為度 * @return */ public static long GetGridCode(double minX, double minY, double X, double Y, double gridXSize,double gridYSize){ if (X < minX || Y < minY){ return -1; } int xNum = (int)Math.ceil(Math.abs(X - minX) / gridXSize); int yNum = (int)Math.ceil(Math.abs(Y - minY) / gridYSize); return CreateLongCode(xNum,yNum); }
3.1.2編碼優化
如果我們需要將編碼轉換成數字編碼,那么我們同樣需要設定一種規則。這里,我規定xNum和yNum都必須是八個字符串長度,不足的在前綴以0補充,最后再合並轉換成整數。(注意,這里我設計以0作為前綴而不是后綴補充,是為了及時轉換成數字后,以后可以通過數字將編碼反轉換為空間范圍)
/*** * 以8位數和8位數分別將col和row填充組合成一個整數 */ private static long CreateLongCode(int x,int y){ String sx=String.valueOf(y); String sy=String.valueOf(y); for(int i=sx.length();i<XLen;i++){ sx="0"+sx; } for(int j=sy.length();j<YLen;j++){ sy="0"+sy; } String scode=sx+sy; long code=Long.parseLong(scode); return code; } /*** * 獲取網格編碼所對應的真實地理范圍 * @param minX * @param minY * @param value 編碼值 * @param gridXSize X方向精確度。平面坐標為M,經緯度坐標為度 * @param gridYSize Y方向精確度。平面坐標為M,經緯度坐標為度 * @return */ public static List<Double> Decode(double minX, double minY, long value, double gridXSize,double gridYSize){ String svalue=String.valueOf(value); String sx=svalue.substring(0,svalue.length()-YLen-1); String sy=svalue.substring(svalue.length()-YLen); int xnum=Integer.parseInt(sx); int ynum=Integer.parseInt(sy); double boundMinX = minX + (xnum - 1) * gridXSize; double boundMaxX = boundMinX + gridXSize; double boundMinY = minY + (ynum - 1) * gridYSize; double boundMaxY = boundMinY + gridYSize; List<Double> bound = new ArrayList<Double>(); bound.add(boundMinX); bound.add(boundMinY); bound.add(boundMaxX); bound.add(boundMaxY); return bound; }
3.2范圍查詢
同樣,這里也需要考慮與geohash查詢時一樣的情況:
- 查詢XY落在網格的邊角上
- 查詢范圍閾值大於網格大小 解決思路與之前相同:
/*** * 通過傳入地圖起始點、網格X和Y方向精確度、查詢范圍和查詢點,返回對應查詢范圍內所有網格編碼 * @param minX * @param minY * @param X * @param Y * @param gridXSize X方向精確度。平面坐標為M,經緯度坐標為度 * @param gridYSize Y方向精確度。平面坐標為M,經緯度坐標為度 * @param range 查詢范圍,平面坐標為M,經緯度坐標為度 * @return */ public static List<Long> GridCodeSearch(double minX, double minY, double X, double Y, double gridXSize, double gridYSize,double range){ if (X < minX || Y < minY){ return null; } double boundMinX = X - range; double boundMinY = Y - range; double boundMaxX = X + range; double boundMaxY = Y + range; double value=0.5; List<Long> searchResult = new ArrayList<Long>(); for (int i = 0; boundMinX + (i - value) * gridXSize <= boundMaxX; i++){ for (int j = 0; boundMinY + (j - value) * gridYSize <= boundMaxY; j++){ long gridCode = GetGridCode(minX, minY, boundMinX + i * gridXSize, boundMinY + j * gridYSize, gridXSize, gridYSize); if (!searchResult.contains(gridCode)){ searchResult.add(gridCode); } } } return searchResult; }
3.3格網划分的一點建議
- 格網不宜划分太小,建議划分的比查詢范圍大,這樣保證范圍過濾查詢時返回的匹配格網編碼少。比如,格網大小500M,查詢范圍100M,查詢時,在多數情況下將只返回一個編碼。當然,此時基於該編碼去數據庫中查詢,將得到更多的數據點,於是需要我們做精確的范圍計算量變大。但是:將數據庫壓力適當轉移到服務器計算是一種更划算的策略。當然,格網划的太大,也會適得其反,建議通用查詢范圍一兩倍即可。
4.后續方案描述
- 坐標存入時,將坐標基於格網編碼並同步存入到指定字段,對該字段建立索引(此時字段為長度大於16的長整型)。
- 查詢時,調用編碼查詢接口,獲取到該XY以及查詢范圍下,對應的網格編碼。在數據庫中利用這些編碼做匹配查詢(粗過濾)。對返回的結果進一步做精確范圍匹配(精過濾可做可不做,視需求規格而定)。
-----歡迎轉載,但保留版權,請於明顯處標明出處:http://www.cnblogs.com/naaoveGIS/
如果您覺得本文確實幫助了您,可以微信掃一掃,進行小額的打賞和鼓勵,謝謝 ^_^