it-swarm-ko.tech

근접 웹 기반 호스트에서 근접 식 저장소 위치 검색 최적화

클라이언트 저장소 로케이터를 구축해야하는 프로젝트가 있습니다.

저는 맞춤형 게시물 유형 "restaurant-location"을 사용하고 있으며 Google Geocoding API 를 사용하여 우편 번호에 저장된 주소를 지오 코딩하는 코드를 작성했습니다 ( 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은 사용 가능한 인덱스로 정렬 할 수 없습니다. 현재는이를 최적화하는 방법이별로 없습니다.

이미 내가 한 것을 고려하면 다음과 같습니다. 이 유스 케이스를 최적화하는 방법에 대해 어떻게 생각하십니까?

더 나은 해결책을 찾지 못하면 내가 한 일을 계속하는 것이 중요하지 않습니다. 저는 거의 모든 솔루션을 고려해 볼만합니다스핑크스 서버 설치 또는 커스터마이징 된 MySQL 구성을 필요로하는 것을 제외하면 기본적으로 솔루션은 일반 바닐라 워드 프레스에서 작동 할 수 있어야합니다. (누군가가 더 발전하고 후세를 얻을 수있는 다른 사람들을 위해 대안을 열거하고 싶다면 좋을 것이다.)

자원 발견

참고로, 저는 이것에 대한 약간의 연구를 했으므로 연구를 다시하거나 오히려 이러한 링크를 답으로 게시 해 두었습니다.

스핑크스 검색 관련

11
MikeSchinkel

어떤 정밀도가 필요합니까? 그것이 주/전국의 광범위한 검색 일 경우 어쩌면 당신은 우편 번호 조회에 위도 - 경도를 수행 할 수 있고 우편 번호 영역에서 식당의 우편 번호 영역까지의 거리를 미리 계산할 수 있습니다. 정확한 거리가 필요한 경우 좋은 옵션이 아닙니다.

Geohash 솔루션을 살펴보아야합니다. Wikipedia 기사에서 geohash에 대한 디코드 래트를 인코딩하기 위해 PHP 라이브러리에 대한 링크가 있습니다.

여기에 좋은 기사 )가 Google App Engine (파이썬 코드이지만 쉽게 따라 할 수있는 이유)을 사용하는 이유를 설명합니다. GAE에서 geohash를 사용해야하기 때문에 좋은 파이썬 라이브러리 및 예.

이 블로그 게시물 이 설명하는 것처럼, 지렁이를 사용하는 이점은 해당 필드의 MySQL 테이블에 색인을 생성 할 수 있다는 것입니다.

6
user324

이것은 당신에게 너무 늦을 수도 있지만, 어쨌든 이 관련된 질문에 준 것과 비슷한 대답 으로 회신 할 것입니다. 그래서 _ 미래 방문객은 두 질문 모두를 참조 할 수 있습니다.

나는이 값을 게시 메타 데이터 테이블에 저장하지 않거나 최소한 only there가 아닌 값으로 저장합니다. 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();
9
Jan Fabry

나는이 파티에 늦었지만, 이것에 대해 되돌아 보면, get_post_meta는 실제로 사용하고있는 SQL 쿼리가 아니라 여기서 문제입니다.

나는 최근에 내가 실행하는 사이트에서 유사한 지리 검색을해야했다. 대신 lat와 lon을 저장하기 위해 메타 테이블을 사용했다. (두 개의 조인을 찾아보고 get_post_meta를 사용한다면 두 개의 추가 데이터베이스가 필요하다. 위치 당 쿼리), 공간적으로 인덱싱 된 기하 도형 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은 공개 geo IP 조회 서비스에서 반환 한 값입니다 (geoio.com을 사용했지만 비슷한 숫자가 있습니다).

다루기 힘들어 보일지 모르지만 테스트 할 때 .4 초 미만의 80,000 행 테이블에서 가장 가까운 5 개의 위치를 ​​일관되게 반환했습니다.

MySQL이 제안 된 DISTANCE 함수를 롤아웃 할 때까지 이것은 위치 룩업을 구현하는 가장 좋은 방법 인 것 같습니다.

EDIT : 이 특정 테이블에 대한 테이블 구조 추가. 이 속성 집합은 속성 목록이므로 다른 유스 케이스와 비슷하거나 다를 수 있습니다.

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) 좌표들로 구성되어 있습니다. 좌표는 데이터베이스에 새로운 값을 가져올 때 주소를 조회하는 것입니다.

1
goldenapples

모든 개체 사이의 거리를 미리 계산하면됩니다. 나는 그것을 데이터베이스 테이블에 저장할 것이고, 값을 인덱스 할 수있는 능력이있다.

0
hakre