Оптимізація пошуку місця розташування магазину на основі близькості на спільному веб-хості?


11

У мене є проект, де мені потрібно побудувати локатор магазину для клієнта.

Я використовую користувальницький тип пошти " restaurant-location" і я написав код для геокодування адрес, збережених у postmeta за допомогою API геокодування Google (ось тут посилання, яке геокодує Білий дім США в JSON, і я зберігав широту і довготу назад до спеціальних полів.

Я написав get_posts_by_geo_distance()функцію, яка повертає список публікацій в порядку тих, які є найближчими географічно, використовуючи формулу, яку я знайшов у слайд-шоу в цій публікації . Ви можете назвати мою функцію так (я починаю з фіксованого "джерела" 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 не може замовити будь-який доступний індекс, оскільки гео джерела є змінним і не існує обмеженого набору геошоу джерела для кешування. В даний час я натрапив на шляхи оптимізації.

Беручи до уваги те, що я вже зробив, питання полягає в тому, як би ви зробили оптимізацію цього випадку використання?

Не важливо, що я зберігаю все, що я зробив, якщо краще рішення дозволило б мені це викинути. Я готовий розглянути практично будь-яке рішення, за винятком рішення, яке вимагає зробити щось на зразок встановлення сервера Sphinx або будь-чого, що вимагає налаштованої конфігурації MySQL. В основному рішення повинно мати можливість працювати на будь-якій звичайній ванільній програмі WordPress. (Це означає, що було б чудово, якщо хтось хоче перелічити альтернативні рішення для інших, які могли б отримати більш просунуті та для нащадків.)

Знайдені ресурси

FYI, я трохи зробив дослідження з цього питання, а не ти робиш дослідження знову, а не опублікувавш будь-яке з цих посилань як відповідь, я продовжую їх і включу.

Щодо пошуку Сфінкса

Відповіді:


6

Яка точність вам потрібна? якщо це загальнодержавний / загальнодержавний пошук, можливо, ви могли б зробити Lat-lon для поштового пошуку та попередньо обчислити відстань від поштової області до поштової зони ресторану. Якщо вам потрібні точні відстані, це не буде хорошим варіантом.

Ви повинні заглянути в рішення Geohash , у статті Вікіпедії є посилання на бібліотеку PHP, щоб кодувати розшифровку lat на long-geohashs.

Тут ви маєте гарну статтю, яка пояснює, чому і як вони використовують її в Google App Engine (код Python, але простий у виконанні.) Через необхідність використання geohash в GAE ви можете знайти кілька хороших бібліотек і приклади python.

Як пояснюється в цьому дописі в блозі , перевагою використання геохашей є те, що ви можете створити індекс у таблиці MySQL у цьому полі.


Дякуємо за пропозицію на GeoHash! Я точно перевіряю це, але виїжджаю до WordCamp Savannah через годину, тому зараз не можу. Це локатор ресторану для туристів, які відвідують місто, тому 0,1 милі, ймовірно, буде мінімальною точністю. В ідеалі було б краще за це. Я відредагую ваші посилання!
MikeSchinkel

Якщо ви збираєтеся відобразити результати на мапі Google ви можете використовувати їх API , щоб зробити сортування code.google.com/apis/maps/documentation/mapsdata / ...

Оскільки це найцікавіша відповідь, я збираюся її прийняти, хоча я не встиг дослідити та спробувати її.
MikeSchinkel

9

Це може бути для вас занадто пізно, але я все одно відповім із подібною відповіддю, як і на це питання , тому майбутні відвідувачі можуть звернутися до обох питань.

Я б не зберігав ці значення в таблиці метаданих після або принаймні не тільки там. Ви хочете , щоб таблиця з 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();

@Jan : Дякую за відповідь. Як ви думаєте, ви можете надати якийсь фактичний код, що відображає ці реалізовані?
MikeSchinkel

@Mike: Це був цікавий виклик, але ось якийсь код, який повинен працювати.
Ян Фабрі

@Jan Fabry: Класно! Я перевірю це, коли відскочу на той проект.
MikeSchinkel

1

Я спізнююся на учасник цього питання, але озираючись на це, get_post_metaтут справді проблема, а не запит SQL, який ви використовуєте.

Нещодавно мені довелося зробити подібний гео-пошук на веб-сайті, на якому я запускаю, а не використовувати мета-таблицю для зберігання lat та lon (для пошуку в кращому випадку потрібно два приєднання, і, якщо ви використовуєте get_post_meta, дві додаткові бази даних запити на місце розташування), я створив нову таблицю з просторово індексованою геометричною типом даних POINT.

Мій запит був дуже схожий на ваш: MySQL робив багато важких підйомів (я випустив триггерні функції і спростив усе до двовимірного простору, тому що він був досить близьким для моїх цілей):

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_location - це значення, повернене загальнодоступною службою пошуку гео IP (я використовував geoio.com, але є ряд подібних.)

Це може здатися непростим, але, випробовуючи це, він послідовно повертав найближчі 5 місцеположень із таблиці 80000 рядків за менше ніж 4 секунди.

Поки MySQL не виконує запропоновану функцію DISTANCE, це здається найкращим способом для здійснення пошуку локацій.

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))
)

geolocationСтовпець єдиним значення для цілей тут; він складається з x (lon), y (lat) координат, які я просто шукаю з адреси при імпорті нових значень у базу даних.


Дякуємо за подальші дії. Я дійсно намагався уникати додавання таблиці, але в кінцевому підсумку додав і таблицю, хоча намагався зробити її більш загальною, ніж конкретні випадки використання. Крім того, я не використовував тип даних POINT, тому що хотів дотримуватися стандартних типів даних, які краще знають; Гео розширення MySQL потребують хорошого навчання, щоб комфортно почуватись. Однак, чи можете ви оновити відповідь, будь ласка, за допомогою DDL для таблиці, яку ви використовували? Я думаю, було б корисно для інших, хто читає це в майбутньому.
MikeSchinkel

0

Просто попередньо обчисліть відстані між усіма сутностями. Я б зберігав це в самостійну таблицю бази даних, з можливістю індексувати значення.


Це практично нескінченна кількість записів ...
MikeSchinkel

Інфінте? Я бачу тут лише n ^ 2, це не є інтимним. Тим більше, коли все більше записів слід попередньо проводити попереднє обчислення.
хакре

Практично нескінченна. Дано Lat / Long з точністю до 7 знаків після коми, що дало б 6.41977E + 17 записів. Так, нас не так багато, але ми мали б набагато більше, ніж все, що було б розумним.
MikeSchinkel

Нескінченний - це чітко визначений термін, і додавання до нього прикметників не сильно змінюється. Але я знаю, що ти маєш на увазі, ти вважаєш, що це занадто багато для обчислення. Якщо ви з часом не додаєте величезної кількості нових місць, цей попередній розрахунок можна виконати поетапно, виконуючи завдання, відмінні від програми у фоновому режимі. Точність не змінює кількість обчислень. Кількість місць є. Але, можливо, я неправильно прочитав цю частину вашого коментаря. Наприклад, 64 розташування призведе до 4 096 (або 4 032 для n * (n-1)) обчислень і, отже, записів.
хакре
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.