공유 웹 호스트에서 근접성 기반 매장 위치 검색을 최적화 하시겠습니까?


11

클라이언트를위한 매장 검색기를 구축해야하는 프로젝트가 있습니다.

맞춤 게시물 유형 ' restaurant-location'을 (를) 사용하고 있으며 Google Geocoding API를 사용하여 postmeta에 저장된 주소를 지오 코딩하기위한 코드를 작성했습니다 (여기서 미국 백악관을 JSON으로 지오 코딩 하는 링크가 있고 위도와 경도를 다시 저장했습니다) 사용자 정의 필드에.

필자는 이 게시물의 슬라이드 쇼에서 찾은 공식을get_posts_by_geo_distance() 사용 하여 지리적으로 가장 가까운 게시물 순서로 게시물 목록을 반환 하는 함수를 작성했습니다 . 내 함수를 이렇게 호출 할 수 있습니다 (고정 된 "소스"위도 / 경도로 시작합니다).

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이 최대한 최적화되지 않았다는 것입니다. 소스 지오 (geo) 는 변경 가능하고 캐시 할 유한 소스 지오 세트 가 없기 때문에 MySQL은 사용 가능한 인덱스로 주문할 수 없습니다 . 현재 최적화 방법에 대해 잘 알고 있습니다.

내가 이미 한 일을 고려할 때의 문제는 다음과 같습니다. 이 사용 사례를 최적화하는 방법은 무엇입니까?

더 나은 솔루션으로 인해 버리면 내가 한 일을 유지하는 것이 중요하지 않습니다. Sphinx 서버 설치 또는 사용자 정의 MySQL 구성이 필요한 것을 제외하고는 거의 모든 솔루션을 고려할 수 있습니다. 기본적으로 솔루션은 모든 일반 바닐라 워드 프레스 설치에서 작동 할 수 있어야합니다. (즉, 더 발전하고 후손이 될 수있는 다른 사람들을위한 대체 솔루션을 나열하려는 사람이 있으면 좋을 것입니다.)

발견 된 자료

참고로, 나는 이것에 대해 약간의 연구를 했으므로 연구를 다시 수행 하거나이 링크 중 하나를 대답으로 게시하지 않고 내가 포함시킬 것입니다.

스핑크스 검색에 대하여

답변:


6

어떤 정밀도가 필요합니까? 주 / 국가 전체 검색 인 경우 위도-경도 검색을 수행하고 식당의 우편 영역에서 우편 영역까지 사전 계산 된 거리를 가질 수 있습니다. 정확한 거리가 필요한 경우 좋은 옵션이 아닙니다.

Geohash 솔루션을 살펴보아야 합니다. Wikipedia 기사에는 지오 해시까지 긴 디코딩을 인코딩하는 PHP 라이브러리에 대한 링크가 있습니다.

다음 은 Google App Engine에서 왜 그리고 어떻게 사용하는지 설명 하는 좋은 기사입니다 (Python 코드이지만 따르기 쉽습니다). GAE에서 geohash를 사용해야하기 때문에 좋은 Python 라이브러리와 예제를 찾을 수 있습니다.

으로 이 블로그 게시물을 설명하고, geohashes 사용의 이점은 사용자가 해당 필드에 MySQL의 테이블에 인덱스를 만들 수 있다는 것입니다.


GeoHash에 대한 제안에 감사드립니다! 나는 그것을 확실히 확인할 것이지만 한 시간 안에 WordCamp Savannah를 떠나기 때문에 지금은 할 수 없습니다. 마을을 방문하는 관광객을위한 식당 찾기이므로 0.1 마일은 최소 정밀도 일 것입니다. 이상적으로는 그것보다 낫습니다. 당신의 링크를 편집하겠습니다!
MikeSchinkel 16:20에

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

@ 1 월 : 답변 주셔서 감사합니다. 구현 된 것을 보여주는 실제 코드를 제공 할 수 있다고 생각하십니까?
MikeSchinkel

@ Mike : 흥미로운 도전 이었지만 여기에 작동하는 코드가 있습니다.
Jan Fabry

@Jan Fabry : 쿨! 해당 프로젝트로 돌아갈 때 확인하겠습니다.
MikeSchinkel

1

나는 이것에 대해 파티에 늦었지만 이것을 되돌아 보면 get_post_meta, 사용중인 SQL 쿼리가 아니라 실제로 문제입니다.

나는 최근에 내가 운영하는 사이트에서 비슷한 지리적 조회를 수행해야했고, 메타 테이블을 사용하여 위도 및 경도를 저장하는 대신 (최대 2 개의 조인이 필요하며 get_post_meta를 사용하는 경우 2 개의 추가 데이터베이스 위치 당 쿼리 수), 공간적으로 인덱스 된 지오메트리 POINT 데이터 형식으로 새 테이블을 만들었습니다.

내 쿼리는 MySQL과 같이 많은 노력을 기울이고 있습니다 (트리거 기능을 생략하고 모든 것을 2 차원 공간으로 단순화했습니다.

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을 사용했지만 유사한 항목이 많이 있습니다).

다루기 어려워 보일지 모르지만 테스트 할 때 .4 초 안에 80,000 행 테이블에서 가장 가까운 5 곳을 일관되게 반환했습니다.

MySQL이 제안 된 DISTANCE 기능을 출시 할 때까지는 위치 조회를 구현하는 가장 좋은 방법 인 것 같습니다.

편집 : 이 특정 테이블에 대한 테이블 구조 추가. 속성 목록 세트이므로 다른 유스 케이스와 유사하거나 유사하지 않을 수 있습니다.

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 만 보았습니다. 특히 점점 더 많은 출품작으로 인해 사전 계산이 점점 더 고려되어야합니다.
hakre

실질적으로 무한합니다. Lat / Long은 소수점 이하 7 자리의 정밀도로 6.41977E + 17 레코드를 제공합니다. 예, 우리는 그 수가 많지 않지만 합리적인 것보다 훨씬 더 많이 있습니다.
MikeSchinkel 16:20의

무한은 잘 정의 된 용어이며 형용사를 추가해도 크게 변하지 않습니다. 그러나 나는 당신이 무엇을 의미하는지 알고 있습니다, 당신은 이것이 너무 계산하기 어렵다고 생각합니다. 시간이 지남에 따라 방대한 양의 새로운 위치를 유창하게 추가하지 않는 경우 백그라운드에서 응용 프로그램과 별도로 실행되는 작업을 통해이 사전 계산을 단계별로 수행 할 수 있습니다. 정밀도는 계산 수를 변경하지 않습니다. 위치 수 그러나 나는 당신의 의견의 그 부분을 잘못 읽었을 것입니다. 예를 들어 64 개의 위치는 4 096 (또는 n * (n-1)의 경우 4 032) 계산으로 기록됩니다.
hakre
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.