使用PHP實現查找附近的人

最近有個業務場景使用到了查找附近的人,於是查閱了相關資料,並對使用PHP實現相關功能的多種方式和具體實現做一篇技術總結,歡迎各位看官提出意見和糾錯,下面開始進入正題:

LBS(基於位置的服務)

查找附近的人有個更大的專有名詞叫做LBS(基於位置的服務),LBS是指是指通過電信移動運營商的無線電通訊網路或外部定位方式,獲取移動終端用戶的位置信息,在GIS平台的支持下,為用戶提供相應服務的一種增值業務。因此首先得獲取用戶的位置,獲取用戶的位置有基於GPS、基於運營商基站、WIFI等方式,一般由客戶端獲取用戶位置的經緯度坐標上傳至應用伺服器,應用伺服器對用戶坐標進行保存,客戶端獲取附近的人數據的時候,應用伺服器基於請求人的地理位置配合一定的條件(距離,性別,活躍時間等)去資料庫進行篩選和排序。

根據經緯度如何得出兩點之間的距離?

我們都知道平面坐標內的兩點坐標可以使用平面坐標距離公式來計算,但經緯度是利用三度空間的球面來定義地球上的空間的球面坐標系統,假定地球是正球體,關於球面距離計算公式如下:

d(x1,y1,x2,y2)=r*arccos(sin(x1)*sin(x2)+cos(x1)*cos(x2)*cos(y1-y2))

具體推斷過程有興趣的推薦這篇文章:根據經緯度計算地面兩點間的距離-數學公式及推導

PHP函數代碼如下:

/**n * 根據兩點間的經緯度計算距離n * @param $lat1n * @param $lng1n * @param $lat2n * @param $lng2n * @return floatn */n public static function getDistance($lat1, $lng1, $lat2, $lng2){n $earthRadius = 6367000; //approximate radius of earth in metersn $lat1 = ($lat1 * pi() ) / 180;n $lng1 = ($lng1 * pi() ) / 180;n $lat2 = ($lat2 * pi() ) / 180;n $lng2 = ($lng2 * pi() ) / 180;n $calcLongitude = $lng2 - $lng1;n $calcLatitude = $lat2 - $lat1;n $stepOne = pow(sin($calcLatitude / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($calcLongitude / 2), 2);n $stepTwo = 2 * asin(min(1, sqrt($stepOne)));n $calculatedDistance = $earthRadius * $stepTwo;n return round($calculatedDistance);n }n

MySQL代碼如下:

SELECT n id, ( n 3959 * acos ( n cos ( radians(78.3232) ) n * cos( radians( lat ) ) n * cos( radians( lng ) - radians(65.3234) ) n + sin ( radians(78.3232) ) n * sin( radians( lat ) ) n ) n ) AS distance nFROM markers nHAVING distance < 30 nORDER BY distance nLIMIT 0 , 20; n

除了上面通過計算球面距離公式來獲取,我們可以使用某些資料庫服務得到,比如Redis和MongoDB:

Redis 3.2提供GEO地理位置功能,不僅可以獲取兩個位置之間的距離,獲取指定位置範圍內的地理信息位置集合也很簡單。Redis命令文檔

1.增加地理位置

GEOADD key longitude latitude member [longitude latitude member ...]n

2.獲取地理位置

GEOPOS key member [member ...]n

3.獲取兩個地理位置的距離

GEODIST key member1 member2 [unit]n

4.獲取指定經緯度的地理信息位置集合

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]n

5.獲取指定成員的地理信息位置集合

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]n

MongoDB專門針對這種查詢建立了地理空間索引。 2d和2dsphere索引,分別是針對平面和球面。 MongoDB文檔

1.添加數據

db.location.insert( {uin : 1 , loc : { lon : 50 , lat : 50 } } )n

2.建立索引

db.location.ensureIndex( { loc : "2d" } )n

3.查找附近的點

db.location.find( { loc :{ $near : [50, 50] } )n

4.最大距離和限制條數

db.location.find( { loc : { $near : [50, 50] , $maxDistance : 5 } } ).limit(20)n

5.使用geoNear在查詢結果中返回每個點距離查詢點的距離

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { type : "museum" } } )n

6.使用geoNear附帶查詢條件和返回條數,geoNear使用runCommand命令不支持find查詢中分頁相關limit和skip參數的功能

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { uin : 1 } })n

PHP多種方式和具體實現

1.基於MySql

成員添加方法:

public function geoAdd($uin, $lon, $lat)n{n $pdo = $this->getPdo();n $sql = INSERT INTO `markers`(`uin`, `lon`, `lat`) VALUES (?, ?, ?);n $stmt = $pdo->prepare($sql);n return $stmt->execute(array($uin, $lon, $lat));n}n

查詢附近的人(支持查詢條件和分頁):

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)n{n $pdo = $this->getPdo();n $sql = "SELECT n id, ( n 3959 * acos ( n cos ( radians(:lat) ) n * cos( radians( lat ) ) n * cos( radians( lon ) - radians(:lon) ) n + sin ( radians(:lat) ) n * sin( radians( lat ) ) n ) n ) AS distance n FROM markers";nn $input[:lat] = $lat;n $input[:lon] = $lon;nn if ($where) {n $sqlWhere = WHERE ;n foreach ($where as $key => $value) {n $sqlWhere .= "`{$key}` = :{$key} ,";n $input[":{$key}"] = $value;n }n $sql .= rtrim($sqlWhere, ,);n }nn if ($maxDistance) {n $sqlHaving = " HAVING distance < :maxDistance";n $sql .= $sqlHaving;n $input[:maxDistance] = $maxDistance;n }nn $sql .= ORDER BY distance;nn if ($page) {n $page > 1 ? $offset = ($page - 1) * $this->pageCount : $offset = 0;n $sqlLimit = " LIMIT {$offset} , {$this->pageCount}";n $sql .= $sqlLimit;n }nn $stmt = $pdo->prepare($sql);n $stmt->execute($input);n $list = $stmt->fetchAll(PDO::FETCH_ASSOC);nn return $list;n}n

