Tối ưu hóa Tìm kiếm vị trí cửa hàng dựa trên tiệm cận trên máy chủ web dùng chung?


11

Tôi đã có một dự án mà tôi cần xây dựng một bộ định vị cửa hàng cho một khách hàng.

Tôi đang sử dụng loại bài đăng tùy chỉnh " restaurant-location" và tôi đã viết mã để mã hóa địa lý các địa chỉ được lưu trữ trong postmeta bằng API mã hóa địa lý của Google (đây là liên kết mã hóa Nhà Trắng Hoa Kỳ trong JSON và tôi đã lưu lại vĩ độ và kinh độ đến các lĩnh vực tùy chỉnh.

Tôi đã viết một get_posts_by_geo_distance()hàm trả về một danh sách các bài đăng theo thứ tự gần nhất về mặt địa lý bằng cách sử dụng công thức tôi tìm thấy trong trình chiếu tại bài đăng này . Bạn có thể gọi hàm của tôi như vậy (Tôi đang bắt đầu với một "nguồn" lat / long cố định):

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;

Đây là chức năng của get_posts_by_geo_distance()chính nó:

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

Mối quan tâm của tôi là SQL không được tối ưu hóa như bạn có thể nhận được. MySQL không thể đặt hàng theo bất kỳ chỉ mục có sẵn nào vì địa lý nguồn có thể thay đổi và không có bộ địa lý nguồn hữu hạn nào để lưu vào bộ đệm. Hiện tại tôi đang bối rối để tìm cách tối ưu hóa nó.

Cân nhắc những gì tôi đã làm đã đặt câu hỏi là: Làm thế nào bạn sẽ tối ưu hóa trường hợp sử dụng này?

Điều quan trọng là tôi giữ bất cứ điều gì tôi đã làm nếu một giải pháp tốt hơn sẽ khiến tôi vứt bỏ nó. Tôi sẵn sàng xem xét hầu hết mọi giải pháp ngoại trừ một giải pháp yêu cầu thực hiện một số thứ như cài đặt máy chủ Sphinx hoặc bất kỳ thứ gì yêu cầu cấu hình MySQL tùy chỉnh. Về cơ bản, giải pháp cần có khả năng hoạt động trên mọi cài đặt WordPress đơn giản. (Điều đó nói rằng, sẽ thật tuyệt nếu bất cứ ai muốn liệt kê các giải pháp thay thế cho những người khác có thể có thể tiến bộ hơn và cho hậu thế.)

Tài nguyên được tìm thấy

FYI, tôi đã nghiên cứu một chút về vấn đề này thay vì bạn phải nghiên cứu lại hay thay vì bạn đăng bất kỳ liên kết nào trong số này như một câu trả lời tôi sẽ tiếp tục và đưa chúng vào.

Về Tìm kiếm Nhân sư

Câu trả lời:


6

Bạn cần độ chính xác nào? nếu đó là tìm kiếm trên toàn tiểu bang / quốc gia, có thể bạn có thể thực hiện tra cứu zip-lon và có khoảng cách từ khu vực zip đến khu vực zip của nhà hàng. Nếu bạn cần khoảng cách chính xác sẽ không phải là một lựa chọn tốt.

Bạn nên xem xét một giải pháp Geohash , trong bài viết Wikipedia có một liên kết đến thư viện PHP để mã hóa giải mã lat lat thành geohashs.

Ở đây bạn có một bài viết hay giải thích lý do và cách họ sử dụng nó trong Google App Engine (mã Python nhưng dễ theo dõi.) Vì nhu cầu sử dụng geohash trong GAE, bạn có thể tìm thấy một số thư viện và ví dụ về python tốt.

Như bài đăng trên blog này giải thích, lợi thế của việc sử dụng geohash là bạn có thể tạo một chỉ mục trên bảng MySQL trên trường đó.


Cảm ơn lời đề nghị trên GeoHash! Tôi chắc chắn sẽ kiểm tra nó nhưng sẽ rời WordCamp Savannah sau một giờ nữa vì vậy không thể ngay bây giờ. Đó là một địa điểm nhà hàng cho khách du lịch đến thăm một thị trấn, vì vậy 0,1 dặm có lẽ sẽ là sự chính xác tối thiểu. Lý tưởng nhất sẽ tốt hơn thế. Tôi sẽ chỉnh sửa các liên kết của bạn!
MikeSchinkel

Nếu bạn đang đi để hiển thị các kết quả trong một bản đồ google bạn có thể sử dụng api của họ để thực hiện sắp xếp code.google.com/apis/maps/documentation/mapsdata/...

Vì đây là câu trả lời thú vị nhất mà tôi sẽ chấp nhận mặc dù tôi không có thời gian để nghiên cứu và thử nó.
MikeSchinkel

9

Điều này có thể là quá muộn đối với bạn, nhưng dù sao tôi cũng sẽ trả lời, với một câu trả lời tương tự như tôi đã đưa ra cho câu hỏi liên quan này , vì vậy khách truy cập trong tương lai có thể tham khảo cả hai câu hỏi.

Tôi sẽ không lưu trữ các giá trị này trong bảng siêu dữ liệu bài đăng, hoặc ít nhất là không chỉ ở đó. Bạn muốn có một bảng với post_id, lat, loncột, vì vậy bạn có thể đặt một chỉ số của lat, lonvà truy vấn về điều đó. Điều này không quá khó để cập nhật với một cái móc trên lưu bài đăng và cập nhật.

Khi bạn truy vấn cơ sở dữ liệu, bạn xác định một hộp giới hạn xung quanh điểm bắt đầu, do đó bạn có thể thực hiện một truy vấn hiệu quả cho tất cả các lat, loncặp giữa biên giới Bắc-Nam và Đông-Tây của hộp.

Sau khi bạn nhận được kết quả giảm này, bạn có thể thực hiện phép tính khoảng cách nâng cao (vòng tròn hoặc hướng lái xe thực tế) tiên tiến hơn để lọc ra các vị trí nằm trong các góc của hộp giới hạn và ở xa hơn bạn mong muốn.

Ở đây bạn tìm thấy một ví dụ mã đơn giản hoạt động trong khu vực quản trị. Bạn cần phải tự tạo bảng cơ sở dữ liệu bổ sung. Mã được sắp xếp từ hầu hết đến ít thú vị nhất.

<?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 : Cảm ơn câu trả lời. Bạn có nghĩ rằng bạn có thể cung cấp một số mã thực tế hiển thị những mã này được triển khai không?
MikeSchinkel

@Mike: Đó là một thử thách thú vị, nhưng đây là một số mã nên hoạt động.
Jan Fabry

@Jan Fabry: Tuyệt! Tôi sẽ kiểm tra xem khi tôi quay lại dự án đó.
MikeSchinkel

1

Tôi đến bữa tiệc muộn, nhưng nhìn lại, đây get_post_metathực sự là vấn đề ở đây, thay vì truy vấn SQL bạn đang sử dụng.

Gần đây tôi đã phải thực hiện một tra cứu địa lý tương tự trên một trang web mà tôi điều hành, thay vì sử dụng bảng meta để lưu trữ lat và lon (yêu cầu tối đa hai liên kết để tra cứu và, nếu bạn đang sử dụng get_post_meta, hai cơ sở dữ liệu bổ sung truy vấn trên mỗi vị trí), tôi đã tạo một bảng mới với kiểu dữ liệu POINT hình học được lập chỉ mục không gian.

Truy vấn của tôi trông rất giống với truy vấn của bạn, với MySQL thực hiện rất nhiều công việc nặng nhọc (tôi đã bỏ qua các chức năng trig và đơn giản hóa mọi thứ thành không gian hai chiều, vì nó đủ gần với mục đích của tôi):

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

trong đó $ client_location là một giá trị được trả về bởi dịch vụ tra cứu IP địa lý công cộng (tôi đã sử dụng Geoio.com, nhưng có một số giá trị tương tự.)

Nó có vẻ khó sử dụng, nhưng khi thử nghiệm, nó đã liên tục trả về 5 vị trí gần nhất trong số 80.000 hàng trong vòng dưới 4 giây.

Cho đến khi MySQL triển khai chức năng DISTANCE đang được đề xuất, đây có vẻ là cách tốt nhất tôi tìm thấy để thực hiện tra cứu vị trí.

EDIT: Thêm cấu trúc bảng cho bảng cụ thể này. Đây là một tập hợp các danh sách tài sản, vì vậy nó có thể giống hoặc không giống với bất kỳ trường hợp sử dụng nào khác.

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

Các geolocationcột là điều duy nhất có liên quan cho các mục đích ở đây; nó bao gồm các tọa độ x (lon), y (lat) mà tôi chỉ cần tra cứu từ địa chỉ khi nhập giá trị mới vào cơ sở dữ liệu.


Cảm ơn đã theo lên. Tôi thực sự đã cố gắng tránh thêm một bảng nhưng cuối cùng cũng thêm một bảng, mặc dù đã cố gắng làm cho nó chung chung hơn trường hợp sử dụng cụ thể. Hơn nữa, tôi đã không sử dụng kiểu dữ liệu POINT vì tôi muốn gắn bó với kiểu dữ liệu chuẩn hơn; Các phần mở rộng địa lý của MySQL đòi hỏi một chút học tập để có được sự thoải mái. Điều đó nói rằng, bạn có thể cập nhật câu trả lời của bạn với DDL cho bảng mà bạn đã sử dụng không? Tôi nghĩ rằng nó sẽ được hướng dẫn cho những người khác đọc điều này trong tương lai.
MikeSchinkel

0

Chỉ cần tính toán trước khoảng cách giữa tất cả các thực thể. Tôi sẽ tự lưu trữ nó vào một bảng cơ sở dữ liệu, với khả năng lập chỉ mục các giá trị.


Đó là số lượng hồ sơ thực tế vô hạn ...
MikeSchinkel

Thông tin? Tôi chỉ thấy n ^ 2 ở đây, đó không phải là infinte. Đặc biệt với ngày càng nhiều mục, prealcultaion nên được xem xét ngày càng nhiều.
hakre

Thực tế vô hạn. Cho Lat / Long ở độ chính xác 7 chữ số thập phân sẽ cung cấp cho 6.41977E + 17 bản ghi. Vâng, chúng tôi không có nhiều nhưng chúng tôi có nhiều hơn bất cứ điều gì hợp lý.
MikeSchinkel

Infinite là một thuật ngữ được xác định rõ và việc thêm tính từ vào nó không thay đổi nhiều. Nhưng tôi biết ý của bạn là gì, bạn nghĩ rằng điều này là quá nhiều để tính toán. Nếu bạn không thêm trôi chảy một lượng lớn các vị trí mới theo thời gian, việc tính toán trước này có thể được thực hiện từng bước bởi một công việc chạy ngoài ứng dụng của bạn trong nền. Độ chính xác không thay đổi số lượng tính toán. Số lượng địa điểm nào. Nhưng có lẽ tôi đã đọc sai một phần bình luận của bạn. Ví dụ: 64 vị trí sẽ dẫn đến 4 096 (hoặc 4 032 cho các tính toán n * (n-1)) và do đó ghi lại.
hakre
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.