1. 程式人生 > >基於快速GeoHash,如何實現海量商品與商圈的高效匹配?

基於快速GeoHash,如何實現海量商品與商圈的高效匹配?

起點 blog 屬於 描述 如何 提高 mbr 更多 每天

技術分享圖片

閑魚是一款閑置物品的交易平臺APP。通過這個平臺,全國各地“無處安放”的物品能夠輕松實現流動。這種分享經濟業務形態被越來越多的人所接受,也進一步實現了低碳生活的目標。

今天,閑魚團隊就商品與商圈的匹配算法為我們展開詳細解讀。

摘要
閑魚app根據交通條件、商場分布情況、住宅區分布情況綜合考慮,將城市劃分為一個個商圈。杭州部分區域商圈劃分如下圖所示。
技術分享圖片
閑魚的商品是由用戶發布的GPS隨機分布在地圖上的點數據。當用戶處於某個商圈範圍內時,app會向用戶推薦GPS位於此商圈中的商品。要實現精準推薦服務,就需要計算出哪些商品是歸屬於你所處的商圈。
在數據庫中,商圈是由多個點圍成的面數據,這些面數據形狀、大小各異,且互不重疊。商品是以GPS標記的點數據,如何能夠快速高效地確定海量商品與商圈的歸屬關系呢?傳統而直接的方法是,利用幾何學的空間關系計算公式對海量數據實施直接的“點—面”關系計算,來確定每一個商品是否位於每一個商圈內部。

閑魚目前有10億商品數據,且每天還在快速增加。全國所有城市的商圈數量總和大約為1萬,每個商圈的大小不一,邊數從10到80不等。如果直接使用幾何學點面關系運算,需要的計算量級約為2億億次基本運算。按照這個思路,我們嘗試過使用阿裏巴巴集團內部的離線計算集群來執行計算,結果集群在運行了超過2天之後也未能給出結果。
經過算法改進,我們采用了一種基於GeoHash精確匹配,結合GeoHash非精確匹配並配合小範圍幾何學關系運算精匹配的算法,大大降低了計算量,高效地實現了離線環境下海量點-面數據的包含關系計算。同樣是對10億條商品和1萬條商圈數據做匹配,可以在1天內得到結果。
▌點數據GeoHash原理與算法
GeoHash是一種對地理坐標進行編碼的方法,它將二維坐標映射為一個字符串。每個字符串代表一個特定的矩形,在該矩形範圍內的所有坐標都共用這個字符串。字符串越長精度越高,對應的矩形範圍越小。
對一個地理坐標編碼時,按照初始區間範圍緯度[-90,90]和經度[-180,180],計算目標經度和緯度分別落在左區間還是右區間。落在左區間則取0,右區間則取1。然後,對上一步得到的區間繼續按照此方法對半查找,得到下一位二進制編碼。當編碼長度達到業務的進度需求後,根據“偶數位放經度,奇數位放緯度”的規則,將得到的二進制編碼穿插組合,得到一個新的二進制串。最後,根據base32的對照表,將二進制串翻譯成字符串,即得到地理坐標對應的目標GeoHash字符串。
以坐標“30.280245, 120.027162”為例,計算其GeoHash字符串。首先對緯度做二進制編碼:
將[-90,90]平分為2部分,“30.280245”落在右區間(0,90],則第一位取1。
將(0,90]平分為2分,“30.280245”落在左區間(0,45],則第二位取0。
不斷重復以上步驟,得到的目標區間會越來越小,區間的兩個端點也越來越逼近“30.280245”。
下圖的流程詳細地描述了前幾次叠代的過程:
技術分享圖片
按照上面的流程,繼續往下叠代,直到編碼位數達到我們業務對精度的需求為止。完整的15位二進制編碼叠代表格如下:
技術分享圖片
得到的緯度二進制編碼為10101 01100 01000。

按照同樣的流程,對經度做二進制編碼,具體叠代詳情如下:
技術分享圖片
得到的經度二進制編碼為11010 10101 01101。
按照“偶數位放經度,奇數位放緯度”的規則,將經緯度的二進制編碼穿插,得到完成的二進制編碼為:11100 11001 10011 10010 00111 00010。由於後續要使用的是base32編碼,每5個二進制數對應一個32進制數,所以這裏將每5個二進制位轉換成十進制位,得到28,25,19,18,7,2。 對照base32編碼表,得到對應的編碼為:wtmk72。
技術分享圖片
可以在geohash.org/網站對上述結果進行驗證,驗證結果如下:

