使用PHP实现查找附近的人
https://zhuanlan.zhihu.com/p/31380780
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实现查找附近的人的更多相关文章
- redis 查找附近的人
儿童定位手表,有个交友功能,查找附近的人,用redis的geo来实现比较简单,其实是一个ZSET(有序集合) redis 版本要大于3.2 查看redis 版本 /usr/bin/redis-s ...
- Redis(6)——GeoHash查找附近的人
像微信 "附近的人",美团 "附近的餐厅",支付宝共享单车 "附近的车" 是怎么设计实现的呢? 一.使用数据库实现查找附近的人 我们都知道, ...
- Redis实战篇(四)基于GEO实现查找附近的人功能
如果现在要开发一个功能: 要为一款交友App实现查找附近的人,并按距离进行排序. 让你来开发这个功能,你会如何实现? MySQL 不合适 你可能想到,把用户用户的经纬度坐标使用MySQL等关系数据库( ...
- 终极二分查找--传说十个人写九个有bug
之前写过一篇极为罗嗦的二分查找,非常得意地以为以后就可以避免踩坑了,但是今天才知道二分查找可以写的既简洁又鲁棒,唉!还是要多学习啊! 给一个按照从大到小的顺序排序好的数组a[]={1,2,3,4,7, ...
- IntelliJ IDEA - 查找代码提交人
转载. https://blog.csdn.net/abcyyjjkk/article/details/88995503 如果Annocation不可用
- (转)javascript中的对象查找
本文转自:http://otakustay.com/object-lookup-in-javascript/ ---很棒的一篇文章,作者的其他文章还暂时没读,但相信作者是一个谦虚 谨慎的好工程师 近 ...
- SpringBoot入门教程(五)Java基于MySQL实现附近的人
“附近的人”这个功能估计都不陌生,与之类似的功能最开始是在各大地图应用上接触过,比如搜附近的电影院,附近的超市等等.然而真正让附近的人火遍大江南北的应该是微信"附近的人"这个功能, ...
- Redis 到底是怎么实现“附近的人”这个功能的?
前言:针对“附近的人”这一位置服务领域的应用场景,常见的可使用PG.MySQL和MongoDB等多种DB的空间索引进行实现.而Redis另辟蹊径,结合其有序队列zset以及geohash编码,实现了空 ...
- IM里“附近的人”功能实现原理是什么?如何高效率地实现它?
1.引言 基本上以陌生人社交为主的IM产品里,都会增加“附近的人”.“附近的xxx”这种以LBS(地理位置)为导向的产品特色(微信这个熟人社交产品里为啥也有“附近的人”?这当然是历史原因了,微信当初还 ...
随机推荐
- kubernetes master节点部署(三)
一.部署kubernetes api服务 1.1.准备软件包 [root@linux-node1 ~]# cd /usr/local/src/kubernetes [root@linux-node1 ...
- IoC与DI的理解
首先要分享的是Iteye的开涛这位技术牛人对Spring框架的IOC的理解,写得非常通俗易懂,以下内容全部来自原文,原文地址:http://jinnianshilongnian.iteye.com/b ...
- GoogLeNet网络的Pytorch实现
1.文章原文地址 Going deeper with convolutions 2.文章摘要 我们提出了一种代号为Inception的深度卷积神经网络,它在ILSVRC2014的分类和检测任务上都取得 ...
- Java对象内存分配原理与布局
当一个对象被创建了,那在JVM中是如何的从一个对象不存在到存到,然后将对象存放在什么地方呢?这次主要来探讨一下Java对象创建的过程. new关键字创建对象的3个步骤: 1.在堆内存中创建出对象的实例 ...
- HTTP/TCP/IP UDP Socket等区别联系
1.TCP连接 手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接.TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上. ...
- 什么是EDID,EDID能做什么,EDID基本介绍
转至 http://blog.csdn.net/Jkf40622/article/details/48311455 Q1: 为什么要写这篇文章? A1:在最近的工作中遇到了不少问题,其中很多都是和ED ...
- Scrapy框架的八个扩展
一.proxies代理 首先需要在环境变量中设置 from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddlewa ...
- POJ-2065-SETI(高斯消元)
链接: https://vjudge.net/problem/POJ-2065 题意: For some years, quite a lot of work has been put into li ...
- sql server 存储过程中,调用事务 tran
Sql Server 2005/2008中提供了begin tran,commit tran和rollback tran来使用事务. begin tran表示开始事务, commit tran表示 ...
- 使用webuploader实现分片上传
这里只写后端的代码,基本的思想就是,前端将文件分片,然后每次访问上传接口的时候,向后端传入参数:当前为第几块文件,和分片总数 下面直接贴代码吧,一些难懂的我大部分都加上注释了: 上传文件实体类: 看得 ...