У меня есть проект, в котором мне нужно создать локатор магазина для клиента.
Я использую пользовательский тип записи "restaurant-location
", и я написал код для геокодирования адресов, сохраненных в postmeta, с помощью Google Geocoding API (вот ссылка геокодирует Белый дом США в JSON и Я сохранил широту и долготу обратно в пользовательские поля.
Я написал функцию get_posts_by_geo_distance()
, которая возвращает список постов в порядке самых близких по географическому признаку с использованием формулы, которую я нашел в слайд-шоу в этом посте . Вы можете вызвать мою функцию следующим образом (я начинаю с фиксированного "исходного" значения 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;
Вот сама функция 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-код настолько неоптимизирован, насколько это возможно. MySQL не может упорядочить по любому доступному индексу, так как исходное гео является изменяемым и нет конечного набора исходных геосигналов для кэширования. В настоящее время я озадачен тем, как оптимизировать его.
Принимая во внимание то, что я уже сделал, вопрос: Как бы вы пошли на оптимизацию этого варианта использования?
Не важно, чтобы я сохранил все, что сделал, если бы лучшее решение заставило меня его выбросить. Я открыт для рассмотрения практически любого решения за исключением одного, которое требует чего-то вроде установки сервера Sphinx или чего-либо, что требует настраиваемой конфигурации MySQL. По сути, решение должно работать на любой простой установке Vanilla WordPress. (Тем не менее, было бы здорово, если бы кто-нибудь захотел перечислить альтернативные решения для тех, кто мог бы стать более продвинутым и для потомков.)
К вашему сведению, я провел небольшое исследование по этому вопросу, поэтому вместо того, чтобы вы снова провели исследование или вместо того, чтобы вы разместили какие-либо из этих ссылок в качестве ответа, я продолжу и включу их.
Какая точность вам нужна? если это поиск по штату/штату, возможно, вы могли бы выполнить поиск по лат-лону в Zip и предварительно рассчитать расстояние от зоны Zip до зоны Zip ресторана. Если вам нужны точные расстояния, это не будет хорошим вариантом.
Вы должны найти решение Geohash , в статье в Википедии есть ссылка на библиотеку PHP для кодирования декодирования lat long в геохеш.
Здесь у вас есть хорошая статья объясняющая, почему и как они используют ее в Google App Engine (код на Python, но простой в использовании.) Из-за необходимости использовать геохэш в GAE вы можете найти несколько хороших библиотеки и примеры Python.
Как объясняет это сообщение в блоге , преимущество использования геохешей заключается в том, что вы можете создать индекс для таблицы MySQL по этому полю.
Это может быть слишком поздно для вас, но я все равно отвечу, ответ, аналогичный тому, который я дал на этот связанный вопрос , поэтому будущие посетители могут обратиться к обоим вопросам.
Я бы не стал хранить эти значения в таблице метаданных поста или, по крайней мере, не только там. Вам нужна таблица со столбцами 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();
Я опаздываю на вечеринку по этому вопросу, но, оглядываясь назад, проблема действительно в get_post_meta
, а не в SQL-запросе, который вы используете.
Недавно мне пришлось выполнить аналогичный гео-поиск на сайте, который я запускаю, и вместо того, чтобы использовать мета-таблицу для хранения значений lat и lon (для этого требуется в лучшем случае два соединения для поиска и, если вы используете get_post_meta, две дополнительные базы данных запросов на местоположение), я создал новую таблицу с пространственно индексируемой геометрией типа данных POINT.
Мой запрос выглядел очень похоже на ваш, поскольку MySQL выполнял большую часть тяжелой работы (я оставил функции триггера и упростил все до двухмерного пространства, потому что он был достаточно близок для моих целей):
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, но есть ряд подобных).
Это может показаться громоздким, но при тестировании он последовательно возвращал ближайшие 5 местоположений из таблицы с 80000 строками за менее чем 0,4 секунды.
До тех пор, пока 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), которые я просто смотрю с адреса при импорте новых значений в базу данных.
Просто предварительно рассчитайте расстояния между всеми объектами. Я бы сохранил это в таблице базы данных самостоятельно, с возможностью индексации значений.