技術分享圖片
驗證結果的前幾位與我們的計算結果一致。如果我們利用二分法獲取二進制編碼時叠代更多次,就會得到驗證網站中這樣的位數更多的更精確結果。

GeoHash字符串的長度與精度的對應關系如下:
技術分享圖片
▌面數據GeoHash編碼實現
上一節介紹的標準GeoHash算法只能用來計算二維點坐標對應的GeoHash編碼,我們的場景中還需要計算面數據(即GIS中的POLYGON多邊形對象)對應的GeoHash編碼,需要擴展算法來實現。
算法思路是,先找到目標Polygon的最小外接矩形MBR,計算此MBR西南角坐標對應的GeoHash編碼。然後用GeoHash編碼的逆算法,反解出此編碼對應的矩形GeoHash塊。以此GeoHash塊為起點,循環往東、往北找相鄰的同等大小的GeoHash塊,直到找到的GeoHash塊完全超出MBR的範圍才停止。如此找到的多個GeoHash塊,邊緣上的部分可能與目標Polygon完全不相交,這部分塊需要通過計算剔除掉,如此一來可以減少後續不必要的計算量。
技術分享圖片
上面的例子中最終得到的結果高清大圖如下,其中藍色的GeoHash塊是與原始Polygon部分相交的,橘×××的GeoHash塊是完全被包含在原始Polygon內部的。
技術分享圖片
上述算法總結成流程圖如下:
技術分享圖片
▌求臨近GeoHash塊的快速算法
上一節對面數據進行GeoHash編碼的流程圖中標記為綠色和橘×××的兩步,分別是要尋找相鄰的東邊或北邊的GeoHash字符串。
傳統的做法是,根據當前GeoHash塊的反解信息,求出相鄰塊內部的一點,在對這個點做GeoHash編碼,即為相鄰塊的GeoHash編碼。如下圖,我們要計算"wtmk72"周圍的8個相鄰塊的編碼,就要先利用GeoHash逆算法將"wtmk72"反解出4個頂點的坐標N1、N2、N3、N4,然後由這4個坐標計算出右側鄰接塊內部的任意一點坐標N5,再對N5做GeoHash編碼,得到的“wtmk78”就是我們要求的右邊鄰接塊的編碼。按照同樣的方法,求可以求出"wtmk72"周圍總共8個鄰接塊的編碼。
技術分享圖片
這種方法需要先解碼一次再編碼一次,比較耗時,尤其是在指定的GeoHash字符串長度較長需要循環較多次的情況下。
通過觀察GeoHash編碼表的規律,結合GeoHash編碼使用的Z階曲線的特性,驗證了一種通過查表來快速求相鄰GeoHash字符串的方法。
還是以“wtmk72”這個GeoHash字符串為例,對應的10進制數是“28,25,19,18,7,2”,轉換成二進制就是11100 11001 10011 10010 00111 00010。其中,w對應11100,這5個二進制位分別代表“經 緯 經 緯 經”;t對應11001,這5個二進制位分別代表“緯 經 緯 經 緯”。由此推廣開來可知,GeoHash中的奇數位字符(本例中的‘w‘、‘m‘、‘7‘)代表的二進制位分別對應“經 緯 經 緯 經”,偶數位字符(本例中的‘t‘、‘k‘、‘2‘)代表的二進制位分別對應“緯 經 緯 經 緯”。
‘w‘的二進制11100,轉換成方位含義就是“右 上 右 下 左”。‘t‘的二進制11001,轉換成方位含義就是“上 右 下 左 上”。
根據這個字符與方位的轉換關系,我們可以知道,奇數位上的字符與位置對照表如下:

