在共享网络主机上优化基于邻近度的商店位置搜索?

时间:2010-08-17 作者:MikeSchinkel

我有一个项目,我需要为客户构建一个商店定位器。

我正在使用自定义帖子类型“restaurant-location“我已经编写了代码,使用Google Geocoding API (以下是链接geocodes the US White House in JSON 我已经将纬度和经度存储回自定义字段。

我写了一个get_posts_by_geo_distance() 函数,按地理位置最接近的帖子的顺序返回帖子列表the formula I found in the slideshow at this post. 您可以这样调用我的函数(我从固定的“源”lat/long开始):

include "wp-load.php";

$source_lat = 30.3935337;
$source_long = -86.4957833;

$results = get_posts_by_geo_distance(
    \'restaurant-location\',
    \'geo_latitude\',
    \'geo_longitude\',
    $source_lat,
    $source_long);

echo \'<ul>\';
foreach($results as $post) {
    $edit_url = get_edit_url($post->ID);
    echo "<li>{$post->distance}: <a href=\\"{$edit_url}\\" target=\\"_blank\\">{$post->location}</a></li>";
}
echo \'</ul>\';
return;
下面是函数get_posts_by_geo_distance() 本身:

function get_posts_by_geo_distance($post_type,$lat_key,$lng_key,$source_lat,$source_lng) {
    global $wpdb;
    $sql =<<<SQL
SELECT
    rl.ID,
    rl.post_title AS location,
    ROUND(3956*2*ASIN(SQRT(POWER(SIN(({$source_lat}-abs(lat.lat))*pi()/180/2),2)+
    COS({$source_lat}*pi()/180)*COS(abs(lat.lat)*pi()/180)*
    POWER(SIN(({$source_lng}-lng.lng)*pi()/180/2),2))),3) AS distance
FROM
    wp_posts rl
    INNER JOIN (SELECT post_id,CAST(meta_value AS DECIMAL(11,7)) AS lat FROM wp_postmeta lat WHERE lat.meta_key=\'{$lat_key}\') lat ON lat.post_id = rl.ID
    INNER JOIN (SELECT post_id,CAST(meta_value AS DECIMAL(11,7)) AS lng FROM wp_postmeta lng WHERE lng.meta_key=\'{$lng_key}\') lng ON lng.post_id = rl.ID
WHERE
    rl.post_type=\'{$post_type}\' AND rl.post_name<>\'auto-draft\'
ORDER BY
    distance
SQL;
    $sql = $wpdb->prepare($sql,$source_lat,$source_lat,$source_lng);
    return $wpdb->get_results($sql);
}
我担心的是,SQL是你能得到的最不优化的。MySQL无法按任何可用索引排序,因为源地理位置是可变的,并且没有有限的源地理位置可缓存。目前,我很难找到优化它的方法。

考虑到我已经做了什么,问题是:How would you go about optimizing this use-case?

如果有更好的解决方案会让我放弃,那么我保留我做过的任何事情都不重要我愿意考虑几乎任何解决方案,除了需要安装Sphinx服务器或任何需要定制MySQL配置的解决方案。基本上,这个解决方案需要能够在任何普通的WordPress安装上运行。(也就是说,如果有人想为其他可能更先进的人和子孙后代列出替代解决方案,那就太好了。)

参考资料:仅供参考,我对此做了一些研究,所以我不会让你再做研究,也不会让你发布这些链接作为答案,我会继续并包括它们。

3 个回复
SO网友:Jan Fabry

这对你来说可能太晚了,但我还是会回复你a similar answer as I gave to this related question, 因此,未来的访问者可以参考这两个问题。

我不会将这些值存储在post元数据表中,或者至少不会仅存储在那里。你想要一张桌子吗post_id, lat, lon 列,以便可以放置lat, lon 并对此提出质疑。在后期保存和更新时使用挂钩来保持最新状态应该不会太难。

查询数据库时,可以在起点周围定义一个边界框,以便对所有lat, lon 框的南北边界和东西边界之间成对。

获得此简化结果后,可以执行更高级的(圆形或实际行驶方向)距离计算,以过滤出位于边界框角落中的位置,从而使其远离所需距离。

在这里,您可以找到一个在管理领域工作的简单代码示例。您需要自己创建额外的数据库表。代码按从最有趣到最不有趣的顺序排列。

<?php
/*
Plugin Name: Monkeyman geo test
Plugin URI: http://www.monkeyman.be
Description: Geolocation test
Version: 1.0
Author: Jan Fabry
*/

class Monkeyman_Geo
{
    public function __construct()
    {
        add_action(\'init\', array(&$this, \'registerPostType\'));
        add_action(\'save_post\', array(&$this, \'saveLatLon\'), 10, 2);

        add_action(\'admin_menu\', array(&$this, \'addAdminPages\'));
    }

    /**
     * On post save, save the metadata in our special table
     * (post_id INT, lat DECIMAL(10,5), lon DECIMAL (10,5))
     * Index on lat, lon
     */
    public function saveLatLon($post_id, $post)
    {
        if ($post->post_type != \'monkeyman_geo\') {
            return;
        }
        $lat = floatval(get_post_meta($post_id, \'lat\', true));
        $lon = floatval(get_post_meta($post_id, \'lon\', true));

        global $wpdb;
        $result = $wpdb->replace(
            $wpdb->prefix . \'monkeyman_geo\',
            array(
                \'post_id\' => $post_id,
                \'lat\' => $lat,
                \'lon\' => $lon,
            ),
            array(\'%s\', \'%F\', \'%F\')
        );
    }

    public function addAdminPages()
    {
        add_management_page( \'Quick location generator\', \'Quick generator\', \'edit_posts\', __FILE__  . \'generator\', array($this, \'doGeneratorPage\'));
        add_management_page( \'Location test\', \'Location test\', \'edit_posts\', __FILE__ . \'test\', array($this, \'doTestPage\'));

    }

    /**
     * Simple test page with a location and a distance
     */
    public function doTestPage()
    {
        if (!array_key_exists(\'search\', $_REQUEST)) {
            $default_lat = ini_get(\'date.default_latitude\');
            $default_lon = ini_get(\'date.default_longitude\');

            echo <<<EOF
<form action="" method="post">
    <p>Center latitude: <input size="10" name="center_lat" value="{$default_lat}"/>
        <br/>Center longitude: <input size="10" name="center_lon" value="{$default_lon}"/>
        <br/>Max distance (km): <input size="5" name="max_distance" value="100"/></p>
    <p><input type="submit" name="search" value="Search!"/></p>
</form>
EOF;
            return;
        }
        $center_lon = floatval($_REQUEST[\'center_lon\']);
        $center_lat = floatval($_REQUEST[\'center_lat\']);
        $max_distance = floatval($_REQUEST[\'max_distance\']);

        var_dump(self::getPostsUntilDistanceKm($center_lon, $center_lat, $max_distance));
    }

    /**
     * Get all posts that are closer than the given distance to the given location
     */
    public static function getPostsUntilDistanceKm($center_lon, $center_lat, $max_distance)
    {
        list($north_lat, $east_lon, $south_lat, $west_lon) = self::getBoundingBox($center_lat, $center_lon, $max_distance);

        $geo_posts = self::getPostsInBoundingBox($north_lat, $east_lon, $south_lat, $west_lon);

        $close_posts = array();
        foreach ($geo_posts as $geo_post) {
            $post_lat = floatval($geo_post->lat);
            $post_lon = floatval($geo_post->lon);
            $post_distance = self::calculateDistanceKm($center_lat, $center_lon, $post_lat, $post_lon);
            if ($post_distance < $max_distance) {
                $close_posts[$geo_post->post_id] = $post_distance;
            }
        }
        return $close_posts;
    }

    /**
     * Select all posts ids in a given bounding box
     */
    public static function getPostsInBoundingBox($north_lat, $east_lon, $south_lat, $west_lon)
    {
        global $wpdb;
        $sql = $wpdb->prepare(\'SELECT post_id, lat, lon FROM \' . $wpdb->prefix . \'monkeyman_geo WHERE lat < %F AND lat > %F AND lon < %F AND lon > %F\', array($north_lat, $south_lat, $west_lon, $east_lon));
        return $wpdb->get_results($sql, OBJECT_K);
    }

    /* Geographical calculations: distance and bounding box */

    /**
     * Calculate the distance between two coordinates
     * http://stackoverflow.com/questions/365826/calculate-distance-between-2-gps-coordinates/1416950#1416950
     */
    public static function calculateDistanceKm($a_lat, $a_lon, $b_lat, $b_lon)
    {
        $d_lon = deg2rad($b_lon - $a_lon);
        $d_lat = deg2rad($b_lat - $a_lat);
        $a = pow(sin($d_lat/2.0), 2) + cos(deg2rad($a_lat)) * cos(deg2rad($b_lat)) * pow(sin($d_lon/2.0), 2);
        $c = 2 * atan2(sqrt($a), sqrt(1-$a));
        $d = 6367 * $c;

        return $d;
    }

    /**
     * Create a box around a given point that extends a certain distance in each direction
     * http://www.colorado.edu/geography/gcraft/warmup/aquifer/html/distance.html
     *
     * @todo: Mind the gap at 180 degrees!
     */
    public static function getBoundingBox($center_lat, $center_lon, $distance_km)
    {
        $one_lat_deg_in_km = 111.321543; // Fixed
        $one_lon_deg_in_km = cos(deg2rad($center_lat)) * 111.321543; // Depends on latitude

        $north_lat = $center_lat + ($distance_km / $one_lat_deg_in_km);
        $south_lat = $center_lat - ($distance_km / $one_lat_deg_in_km);

        $east_lon = $center_lon - ($distance_km / $one_lon_deg_in_km);
        $west_lon = $center_lon + ($distance_km / $one_lon_deg_in_km);

        return array($north_lat, $east_lon, $south_lat, $west_lon);
    }

    /* Below this it\'s not interesting anymore */

    /**
     * Generate some test data
     */
    public function doGeneratorPage()
    {
        if (!array_key_exists(\'generate\', $_REQUEST)) {
            $default_lat = ini_get(\'date.default_latitude\');
            $default_lon = ini_get(\'date.default_longitude\');

            echo <<<EOF
<form action="" method="post">
    <p>Number of posts: <input size="5" name="post_count" value="10"/></p>
    <p>Center latitude: <input size="10" name="center_lat" value="{$default_lat}"/>
        <br/>Center longitude: <input size="10" name="center_lon" value="{$default_lon}"/>
        <br/>Max distance (km): <input size="5" name="max_distance" value="100"/></p>
    <p><input type="submit" name="generate" value="Generate!"/></p>
</form>
EOF;
            return;
        }
        $post_count = intval($_REQUEST[\'post_count\']);
        $center_lon = floatval($_REQUEST[\'center_lon\']);
        $center_lat = floatval($_REQUEST[\'center_lat\']);
        $max_distance = floatval($_REQUEST[\'max_distance\']);

        list($north_lat, $east_lon, $south_lat, $west_lon) = self::getBoundingBox($center_lat, $center_lon, $max_distance);


        add_action(\'save_post\', array(&$this, \'setPostLatLon\'), 5);
        $precision = 100000;
        for ($p = 0; $p < $post_count; $p++) {
            self::$currentRandomLat = mt_rand($south_lat * $precision, $north_lat * $precision) / $precision;
            self::$currentRandomLon = mt_rand($west_lon * $precision, $east_lon * $precision) / $precision;

            $location = sprintf(\'(%F, %F)\', self::$currentRandomLat, self::$currentRandomLon);

            $post_data = array(
                \'post_status\' => \'publish\',
                \'post_type\' => \'monkeyman_geo\',
                \'post_content\' => \'Point at \' . $location,
                \'post_title\' => \'Point at \' . $location,
            );

            var_dump(wp_insert_post($post_data));
        }
    }

    public static $currentRandomLat = null;
    public static $currentRandomLon = null;

    /**
     * Because I didn\'t know how to save meta data with wp_insert_post,
     * I do it here
     */
    public function setPostLatLon($post_id)
    {
        add_post_meta($post_id, \'lat\', self::$currentRandomLat);
        add_post_meta($post_id, \'lon\', self::$currentRandomLon);
    }

    /**
     * Register a simple post type for us
     */
    public function registerPostType()
    {
        register_post_type(
            \'monkeyman_geo\',
            array(
                \'label\' => \'Geo Location\',
                \'labels\' => array(
                    \'name\' => \'Geo Locations\',
                    \'singular_name\' => \'Geo Location\',
                    \'add_new\' => \'Add new\',
                    \'add_new_item\' => \'Add new location\',
                    \'edit_item\' => \'Edit location\',
                    \'new_item\' => \'New location\',
                    \'view_item\' => \'View location\',
                    \'search_items\' => \'Search locations\',
                    \'not_found\' => \'No locations found\',
                    \'not_found_in_trash\' => \'No locations found in trash\',
                    \'parent_item_colon\' => null,
                ),
                \'description\' => \'Geographical locations\',
                \'public\' => true,
                \'exclude_from_search\' => false,
                \'publicly_queryable\' => true,
                \'show_ui\' => true,
                \'menu_position\' => null,
                \'menu_icon\' => null,
                \'capability_type\' => \'post\',
                \'capabilities\' => array(),
                \'hierarchical\' => false,
                \'supports\' => array(
                    \'title\',
                    \'editor\',
                    \'custom-fields\',
                ),
                \'register_meta_box_cb\' => null,
                \'taxonomies\' => array(),
                \'permalink_epmask\' => EP_PERMALINK,
                \'rewrite\' => array(
                    \'slug\' => \'locations\',
                ),
                \'query_var\' => true,
                \'can_export\' => true,
                \'show_in_nav_menus\' => true,
            )
        );
    }
}

$monkeyman_Geo_instance = new Monkeyman_Geo();

SO网友:goldenapples

这次我来晚了,但现在回想起来get_post_meta 是这里真正的问题,而不是您使用的SQL查询。

最近,我不得不在我运行的一个站点上执行类似的地理查找,而不是使用元表来存储lat和lon(这最多需要两个连接来查找,如果使用get\\u post\\u meta,每个位置还需要两个数据库查询),我创建了一个具有空间索引几何点数据类型的新表。

我的查询看起来很像你的查询,MySQL做了很多繁重的工作(我省略了trig函数,将所有内容简化为二维空间,因为它足够接近我的目的):

function nearby_property_listings( $number = 5 ) {
    global $client_location, $wpdb;

    //sanitize public inputs
    $lat = (float)$client_location[\'lat\'];  
    $lon = (float)$client_location[\'lon\']; 

    $sql = $wpdb->prepare( "SELECT *, ROUND( SQRT( ( ( ( Y(geolocation) - $lat) * 
                                                       ( Y(geolocation) - $lat) ) *
                                                         69.1 * 69.1) +
                                                  ( ( X(geolocation) - $lon ) * 
                                                       ( X(geolocation) - $lon ) * 
                                                         53 * 53 ) ) ) as distance
                            FROM {$wpdb->properties}
                            ORDER BY distance LIMIT %d", $number );

    return $wpdb->get_results( $sql );
}
其中,$client\\u location是公共地理IP查找服务返回的值(我使用了geoio.com,但也有许多类似的服务)

它可能看起来很笨拙,但在测试中,它始终在0.4秒内返回80000行表中最近的5个位置。

在MySQL推出所提议的距离函数之前,这似乎是我发现的实现位置查找的最佳方法。

EDIT: 添加此特定表的表结构。它是一组属性列表,因此它可能与其他任何用例相似,也可能不相似。

CREATE TABLE IF NOT EXISTS `rh_properties` (
  `listingId` int(10) unsigned NOT NULL,
  `listingType` varchar(60) collate utf8_unicode_ci NOT NULL,
  `propertyType` varchar(60) collate utf8_unicode_ci NOT NULL,
  `status` varchar(20) collate utf8_unicode_ci NOT NULL,
  `street` varchar(64) collate utf8_unicode_ci NOT NULL,
  `city` varchar(24) collate utf8_unicode_ci NOT NULL,
  `state` varchar(5) collate utf8_unicode_ci NOT NULL,
  `zip` decimal(5,0) unsigned zerofill NOT NULL,
  `geolocation` point NOT NULL,
  `county` varchar(64) collate utf8_unicode_ci NOT NULL,
  `bedrooms` decimal(3,2) unsigned NOT NULL,
  `bathrooms` decimal(3,2) unsigned NOT NULL,
  `price` mediumint(8) unsigned NOT NULL,
  `image_url` varchar(255) collate utf8_unicode_ci NOT NULL,
  `description` mediumtext collate utf8_unicode_ci NOT NULL,
  `link` varchar(255) collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (`listingId`),
  KEY `geolocation` (`geolocation`(25))
)
Thegeolocation 列是与此处目的相关的唯一内容;它由x(lon)、y(lat)坐标组成,在将新值导入数据库时,我只是从地址中查找这些坐标。

SO网友:hakre

只需预先计算所有实体之间的距离。我会自己将其存储到一个数据库表中,并具有索引值的能力。

结束

相关推荐