【问题标题】:How to efficiently find the closest locations nearby a given location如何有效地找到给定位置附近的最近位置
【发布时间】:2011-04-24 18:11:08
【问题描述】:

我正在制作一个脚本,其中将大量业务加载到具有纬度和经度的 mySQL 数据库中。然后我为该脚本提供纬度和经度(最终用户的),脚本必须计算从提供的纬度/经度到它从数据库中获取的每个条目的距离,并按照最近到最远的顺序对它们进行排序.

我实际上只需要大约 10 或 20 个“最近”结果,但除了从数据库中获取所有结果并在每个结果上运行函数然后进行数组排序之外,我想不出其他办法。

这是我已经拥有的:

<?php

function getDistance($point1, $point2){

    $radius      = 3958;      // Earth's radius (miles)
    $pi          = 3.1415926;
    $deg_per_rad = 57.29578;  // Number of degrees/radian (for conversion)

    $distance = ($radius * $pi * sqrt(
                ($point1['lat'] - $point2['lat'])
                * ($point1['lat'] - $point2['lat'])
                + cos($point1['lat'] / $deg_per_rad)  // Convert these to
                * cos($point2['lat'] / $deg_per_rad)  // radians for cos()
                * ($point1['long'] - $point2['long'])
                * ($point1['long'] - $point2['long'])
        ) / 180);

    $distance = round($distance,1);
    return $distance;  // Returned using the units used for $radius.
}

include("../includes/application_top.php");

$lat = (is_numeric($_GET['lat'])) ? $_GET['lat'] : 0;
$long = (is_numeric($_GET['long'])) ? $_GET['long'] : 0;

$startPoint = array("lat"=>$lat,"long"=>$long);

$sql = "SELECT * FROM mellow_listings WHERE active=1"; 
$result = mysql_query($sql);

while($row = mysql_fetch_array($result)){
    $thedistance = getDistance($startPoint,array("lat"=>$row['lat'],"long"=>$row['long']));
    $data[] = array('id' => $row['id'],
                    'name' => $row['name'],
                    'description' => $row['description'],
                    'lat' => $row['lat'],
                    'long' => $row['long'],
                    'address1' => $row['address1'],
                    'address2' => $row['address2'],
                    'county' => $row['county'],
                    'postcode' => strtoupper($row['postcode']),
                    'phone' => $row['phone'],
                    'email' => $row['email'],
                    'web' => $row['web'],
                    'distance' => $thedistance);
}

// integrate google local search
$url = "http://ajax.googleapis.com/ajax/services/search/local?";
$url .= "q=Off+licence";    // query
$url .= "&v=1.0";           // version number
$url .= "&rsz=8";           // number of results
$url .= "&key=ABQIAAAAtG"
        ."Pcon1WB3b0oiqER"
        ."FZ-TRQgsWYVg721Z"
        ."IDPMPlc4-CwM9Xt"
        ."FBSTZxHDVqCffQ2"
        ."W6Lr4bm1_zXeYoQ"; // api key
$url .= "&sll=".$lat.",".$long;

// sendRequest
// note how referer is set manually
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_REFERER, /* url */);
$body = curl_exec($ch);
curl_close($ch);

// now, process the JSON string
$json = json_decode($body, true);

foreach($json['responseData']['results'] as $array){

    $thedistance = getDistance($startPoint,array("lat"=>$array['lat'],"long"=>$array['lng']));
    $data[] = array('id' => '999',
                    'name' => $array['title'],
                    'description' => '',
                    'lat' => $array['lat'],
                    'long' => $array['lng'],
                    'address1' => $array['streetAddress'],
                    'address2' => $array['city'],
                    'county' => $array['region'],
                    'postcode' => '',
                    'phone' => $array['phoneNumbers'][0],
                    'email' => '',
                    'web' => $array['url'],
                    'distance' => $thedistance);

}

// sort the array
foreach ($data as $key => $row) {
$id[$key] = $row['id'];
$distance[$key] = $row['distance'];
}