技術分享圖片
偶數位上的字符與位置對照表如下:
技術分享圖片
這裏可以看到一個很有意思的現象,奇數位的對照表和偶數位對照表存在一種轉置和翻轉的關系。
有了以上兩份字符與位置對照表,就可以快速得出每個字符周圍的8個字符分別是什麽。而要計算一個給定GeoHash字符串周圍8個GeoHash值,如果字符串最後一位字符在該方向上未超出邊界,則前面幾位保持不變,最後一位取此方向上的相鄰字符即可;如果最後一位在此方向上超出了對照表邊界,則先求倒數第二個字符在此方向上的相鄰字符,再求最後一個字符在此方向上相鄰字符(對照表環狀相鄰字符);如果倒數第二位在此方向上的相鄰字符也超出了對照表邊界,則先求倒數第三位在此方向上的相鄰字符。以此類推。
以上面的“wtmk72”舉例,要求這個GeoHash字符串的8個相鄰字符串,實際就是求尾部字符‘2’的相鄰字符。‘2’適用偶數對照表,它的8個相鄰字符分別是‘1’、‘3’、‘9’、‘0’、‘8’、‘p’、‘r’、‘x’,其中‘p’、‘r’、‘x’已經超出了對照表的下邊界,是將偶數位對照表上下相接組成環狀得到的相鄰關系。所以,對於這3個超出邊界的“下方”相鄰字符,需要求倒數第二位的下方相鄰字符,即‘7’的下方相鄰字符。‘7’是奇數位,適用奇數位對照表,‘7’在對照表中的“下方”相鄰字符是‘5’,所以“wtmk72”的8個相鄰GeoHash字符串分別是“wtmk71”、“wtmk73”、“wtmk79”、“wtmk70”、“wtmk78”、“wtmk5p”、“wtmk5r”、“wtmk5x”。利用此相鄰字符串快速算法,可以大大提高上一節流程圖中面數據GeoHash編碼算法的效率。
▌高效建立海量點數據與面數據的關系
建立海量點數據與面數據的關系的思路是,先將需要匹配的商品GPS數據(點數據)、商圈AOI數據(面數據)按照前面所述的算法,分別計算同等長度的GeoHash編碼。每個點數據都對應唯一一個GeoHash字符串;每個面數據都對應一個或多個GeoHash編碼,這些編碼要麽是“完全包含字符串”,要麽是“部分包含字符串”。
a)將每個商品的GeoHash字符串與商圈的“完全包含字符串”進行join操作。join得到的結果中出現的<商品,商圈>數據就是能夠確定的“某個商品屬於某個商圈”的關系。
b)對於剩下的還未被確定關系的商品,將這些商品的GeoHash字符串與商圈的“部分包含字符串”進行join操作。join得到的結果中出現的<商品,商圈>數據是有可能存在的“商品屬於某個商圈”的關系,接下來對這批數據中的商品gps和商圈AOI數據進行幾何學關系運算,進而從中篩選出確定的“商品屬於某個商圈”的關系。
如圖,商品1的點數據GeoHash編碼為"wtmk70j",與面數據的“完全包含字符串wtmk70j”join成功,所以可以直接確定商品1屬於此面數據。
商品2的點數據GeoHash編碼為“wtmk70r”,與面數據的“部分包含字符串wtmk70r”join成功,所以商品2疑似屬於面數據,具體是否存在包含關系,還需要後續的點面幾何學計算來確定。 商品3的點數據GeoHash編碼與面數據的任何GeoHash塊編碼都匹配不上,所以可以快速確定商品3不屬於此面數據。技術分享圖片
實際應用中,原始的海量商品GPS範圍散布在全國各地,海量商圈數據也散布在全國各個不同的城市。經過a)步驟的操作後,大部分的商品數據已經確定了與商圈的從屬關系。剩下的未能匹配上的商品數據,經過b)步驟的GeoHash匹配後,可以將後續“商品-商圈幾何學計算”的運算量從“1個商品 x 全國所有商圈”笛卡爾積的量級,降低為“1個商品 x 1個(或幾個)商圈”笛卡爾積的量級,減少了絕大部分不必要的幾何學運算,而這部分運算是非常耗時的。
在閑魚的實際應用中,10億商品和1萬商圈數據,使用本文的快速算法,只需要 10億次GeoHash點編碼 + 1萬次GeoHash面編碼 + 500萬次“點是否在面內部”幾何學運算,粗略換算為基本運算需要的次數約為1800億次,運算量遠小於傳統方法的2億億次基本運算。使用阿裏巴巴的離線計算平臺,本文的算法在不到一天的時間內就完成了全部計算工作。
另外,對於給定的點和多邊形,通過幾何學計算包含關系的算法不止一種,最常用的算法是射線法。簡單來說,就是從這個點出發做一條射線,判斷該射線與多邊形的交點個數是奇數還是偶數。如果是奇數,說明點在多邊形內;否則,點在多邊形外。
▌延伸
面對海量點面數據的空間關系劃分,本文采用是的通過GeoHash來降低計算量的思路,本質上來說是利用了空間索引的思想。事實上,在GIS領域有多種實用的空間索引,常見的如R樹系列(R樹、R+樹、R*樹)、四叉樹、K-D樹、網格索引等,這些索引算法各有特點。本文的思路不僅能用來處理點—面關系的相關問題,還可以用來快速處理點—點關系、面—面關系、點—線關系、線—線關系等問題,比如快速確定大範圍類的海量公交站臺與道路的從屬關系、多條道路或鐵路是否存在交點等問題。

基於快速GeoHash,如何實現海量商品與商圈的高效匹配?