Использование Sphinx для нахождения ближайших объектов по координатам

Для одного из проектов потребовалось реализовать программный функционал поиска ближайших объектов на карте в определенном радиусе от заданной точки с координатами широты и долготы. Такие задачи как правило требуют решения Прямой и Обратной геодезических задач, а поскольку с геодезией я совершенно не дружу, то обратил свое внимание на готовые решения, одним из который стал Sphinx.

Наверняка многие воспринимают Sphinx исключительно как полнотекстовый поисковый движок, я же хотел бы обратить внимание на магическую функцию @geodist, которую можно использовать для нахождения в индексе объектов на заданном расстоянии по их координатам (широте и долготе). Для нахождения ближайших объектов требуется не много:

  • База данных поддерживаемая Sphinx’ом с координатами объектов поиска
  • Установленный Sphinx Search
  • А также PHP + sphinxapi (или любой другой язык для которого есть sphinxapi)

База данных

Я не буду описывать процесс установки Sphinx’а на хостинг, мануалов в инете полно да и сам процесс установки банален и не требует каких-то особенных умений. Ниже привожу примерный файл конфига который понадобится для индексации таблицы с координатами объектов

# Источник данных для поиска
source main
{
    # параметры подключения к mysql
    type = mysql
    sql_host = localhost
    sql_user = user
    sql_pass = password
    sql_db = database
    sql_port = 3306
    # запросы после установки соединения
    sql_query_pre = SET NAMES utf8
    sql_query_pre = SET SESSION query_cache_type=OFF
    # не засыпать между шагами индексации
    sql_ranged_throttle = 0
}
# Источник данных для гео-поиска (наследует блок main)
source geo : main
{
    # запрос выборки широты и долготы в радианах для индексации
    sql_query = \
        SELECT `id`,
            radians(`latitude`), \
            radians(`longitude`) \
        FROM table_with_objects
    # обрабатывать поля как float
    sql_attr_float = latitude
    sql_attr_float = longitude
}
# настройка индекса
index geo
{
    # использовать соответствующий блок из source
    source = geo
    # путь для хранения файлов индекса
    path = /usr/home/www/sphinx/indexes/closest
    docinfo = extern
    mlock = 0
    min_word_len = 1
    charset_type = utf-8
}
# настройки демона
searchd
{
    # прослушивание порта
    listen = 9312
    # хранение логов
    log = /usr/home/www/sphinx/log/searchd.log
    query_log = /usr/home/www/sphinx/log/query.log
    pid_file = /usr/home/www/sphinx/log/searchd.pid
}

Индексация

При первоначальном запуске просто скармливаем созданных конфиг индексатору

indexer --config /usr/home/www/sphinx/sphinx.conf --all

Для переиндексации текущего индекса необходимо использовать параметр «—rotate», он добавит к созданному индексу новые данные

indexer --config /usr/home/www/sphinx/sphinx.conf --rotate

Запускаем демона

searchd --config /usr/home/www/sphinx/sphinx.conf

PHP + sphinxapi

Для проверки работы создадим простую форму

<form action="" method="get">
    Долгота <input name="longitude" size="40" value="<?=$_GET['longitude']?>"><br>
    Широта <input name="latitude" size="40" value="<?=$_GET['latitude']?>" /><br>
    Радиус <input name="radius" size="40" value="<?=$_GET['radius']?>"><br>
    <input type="submit" value="Искать!">
</form>
<?php
if(isset($_GET['longitude']) and strlen($_GET['longitude']) > 2)
{
    require_once('sphinxapi.php');
    function collectIds($arr)
    {
        return $arr['id'];
    }
    $_longitude = $_GET['longitude'];
    $_latitude = $_GET['latitude'];
    $_radius = (intval($_GET['radius']) > 0) ? intval($_GET['radius']) : 10;
    $search = new SphinxClient(); // Создание экземпляра клиента
    $search->SetServer("localhost", 9312); // Подсключаемся
    $search->SetMatchMode(SPH_MATCH_ALL); //
    $search->SetArrayResult(true); // Не использовать id документов в качестве ключей
    $search->SetLimits(0, 5); // Ограничить поиск 5 объектами
    $search->SetSortMode ( SPH_SORT_EXTENDED, "@geodist asc"); // Сортировать объекты по расстоянию от исходной точки поиска
    $search->SetGeoAnchor('latitude', 'longitude', deg2rad($_latitude), deg2rad($_longitude));
    $circle = (float) $_radius;
    $search->SetFilterFloatRange('@geodist', 0.0, $circle);
    $result = $search->Query('', 'closestStores');
    if($result AND $result['total'] > 0)
    {
        print_r($result);
        $ids = array_map('collectIds', $result['matches']);
        print_r($ids);
    }
}
?>

Если все настроено правильно, то под формой отобразится массив с результатами выборки.