Paylaşılan Web Ana Bilgisayarında Yakınlık Tabanlı Mağaza Konum Aramasını Optimize Etme?


11

Bir müşteri için bir mağaza bulma aracı oluşturmam gereken bir projem var.

Ben özel bir yazı türü " restaurant-location" kullanıyorum ve Google Geocoding API ( JSON ABD Beyaz Saray geocodes bağlantı heres bağlantı ) postmeta depolanan adresleri coğrafi kodlamak için kod yazdım ve enlem ve boylam geri sakladım özel alanlara.

Bu yazıdaki slayt gösterisinde bulduğum formülüget_posts_by_geo_distance() kullanarak coğrafi olarak en yakın olanlara göre bir liste gönderen bir işlev yazdım . Benim fonksiyonum şöyle diyebilirsiniz (sabit bir "kaynak" enlem / boylam ile başlıyorum):

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;

İşte işlevin get_posts_by_geo_distance()kendisi:

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

Benim endişem SQL alabileceğiniz kadar optimize edilmemiş olmasıdır. Kaynak coğrafi değişken olduğu ve önbelleğe alınacak sınırlı bir kaynak coğrafi kümesi olmadığından MySQL kullanılabilir herhangi bir dizin tarafından sipariş veremez . Şu anda onu optimize etmenin yollarını buldum

Halihazırda ne yaptığımı göz önünde bulundurarak: Bu kullanım senaryosunu nasıl optimize edersiniz?

Daha iyi bir çözüm beni attırırsa, yaptığım her şeyi saklamam önemli değil. Ben bir Sphinx sunucusu veya özelleştirilmiş bir MySQL yapılandırması gerektiren bir şey gibi bir şey yapmayı gerektiren biri dışında hemen hemen her çözümü düşünmeye açıkım. Temel olarak çözümün herhangi bir düz vanilya WordPress kurulumu üzerinde çalışabilmesi gerekir. (Bu, daha gelişmiş ve gelecek kuşaklar için alternatif çözümler listelemek isteyenlerin harika olacağını söyledi.)

Bulunan Kaynaklar

Bilginize, bu konuda biraz araştırma yaptım, bu yüzden araştırmayı tekrar yapmak yerine ya da bu bağlantılardan herhangi birini cevap olarak yayınlamak yerine devam edip onları dahil edeceğim.

Sfenks Araması Hakkında

Yanıtlar:


6

Hangi hassasiyete ihtiyacınız var? bir eyalet / ulusal geniş arama belki zip arama için lat-lon yapabilir ve zip alan restoran zip alanına önceden hesaplanmış mesafe olabilir. Doğru mesafelere ihtiyacınız varsa, bu iyi bir seçenek olmayacaktır.

Bir Geohash çözümüne bakmalısınız , Wikipedia makalesinde geohash'lere uzun süre kod çözmeyi kodlamak için bir PHP kütüphanesine bağlantı var.

Burada Google App Engine'de neden ve nasıl kullandıklarını açıklayan iyi bir makaleniz var (Python kodu ancak takip edilmesi kolay.) GAE'de geohash kullanma ihtiyacı nedeniyle bazı iyi python kitaplıkları ve örnekleri bulabilirsiniz.

Gibi bu blog yazısı açıklıyor, geohashes kullanmanın avantajı o saha MySQL tablo üzerinde bir dizin oluşturabilirsiniz olmasıdır.


GeoHash ile ilgili öneriniz için teşekkürler! Kesinlikle kontrol edeceğim ama WordCamp Savannah için bir saat içinde bırakarak şu anda olamaz. Bir kasabayı ziyaret eden turistler için bir restoran bulma aracıdır, bu nedenle 0,1 mil muhtemelen minimum hassasiyet olacaktır. İdeal olarak bundan daha iyi olurdu. Bağlantılarınızı düzenleyeceğim!
MikeSchinkel

Eğer bir google haritası sonuçlarını görüntülemek için kullanacaksanız sıralama yapmak için onların api kullanabilirsiniz code.google.com/apis/maps/documentation/mapsdata/...

Bu en ilginç cevap olduğu için, araştırmak ve denemek için zamanım olmasa bile kabul edeceğim.
MikeSchinkel

9

Bu sizin için çok geç olabilir, ancak yine de bu ilgili soruya verdiğim cevapla cevap vereceğim , böylece gelecekteki ziyaretçiler her iki soruya da başvurabilirler.

En azından değil sonrası meta tabloda bu değerleri depolayabilir veya olmaz sadece orada. Sen bir tablo istiyoruz post_id, lat, lonsütunlar, buna bağlı olarak bir dizinini yerleştirebilir lat, lonbu konuda ve sorguyu. Kayıt sonrası ve güncellemede bir kanca ile güncel tutmak için çok zor olmamalıdır.

Veritabanını sorguladığınızda , başlangıç ​​noktası etrafında bir sınırlama kutusu tanımlarsınız , böylece kutununlat, lon Kuzey-Güney ve Doğu-Batı sınırları arasındaki tüm çiftler için etkili bir sorgu yapabilirsiniz .

