Ottimizzare una ricerca della posizione dello store basata sulla prossimità su un host Web condiviso?


11

Ho un progetto in cui ho bisogno di costruire un localizzatore di negozi per un cliente.

Sto usando un tipo di post personalizzato " restaurant-location" e ho scritto il codice per geocodificare gli indirizzi memorizzati in postmeta utilizzando l' API di geocodifica di Google (ecco il link che geocodifica la Casa Bianca degli Stati Uniti in JSON e ho memorizzato la latitudine e la longitudine indietro ai campi personalizzati.

Ho scritto una get_posts_by_geo_distance()funzione che restituisce un elenco di post in ordine di quelli più vicini geograficamente utilizzando la formula che ho trovato nella presentazione di questo post . Potresti chiamare la mia funzione in questo modo (sto iniziando con un "sorgente" fisso 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;

Ecco la funzione get_posts_by_geo_distance()stessa:

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

La mia preoccupazione è che l'SQL non sia ottimizzato. MySQL non può ordinare in base a nessun indice disponibile poiché il geo di origine è modificabile e non esiste un set finito di geo di origine da memorizzare nella cache. Attualmente sono sconcertato sui modi per ottimizzarlo.

Prendendo in considerazione ciò che ho già fatto, la domanda è: come faresti a ottimizzare questo caso d'uso?

Non è importante che tenga tutto ciò che ho fatto se una soluzione migliore mi permettesse di buttarlo fuori. Sono aperto a prendere in considerazione quasi tutte le soluzioni tranne quella che richiede di fare qualcosa come l'installazione di un server Sphinx o qualsiasi cosa che richieda una configurazione MySQL personalizzata. Fondamentalmente la soluzione deve essere in grado di funzionare su qualsiasi installazione semplice di WordPress vaniglia. (Detto questo, sarebbe bello se qualcuno volesse elencare soluzioni alternative per gli altri che potrebbero essere in grado di diventare più avanzati e per i posteri.)

Risorse trovate

Cordiali saluti, ho fatto un po 'di ricerca su questo, quindi piuttosto che ripetere la ricerca o piuttosto che pubblicare uno di questi link come risposta, andrò avanti e li includerò.

Per quanto riguarda la ricerca Sfinge

Risposte:


6

Di quale precisione hai bisogno? se si tratta di una ricerca a livello statale / nazionale, forse potresti fare un po 'di tempo per cercare la zip e avere una distanza pre-calcolata dall'area della zip all'area della zip del ristorante. Se hai bisogno di distanze precise non sarà una buona opzione.

Dovresti cercare una soluzione Geohash , nell'articolo di Wikipedia c'è un collegamento a una libreria PHP per codificare la decodifica lat long in geohashs.

Qui hai un buon articolo che spiega perché e come lo usano in Google App Engine (codice Python ma facile da seguire.) A causa della necessità di utilizzare geohash in GAE puoi trovare alcune buone librerie ed esempi di Python.

Come spiega questo post del blog , il vantaggio dell'uso dei geohash è che puoi creare un indice sulla tabella MySQL in quel campo.


Grazie per il suggerimento su GeoHash! Lo definirò, ma partirò per WordCamp Savannah tra un'ora, quindi non posso adesso. È un localizzatore di ristoranti per i turisti che visitano una città, quindi probabilmente 0,1 miglia sarebbero la precisione minima. Idealmente sarebbe meglio di così. Modificherò i tuoi link!
MikeSchinkel,

Se hai intenzione di visualizzare i risultati in una mappa di Google, puoi utilizzare la loro API per eseguire l'ordinamento code.google.com/apis/maps/documentation/mapsdata/…

Dal momento che questa è la risposta più interessante, la accetterò anche se non ho avuto tempo di fare ricerche e provarla.
MikeSchinkel,

9

Potrebbe essere troppo tardi per te, ma risponderò comunque, con una risposta simile a quella che ho dato a questa domanda correlata , in modo che i futuri visitatori possano fare riferimento a entrambe le domande.

Non memorizzerei questi valori nella tabella dei metadati post, o almeno non solo lì. Si desidera una tabella con post_id, lat, loncolonne, in modo da poter inserire un indice di lat, lonquery e su quello. Questo non dovrebbe essere troppo difficile da tenere aggiornato con un hook sul post salvare e aggiornare.

Quando si esegue una query sul database, si definisce un rettangolo di selezione attorno al punto iniziale, in modo da poter eseguire una query efficiente per tutte le lat, loncoppie tra i bordi nord-sud ed est-ovest del riquadro.

Dopo aver ottenuto questo risultato ridotto, è possibile eseguire un calcolo della distanza più avanzato (circolare o attuale) per filtrare le posizioni che si trovano negli angoli del riquadro di delimitazione e quindi più lontano di quanto si desideri.

Qui trovi un semplice esempio di codice che funziona nell'area di amministrazione. È necessario creare autonomamente la tabella del database aggiuntiva. Il codice è ordinato dal più al meno interessante.

<?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 : grazie per la risposta. Pensi di poter fornire del codice effettivo che mostra questi implementati?
MikeSchinkel,

@ Mike: è stata una sfida interessante, ma ecco un po 'di codice che dovrebbe funzionare.
Jan Fabry,

@ Jan Fabry: Fantastico! Lo controllerò quando tornerò indietro su quel progetto.
MikeSchinkel,

1

Sono in ritardo alla festa su questo, ma guardando indietro a questo, get_post_metail problema è davvero qui, piuttosto che la query SQL che stai usando.

Di recente ho dovuto eseguire una ricerca geografica simile su un sito che eseguo e invece di utilizzare la meta table per archiviare lat e lon (che richiede al massimo due join per cercare e, se si utilizza get_post_meta, due database aggiuntivi query per posizione), ho creato una nuova tabella con un tipo di dati POINT di geometria spazialmente indicizzata.

La mia query assomigliava molto alla tua, con MySQL che faceva molto lavoro pesante (ho lasciato fuori le funzioni di trigge e ho semplificato tutto nello spazio bidimensionale, perché era abbastanza vicino per i miei scopi):

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

dove $ client_location è un valore restituito da un servizio di ricerca IP geo pubblico (ho usato geoio.com, ma ce ne sono alcuni simili.)

Può sembrare ingombrante, ma nel testarlo, ha costantemente restituito le 5 posizioni più vicine su una tabella di 80.000 righe in meno di 4 sec.

Fino a quando MySQL non implementa la funzione DISTANCE proposta, questo sembra il modo migliore che ho trovato per implementare le ricerche di posizione.

EDIT: aggiunta della struttura della tabella per questa particolare tabella. È un insieme di elenchi di proprietà, quindi potrebbe essere o meno simile a qualsiasi altro caso d'uso.

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

La geolocationcolonna è l'unica cosa rilevante ai fini qui; è costituito da coordinate x (lon), y (lat) che ho appena cercato dall'indirizzo durante l'importazione di nuovi valori nel database.


Grazie per il seguito. Ho davvero cercato di evitare di aggiungere una tabella ma alla fine ho aggiunto anche una tabella, anche se ho cercato di renderlo più generico rispetto al caso d'uso specifico. Inoltre, non ho usato il tipo di dati POINT perché volevo attenermi ai tipi di dati standard meglio conosciuti; Le estensioni geografiche di MySQL richiedono un buon po 'di apprendimento per sentirsi a proprio agio. Detto questo, puoi aggiornare la tua risposta per favore con il DDL per la tua tabella che hai usato? Penso che sarebbe istruttivo per gli altri che leggono questo in futuro.
MikeSchinkel,

0

Basta pre-calcolare le distanze tra tutte le entità. Vorrei memorizzarlo in una tabella di database da solo, con la possibilità di indicizzare i valori.


È un numero praticamente infinito di dischi ...
MikeSchinkel,

Infinte? Vedo solo n ^ 2 qui, non è infinte. Soprattutto con sempre più voci, il precalcultaion dovrebbe essere sempre più considerato.
Hacre,

Praticamente infinito. Dato Lat / Long con una precisione di 7 cifre decimali che darebbe 6,41977E + 17 record. Sì, non ne abbiamo molti, ma avremmo molto di più di qualsiasi cosa sia ragionevole.
MikeSchinkel,

Infinito è un termine ben definito e l'aggiunta di aggettivi non cambia molto. Ma so cosa intendi, pensi che sia troppo da calcolare. Se non si sta aggiungendo fluentemente una quantità enorme di nuove posizioni nel tempo, questo pre-calcolo può essere eseguito passo-passo da un lavoro in esecuzione separata dall'applicazione in background. La precisione non cambia il numero di calcoli. Il numero di posizioni lo fa. Ma forse ho letto male quella parte del tuo commento. Ad esempio 64 posizioni si tradurranno in calcoli 4 096 (o 4 032 per n * (n-1)) e quindi record.
Hacre,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.