最近有個業務場景使用到了查詢附近的人,於是查閱了相關資料,並對使用PHP實現相關功能的多種方式和具體實現做一篇技術總結,歡迎各位看官提出意見和糾錯,下面開始進入正題:
LBS(基於位置的服務)
查詢附近的人有個更大的專有名詞叫做LBS(基於位置的服務),LBS是指是指通過電信行動業者的無線電通訊網路或外部定位方式,獲取行動終端使用者的位置資訊,在GIS平臺的支援下,為使用者提供相應服務的一種增值業務。因此首先得獲取使用者的位置,獲取使用者的位置有基於GPS、基於運營商基站、WIFI等方式,一般由使用者端獲取使用者位置的經緯度座標上傳至應用伺服器,應用伺服器對使用者座標進行儲存,使用者端獲取附近的人資料的時候,應用伺服器基於請求人的地理位置配合一定的條件(距離,性別,活躍時間等)去資料庫進行篩選和排序。
根據經緯度如何得出兩點之間的距離?
我們都知道平面座標內的兩點座標可以使用平面座標距離公式來計算,但經緯度是利用三度空間的球面來定義地球上的空間的球面座標系統,假定地球是正球體,關於球面距離計算公式如下:
具體推斷過程有興趣的推薦這篇文章:
PHP函數程式碼如下:
/** * 根據兩點間的經緯度計算距離 * @param $lat1 * @param $lng1 * @param $lat2 * @param $lng2 * @return float */ public static function getDistance($lat1, $lng1, $lat2, $lng2){ $earthRadius = 6367000; //approximate radius of earth in meters $lat1 = ($lat1 * pi() ) / 180; $lng1 = ($lng1 * pi() ) / 180; $lat2 = ($lat2 * pi() ) / 180; $lng2 = ($lng2 * pi() ) / 180; $calcLongitude = $lng2 - $lng1; $calcLatitude = $lat2 - $lat1; $stepOne = pow(sin($calcLatitude / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($calcLongitude / 2), 2); $stepTwo = 2 * asin(min(1, sqrt($stepOne))); $calculatedDistance = $earthRadius * $stepTwo; return round($calculatedDistance); }
MySQL程式碼如下:
SELECT id, ( 3959 * acos ( cos ( radians(78.3232) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(65.3234) ) + sin ( radians(78.3232) ) * sin( radians( lat ) ) ) ) AS distance FROM markers HAVING distance < 30 ORDER BY distance LIMIT 0 , 20;
除了上面通過計算球面距離公式來獲取,我們可以使用某些資料庫服務得到,比如Redis和MongoDB:
Redis 3.2提供GEO地理位置功能,不僅可以獲取兩個位置之間的距離,獲取指定位置範圍內的地理資訊位置集合也很簡單。Redis命令檔案
1.增加地理位置
GEOADD key longitude latitude member [longitude latitude member ...]
2.獲取地理位置
GEOPOS key member [member ...]
3.獲取兩個地理位置的距離
GEODIST key member1 member2 [unit]
4.獲取指定經緯度的地理資訊位置集合
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
5.獲取指定成員的地理資訊位置集合
GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
MongoDB專門針對這種查詢建立了地理空間索引。 2d和2dsphere索引,分別是針對平面和球面。 MongoDB檔案
1.新增資料
db.location.insert( {uin : 1 , loc : { lon : 50 , lat : 50 } } )
2.建立索引
db.location.ensureIndex( { loc : "2d" } )
3.查詢附近的點
db.location.find( { loc :{ $near : [50, 50] } )
4.最大距離和限制條數
db.location.find( { loc : { $near : [50, 50] , $maxDistance : 5 } } ).limit(20)
5.使用geoNear在查詢結果中返回每個點距離查詢點的距離
db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { type : "museum" } } )
6.使用geoNear附帶查詢條件和返回條數,geoNear使用runCommand命令不支援find查詢中分頁相關limit和skip引數的功能
db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { uin : 1 } })
PHP多種方式和具體實現
1.基於MySql
成員新增方法:
public function geoAdd($uin, $lon, $lat) { $pdo = $this->getPdo(); $sql = 'INSERT INTO `markers`(`uin`, `lon`, `lat`) VALUES (?, ?, ?)'; $stmt = $pdo->prepare($sql); return $stmt->execute(array($uin, $lon, $lat)); }
查詢附近的人(支援查詢條件和分頁):
public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0) { $pdo = $this->getPdo(); $sql = "SELECT id, ( 3959 * acos ( cos ( radians(:lat) ) * cos( radians( lat ) ) * cos( radians( lon ) - radians(:lon) ) + sin ( radians(:lat) ) * sin( radians( lat ) ) ) ) AS distance FROM markers"; $input[':lat'] = $lat; $input[':lon'] = $lon; if ($where) { $sqlWhere = ' WHERE '; foreach ($where as $key => $value) { $sqlWhere .= "`{$key}` = :{$key} ,"; $input[":{$key}"] = $value; } $sql .= rtrim($sqlWhere, ','); } if ($maxDistance) { $sqlHaving = " HAVING distance < :maxDistance"; $sql .= $sqlHaving; $input[':maxDistance'] = $maxDistance; } $sql .= ' ORDER BY distance'; if ($page) { $page > 1 ? $offset = ($page - 1) * $this->pageCount : $offset = 0; $sqlLimit = " LIMIT {$offset} , {$this->pageCount}"; $sql .= $sqlLimit; } $stmt = $pdo->prepare($sql); $stmt->execute($input); $list = $stmt->fetchAll(PDO::FETCH_ASSOC); return $list; }
2.基於Redis(3.2以上)
PHP使用Redis可以安裝redis擴充套件或者通過composer安裝predis類庫,本文使用redis擴充套件來實現。
成員新增方法:
public function geoAdd($uin, $lon, $lat) { $redis = $this->getRedis(); $redis->geoAdd('markers', $lon, $lat, $uin); return true; }
查詢附近的人(不支援查詢條件和分頁):
public function geoNearFind($uin, $maxDistance = 0, $unit = 'km') { $redis = $this->getRedis(); $options = ['WITHDIST']; //顯示距離 $list = $redis->geoRadiusByMember('markers', $uin, $maxDistance, $unit, $options); return $list; }
3.基於MongoDB
PHP使用MongoDB的擴充套件有mongo(檔案)和mongodb(檔案),兩者寫法差別很大,選擇好擴充套件需要對應相應的檔案檢視,由於mongodb擴充套件是新版,本文選擇mongodb擴充套件。
假設我們建立db庫和location集合
設定索引:
db.getCollection('location').ensureIndex({"uin":1},{"unique":true}) db.getCollection('location').ensureIndex({loc:"2d"}) #若查詢位置附帶查詢,可以將常查詢條件新增至組合索引 #db.getCollection('location').ensureIndex({loc:"2d",uin:1})
成員新增方法:
public function geoAdd($uin, $lon, $lat) { $document = array( 'uin' => $uin, 'loc' => array( 'lon' => $lon, 'lat' => $lat, ), ); $bulk = new MongoDB\Driver\BulkWrite; $bulk->update( ['uin' => $uin], $document, [ 'upsert' => true] ); //出現noreply 可以改成確認式寫入 $manager = $this->getMongoManager(); $writeConcern = new MongoDB\Driver\WriteConcern(1, 100); //$writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 100); $result = $manager->executeBulkWrite('db.location', $bulk, $writeConcern); if ($result->getWriteErrors()) { return false; } return true; }
查詢附近的人(返回結果沒有距離,支援查詢條件,支援分頁)
public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0) { $filter = array( 'loc' => array( '$near' => array($lon, $lat), ), ); if ($maxDistance) { $filter['loc']['$maxDistance'] = $maxDistance; } if ($where) { $filter = array_merge($filter, $where); } $options = array(); if ($page) { $page > 1 ? $skip = ($page - 1) * $this->pageCount : $skip = 0; $options = [ 'limit' => $this->pageCount, 'skip' => $skip ]; } $query = new MongoDB\Driver\Query($filter, $options); $manager = $this->getMongoManager(); $cursor = $manager->executeQuery('db.location', $query); $list = $cursor->toArray(); return $list; }
查詢附近的人(返回結果帶距離,支援查詢條件,支付返回數量,不支援分頁):
public function geoNearFindReturnDistance($lon, $lat, $maxDistance = 0, $where = array(), $num = 0) { $params = array( 'geoNear' => "location", 'near' => array($lon, $lat), 'spherical' => true, // spherical設為false(預設),dis的單位與座標的單位保持一致,spherical設為true,dis的單位是弧度 'distanceMultiplier' => 6371, // 計算成公里,座標單位distanceMultiplier: 111。 弧度單位 distanceMultiplier: 6371 ); if ($maxDistance) { $params['maxDistance'] = $maxDistance; } if ($num) { $params['num'] = $num; } if ($where) { $params['query'] = $where; } $command = new MongoDB\Driver\Command($params); $manager = $this->getMongoManager(); $cursor = $manager->executeCommand('db', $command); $response = (array) $cursor->toArray()[0]; $list = $response['results']; return $list; }
注意事項:
1.選擇好擴充套件,mongo和mongodb擴充套件寫法差別很大
2.寫資料時出現noreply請檢查寫入確認級別
3.使用find查詢的資料需要自己計算距離,使用geoNear查詢的不支援分頁
4.使用geoNear查詢的距離需要轉化成km使用spherical和distanceMultiplier引數
上述demo可以戳這裡:demo
以上介紹了三種方式去實現查詢附近的人的功能,各種方式都有各自的適用場景,比如資料行比較少,例如查詢使用者和幾座城市之間的距離使用Mysql就足夠了,如果需要實時快速響應並且普通查詢範圍內的距離,可以使用Redis,但如果資料量大並且多種屬性篩選條件,使用mongo會更方便,以上只是建議,具體實現方案還要視具體業務去進行方案評審。
以上就是教你使用PHP實現查詢你想要的附近人的詳細內容,更多請關注TW511.COM其它相關文章!