Bu azaltılmış sonucu elde ettikten sonra, sınırlayıcı kutunun köşelerindeki yerleri ve daha fazlasını istediğiniz yerleri filtrelemek için daha gelişmiş (dairesel veya gerçek sürüş yönleri) mesafe hesaplaması yapabilirsiniz.

Burada, yönetici alanında çalışan basit bir kod örneği bulacaksınız. Ek veritabanı tablosunu kendiniz oluşturmanız gerekir. Kod en çok en az ilginç sıralanır.

<?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 : Cevabınız için teşekkürler. Bunların uygulandığını gösteren gerçek bir kod sağlayabileceğinizi düşünüyor musunuz?
MikeSchinkel

@Mike: İlginç bir sorundu, ama işte çalışması gereken bazı kodlar.
Jan Fabry

@Jan Fabry: Harika! O projeye geri döndüğümde kontrol edeceğim.
MikeSchinkel

1

Bu bir partiye geç kaldım, ama buna geri dönersek get_post_meta, gerçekten burada sorun, kullandığınız SQL sorgusu yerine.

Son zamanlarda çalıştırdığım bir sitede benzer bir coğrafi arama yapmak zorunda kaldım ve lat ve lon depolamak için meta tablo kullanmak yerine (aramak için en iyi iki birleşim gerektirir ve get_post_meta kullanıyorsanız, iki ek veritabanı konum başına sorgu), uzamsal olarak dizine alınmış geometri POINT veri türüne sahip yeni bir tablo oluşturdum.

MySQL, ağır kaldırma işlemlerinin çoğunu yaparken sorgum sizinkine çok benziyordu (Trig fonksiyonlarını bıraktım ve her şeyi iki boyutlu alana basitleştirdim, çünkü benim amacım için yeterince yakındı):

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

burada $ client_location, genel bir coğrafi IP arama hizmeti tarafından döndürülen bir değerdir (geoio.com'u kullandım, ancak benzer birkaç tane var.)

Tuhaf görünebilir, ancak test ederken, sürekli olarak .4 saniyenin altında 80.000 satırlık bir tablodan en yakın 5 konumu döndürdü.

MySQL önerilen DISTANCE fonksiyonunu sunana kadar konum aramalarını uygulamak için bulduğum en iyi yol gibi görünüyor.

EDIT: Bu özel tablo için tablo yapısı ekleme. Bu bir dizi özellik listesidir, bu nedenle başka bir kullanım durumuna benzer veya olmayabilir.

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

geolocationSütun burada amaçlarla ilgili tek şey; yeni değerleri veritabanına aktardığımda adresten aradığım x (lon), y (lat) koordinatlarından oluşur.


Takip için teşekkürler. Gerçekten bir tablo eklemekten kaçınmaya çalıştım ama belirli bir kullanım durumundan daha genel yapmaya çalışmış olsa da, bir tablo ekleyerek sona erdi. Ayrıca, daha iyi bilinen standart veri türlerine bağlı kalmak istediğim için POINT veri türünü kullanmadım; MySQL'in coğrafi uzantıları rahat olmak için iyi bir öğrenme gerektirir. Bununla birlikte, cevabınızı lütfen kullandığınız tablonuz için DDL ile güncelleyebilir misiniz? Gelecekte bunu okuyan başkaları için öğretici olacağını düşünüyorum.
MikeSchinkel

0

Tüm varlıklar arasındaki mesafeleri önceden hesaplamanız yeterlidir. Ben değerleri endeksleme yeteneği ile, kendi başına bir veritabanı tablosuna depolamak.


Bu neredeyse sonsuz sayıda kayıt ...
MikeSchinkel

Kademesiz? Burada sadece n ^ 2 görüyorum, bu infinte değil. Özellikle gittikçe daha fazla giriş ile, önceden hesaplanma daha fazla düşünülmelidir.
hakre

Neredeyse sonsuz. 6.41977E + 17 kayıt verecek 7 ondalık basamak hassasiyetinde Enlem / Boylam verildi. Evet o kadar çok şeyimiz yok ama makul olandan çok daha fazlasına sahiptik.
MikeSchinkel

Sonsuz iyi tanımlanmış bir terimdir ve ona sıfat eklemek çok fazla değişmez. Ama ne demek istediğini biliyorum, bunun hesaplamak için çok fazla olduğunu düşünüyorsun. Zaman içinde çok miktarda yeni yer akıcı bir şekilde eklemiyorsanız, bu ön hesaplama arka planda uygulamanız dışında çalışan bir iş tarafından adım adım yapılabilir. Kesinlik, hesaplama sayısını değiştirmez. Yer sayısı. Ama belki de yorumunun o kısmını yanlış okudum. Örneğin 64 konum 4 096 (veya n * (n-1) için 4 032) hesaplamaya ve dolayısıyla kayıtlara neden olacaktır.
hakre
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.