it-swarm.dev

Optimalizace vyhledávání umístění úložiště založeného na přiblížení na sdíleném webovém hostiteli?

Mám projekt, kde potřebuji vybudovat lokátor obchodu pro klienta.

Používám vlastní typ příspěvku "restaurant-location" a napsal (a) jsem kód pro geocode adres uložených v postmeta pomocí Google Geocoding API (je odkaz, který geocodes amerického Bílého domu v JSON a Uložila jsem zeměpisnou šířku a délku zpět do vlastních polí.

Napsal jsem funkci get_posts_by_geo_distance(), která vrací seznam příspěvků v pořadí, které je nejblíže geograficky pomocí vzorce, který jsem našel v prezentaci v tomto příspěvku . Můžete zavolat jako moje funkce (začínám s pevným "zdrojem" 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;

Zde je funkce get_posts_by_geo_distance() sama:

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

Moje obava je, že SQL je tak ne-optimalizovaný, jak můžete získat. MySQL nemůže objednávat žádný dostupný index, protože source geo je proměnlivý a není zde konečná množina zdrojových geoserverů pro cache. V současné době jsem narazil na způsoby, jak optimalizovat.

Když vezmeme v úvahu to, co jsem již udělal, otázka je: Jak byste šli o optimalizaci tohoto případu použití?

Není důležité, abych udržel cokoliv, co jsem udělal, kdyby mi to lepší řešení pomohlo. Já jsem otevřený zvažovat téměř nějaké řešení kromě pro ten to vyžaduje dělat něco jako instalace Sphinx server nebo něco to vyžaduje přizpůsobenou MySQL konfiguraci. V podstatě řešení musí být schopen pracovat na jakékoli prosté instalaci Vanilla WordPress. (To znamená, že by bylo skvělé, kdyby někdo chtěl uvést alternativní řešení pro ostatní, kteří by byli schopni získat pokročilejší a pro potomky.)

Nalezené zdroje

FYI, udělal jsem trochu výzkumu na to, spíše než jste si výzkum znovu, nebo spíše než jste po nějaké z těchto odkazů jako odpověď půjdu do toho a zahrnout je.

Co se týče hledání sfingy

11
MikeSchinkel

Jakou přesnost potřebujete? pokud je to státní/celostátní široké hledání možná byste mohli udělat lat-lon na Zip vyhledávání a mají precomputed vzdálenost od Zip oblasti do Zip oblasti restaurace. Pokud potřebujete přesné vzdálenosti, které nebudou dobré.

Měli byste se podívat do Geohash solution, v článku Wikipedie je odkaz na knihovnu PHP, která kóduje dekódování lat dlouho do geohashů.

Zde máte dobrý článek vysvětlující, proč a jak ho používají v Google App Engine (Pythonův kód, ale snadno sledovatelný). python knihovny a příklady.

Jako tento příspěvek na blogu vysvětluje, výhodou použití geohashes je, že můžete vytvořit index na tabulce MySQL na tomto poli.

6
user324

To může být pro tebe příliš pozdě, ale stejně odpovím, s podobná odpověď, jakou jsem dal k této otázce , takže budoucí návštěvníci mohou odkazovat na obě otázky.

Tyto hodnoty bych neuložil do tabulky metadat, nebo alespoň ne pouze tam. Chcete tabulku s post_id, lat, lon sloupci, abyste mohli umístit index lat, lon a dotaz na to. To by nemělo být příliš těžké držet krok s hákem na po uložení a aktualizaci.

Když se dotazujete na databázi, definujete ohraničující rámeček kolem počátečního bodu, takže můžete provést efektivní dotaz pro všechny páry lat, lon mezi hranicemi Sever-Jih a Východ-Západ.

Poté, co dosáhnete tohoto sníženého výsledku, můžete provést pokročilejší (kruhový nebo skutečný směr jízdy) výpočet vzdálenosti pro odfiltrování míst, která jsou v rozích ohraničovacího rámečku a tím dále, než si přejete.

Zde najdete jednoduchý příklad kódu, který funguje v oblasti admin. Musíte vytvořit další databázovou tabulku sami. Kód je objednán od většiny po nejméně zajímavé.

<?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();
9
Jan Fabry

Jsem pozdě na večírku v této, ale při pohledu zpět na to, get_post_meta je opravdu problém zde, spíše než SQL dotaz, který používáte.

Nedávno jsem musel provést podobné vyhledávání geo na webu, který používám, a místo toho, abych používal meta tabulku pro ukládání lat a lon (což vyžaduje v nejlepším případě dvě spojení, a pokud používáte get_post_meta, dvě další databáze dotazy na umístění), vytvořil jsem novou tabulku s prostorově indexovaným datovým typem POINT geometrie.

Můj dotaz vypadal hodně jako váš, s MySQL dělá hodně těžkého zvedání (já jsem vynechal trig funkce a zjednodušil všechno na dvojrozměrný prostor, protože to bylo dost blízko pro mé účely):

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

kde $ client_location je hodnota vrácená veřejnou vyhledávací službou geo IP (použila jsem geoio.com, ale existuje několik podobných.)

Může se zdát, že je to nepraktické, ale při jeho testování konzistentně vrátilo nejbližší 5 míst z tabulky řádků 80 000 v rámci 0,4 sek.

Dokud MySQL nenavrhne funkci DISTANCE, která je navržena, vypadá to jako nejlepší způsob, jak jsem našel implementaci vyhledávání polohy.

EDIT: Přidání struktury tabulky pro tuto konkrétní tabulku. Je to soubor majetkových výpisů, takže může nebo nemusí být podobný jakémukoli jinému případu použití.

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

Sloupec geolocation je jediná věc, která je pro tyto účely relevantní; Skládá se z x (lon), y (lat) souřadnic, které jsem se při importu nových hodnot do databáze podíval z adresy.

1
goldenapples

Prostě předpočítejte vzdálenosti mezi všemi entitami. Já bych to uložil do databázové tabulky na jeho vlastní, se schopností indexovat hodnoty.

0
hakre