array_multisort($distance, SORT_ASC, $data); 

header("Content-type: text/xml"); 


echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'."\n";
echo '<plist version="1.0">'."\n";
echo '<array>'."\n";

for($i = 0; isset($distance[$i]); $i++){
    //echo $data[$i]['id']." -> ".$distance[$i]."<br />";
    echo '<dict>'."\n";
        foreach($data[$i] as $key => $val){
            echo '<key><![CDATA['.$key.']]></key>'."\n";
            echo '<string><![CDATA['.htmlspecialchars_decode($val, ENT_QUOTES).']]></string>'."\n";
        }
    echo '</dict>'."\n";
}

echo '</array>'."\n";
echo '</plist>'."\n";
?>

现在,在数据库中只有 2 或 3 个企业的情况下,它运行得足够快,但我目前正在将 5k 企业加载到数据库中,我担心每个条目运行它会非常慢?你怎么看?

它也不是我可以缓存的那种数据,因为两个用户具有相同纬度/经度的可能性非常罕见,因此无济于事。

对此我能做些什么?

感谢您的帮助和任何建议。他们都非常感谢。

【问题讨论】:

  • 也许你应该改写你的标题,因为你正在寻找一种解决方案,如何有效地找到给定位置附近的最近位置。

标签: php mysql


【解决方案1】:

解决这个问题的方法要简单得多。

  1. 我们知道,在完全相同的经度上,0.1 的纬度差异等于 11.12 公里的距离。 (纬度 1.0 表示距离为 111.2 公里)

  2. 同样,经度差为 0.1,同纬度距离为 3.51 公里(1.0 经度将使该距离为 85.18 公里) (要转换成英里,我们乘以 1.60934)

注意。请注意,经度从 -180 到 180,因此 -180 到 179.9 之间的差为 0.1,即 3.51 公里。

我们现在需要知道的是所有带有 lon 和 lat 的邮政编码列表(你已经知道了)

因此,现在要将搜索范围缩小 90%,例如,您只需删除绝对不会在 100 公里范围内的所有结果。 我们的坐标 $lat1 和 $lon2 对于 100 公里,纬度和经度相差 2 就绰绰有余了。

$lon=...;
$lat=...;
$dif=2;

SELECT zipcode from zipcode_table WHERE latitude>($lan-$dif) AND latitude<($lan+$dif) AND longitude>($lon-$dif) AND longitude<($lon+$dif)

类似的东西。 当然,如果您需要覆盖更小或更大的区域,则需要相应地更改 $dif。

这种方式 Mysql 只会考虑非常有限的节省 comp 资源。