2.基於Redis(3.2以上)

PHP使用Redis可以安裝redis擴展或者通過composer安裝predis類庫,本文使用redis擴展來實現。

成員添加方法:

public function geoAdd($uin, $lon, $lat)n{n $redis = $this->getRedis();n $redis->geoAdd(markers, $lon, $lat, $uin);n return true;n}n

查詢附近的人(不支持查詢條件和分頁):

public function geoNearFind($uin, $maxDistance = 0, $unit = km)n{n $redis = $this->getRedis();n $options = [WITHDIST]; //顯示距離n $list = $redis->geoRadiusByMember(markers, $uin, $maxDistance, $unit, $options);n return $list;n}n

3.基於MongoDB

PHP使用MongoDB的擴展有mongo(文檔)和mongodb(文檔),兩者寫法差別很大,選擇好擴展需要對應相應的文檔查看,由於mongodb擴展是新版,本文選擇mongodb擴展。

假設我們創建db庫和location集合

設置索引:

db.getCollection(location).ensureIndex({"uin":1},{"unique":true}) ndb.getCollection(location).ensureIndex({loc:"2d"})n#若查詢位置附帶查詢,可以將常查詢條件添加至組合索引n#db.getCollection(location).ensureIndex({loc:"2d",uin:1})n

成員添加方法:

public function geoAdd($uin, $lon, $lat)n{n $document = array(n uin => $uin,n loc => array(n lon => $lon,n lat => $lat,n ),n );nn $bulk = new MongoDBDriverBulkWrite;n $bulk->update(n [uin => $uin],n $document,n [ upsert => true]n );n //出現noreply 可以改成確認式寫入n $manager = $this->getMongoManager();n $writeConcern = new MongoDBDriverWriteConcern(1, 100);n //$writeConcern = new MongoDBDriverWriteConcern(MongoDBDriverWriteConcern::MAJORITY, 100);n $result = $manager->executeBulkWrite(db.location, $bulk, $writeConcern);nn if ($result->getWriteErrors()) {n return false;n }n return true;n} n

查詢附近的人(返回結果沒有距離,支持查詢條件,支持分頁)

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)n{n $filter = array(n loc => array(n $near => array($lon, $lat),n ),n );n if ($maxDistance) {n $filter[loc][$maxDistance] = $maxDistance;n }n if ($where) {n $filter = array_merge($filter, $where);n }n $options = array();n if ($page) {n $page > 1 ? $skip = ($page - 1) * $this->pageCount : $skip = 0;n $options = [n limit => $this->pageCount,n skip => $skipn ];n }nn $query = new MongoDBDriverQuery($filter, $options);n $manager = $this->getMongoManager();n $cursor = $manager->executeQuery(db.location, $query);n $list = $cursor->toArray();n return $list;n}n

查詢附近的人(返回結果帶距離,支持查詢條件,支付返回數量,不支持分頁):

public function geoNearFindReturnDistance($lon, $lat, $maxDistance = 0, $where = array(), $num = 0)n{n $params = array(n geoNear => "location",n near => array($lon, $lat),n spherical => true, // spherical設為false(默認),dis的單位與坐標的單位保持一致,spherical設為true,dis的單位是弧度n distanceMultiplier => 6371, // 計算成公里,坐標單位distanceMultiplier: 111。 弧度單位 distanceMultiplier: 6371n );nn if ($maxDistance) {n $params[maxDistance] = $maxDistance;n }n if ($num) {n $params[num] = $num;n }n if ($where) {n $params[query] = $where;n }nn $command = new MongoDBDriverCommand($params);n $manager = $this->getMongoManager();n $cursor = $manager->executeCommand(db, $command);n $response = (array) $cursor->toArray()[0];n $list = $response[results];n return $list;n}n

注意事項:

1.選擇好擴展,mongo和mongodb擴展寫法差別很大

2.寫數據時出現noreply請檢查寫入確認級別

3.使用find查詢的數據需要自己計算距離,使用geoNear查詢的不支持分頁

4.使用geoNear查詢的距離需要轉化成km使用spherical和distanceMultiplier參數

上述demo可以戳這裡:demo

總結

以上介紹了三種方式去實現查詢附近的人的功能,各種方式都有各自的適用場景,比如數據行比較少,例如查詢用戶和幾座城市之間的距離使用Mysql就足夠了,如果需要實時快速響應並且普通查找範圍內的距離,可以使用Redis,但如果數據量大並且多種屬性篩選條件,使用mongo會更方便,以上只是建議,具體實現方案還要視具體業務去進行方案評審。

推薦閱讀:

如何滿足PHP源代碼加密和混淆的需求?
網頁前端和後台人員都是如何看待全棧工程師的?
能在郵件中嵌入PHP嗎?
PHP運維自動化方向涉及哪些業務或工具開發么?有啥優勢或劣勢么?

TAG:PHP | LBS位置 | PHP学习 |