【讨论】:

    【解决方案2】:

    选项 1: 通过切换到支持 GeoIP 的数据库对数据库进行计算。

    选项 2: 对数据库进行计算:您使用的是 MySQL,因此以下存储过程应该会有所帮助

    CREATE FUNCTION distance (latA double, lonA double, latB double, LonB double)
        RETURNS double DETERMINISTIC
    BEGIN
        SET @RlatA = radians(latA);
        SET @RlonA = radians(lonA);
        SET @RlatB = radians(latB);
        SET @RlonB = radians(LonB);
        SET @deltaLat = @RlatA - @RlatB;
        SET @deltaLon = @RlonA - @RlonB;
        SET @d = SIN(@deltaLat/2) * SIN(@deltaLat/2) +
        COS(@RlatA) * COS(@RlatB) * SIN(@deltaLon/2)*SIN(@deltaLon/2);
        RETURN 2 * ASIN(SQRT(@d)) * 6371.01;
    END//
    

    编辑

    如果您的数据库中有纬度和经度索引,您可以通过在 PHP 中计算初始边界框($minLat、$maxLat、$minLong 和 $maxLong)来减少需要计算的计算次数,并基于此将行限制为条目的子集(WHERE latitude BETWEEN $minLat AND $maxLat AND longitude BETWEEN $minLong AND $maxLong)。那么 MySQL 只需要对该行的子集执行距离计算。

    进一步编辑(作为对先前编辑的解释)

    如果您只是使用 Jonathon 提供的 SQL 语句(或存储过程来计算距离),那么 SQL 仍然需要查看数据库中的每条记录,并在之前计算数据库中每条记录的距离它可以决定是返回该行还是丢弃它。

    由于计算执行起来相对较慢,最好减少需要计算的行集,消除明显超出所需距离的行,这样我们只执行较少行数的昂贵计算。

    如果您认为您正在做的基本上是在地图上画一个圆,以您的初始点为中心,并以距离为半径;那么这个公式只是简单地确定哪些行落在那个圆圈内……但它仍然需要检查每一行。

    使用边界框就像首先在地图上绘制一个正方形,左、右、上、下边缘与我们的中心点相距适当的距离。然后,我们的圆圈将在该框内绘制,圆圈上的最北端、最东端、最南端和最西点与框的边界相接触。有些行会落在该框之外,因此 SQL 甚至不会费心尝试计算这些行的距离。它只计算落在边界框内的那些行的距离,以查看它们是否也在圆圈内。

    在 PHP 中,我们可以使用一个非常简单的计算,根据距离计算出最小和最大纬度和经度,然后在 SQL 语句的 WHERE 子句中设置这些值。这实际上是我们的盒子,任何超出该盒子的东西都会被自动丢弃,而无需实际计算它的距离。

    Movable Type website 上对此有很好的解释(使用 PHP 代码),对于计划在 PHP 中进行任何地理定位工作的人来说,这应该是必不可少的阅读材料。

    【讨论】:

    • 非常感谢,我有两个问题。 1 - 什么是存储过程?我将如何实现它,然后使用它? 2 - 这比 Jonathon 的回答更有效还是更好?
    • “存储过程”基本上是存储在数据库服务器本身中的用户定义的代码块。您可以在dev.mysql.com/doc/refman/5.1/en/stored-routines.html 阅读有关 MySQL 存储过程的信息,它与 Jonathon 的代码基本相同,但您只需在 select 中引用新函数 distance(),而不是 select 语句中的所有数学运算(传入适当的参数)就像你对任何 MySQL 内置函数所做的那样
    • 注意,6371.01 是以公里为单位的半径乘数,如果您想要以英里为单位的值,请将其更改为 3958
    【解决方案3】:

    如果您有很多点,其中包含距离公式的查询将非常慢,因为它没有使用索引进行搜索。为了提高效率,您要么必须使用矩形边界框以使其更快,要么可以使用内置 GIS 功能的数据库。PostGIS 是免费的,这里有一篇关于进行最近邻搜索的文章:

    http://www.bostongis.com/PrinterFriendly.aspx?content_name=postgis_nearest_neighbor_generic

    【讨论】:

      【解决方案4】:

      我认为使用 SQL 中的 Haversine formula 可以更好地实现您想要实现的目标。 Google 有一个关于如何get the nearest locations in a MySQL database 的教程,但总体思路是这样的 SQL:

      SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) )
        * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) 
        * sin( radians( lat ) ) ) ) AS distance
      FROM markers
      HAVING distance < 25
      ORDER BY distance LIMIT 0 , 20;
      

      那么你需要做的所有工作都是在数据库上完成的,所以你不用在检查距离之前把所有的业务都拉到你的PHP脚本中。

      【讨论】:

      • 不知道那个公式,看起来很有用。感谢分享!
      • 很简单,用SQL就能搞定!
      • 要按公里而不是英里搜索,请将 3959 替换为 6371
      • 最后一件事你能告诉我们这是否适用于数千条记录。谢谢
      猜你喜欢
      • 1970-01-01
      • 2011-02-13
      • 2019-10-30
      • 2018-08-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-28
      • 1970-01-01
      相关资源
      最近更新 更多