Статьи

Геопространственный поиск с помощью SOLR и Solarium

В недавней серии статей я подробно рассмотрел SOLR и солярий Apache.

Резюмировать; SOLR — это поисковая служба с множеством функций, таких как многогранный поиск и выделение результатов, которая работает как веб-служба. Solarium — это библиотека PHP, которая позволяет вам интегрироваться с SOLR — локальным или удаленным — взаимодействуя с ним, как если бы он был встроенным компонентом вашего приложения. Если вы не знакомы ни с одним из них, тогда моя серия уже здесь , и я призываю вас взглянуть.

В этой статье я собираюсь посмотреть на другую часть SOLR, которая требует своего собственного обсуждения; Геопространственный поиск.

locationsearch

Пример

Я собрал простой пример приложения, чтобы сопровождать эту статью. Вы можете получить его от Github или посмотреть его здесь .

Прежде чем мы углубимся в это, давайте посмотрим на некоторые предпосылки.

Иногда вещи, которые вы хотите найти, имеют географическое положение. Часто это обеспечивает жизненный контекст. Мне очень хорошо, что я могу искать «итальянские рестораны», но я голоден — ресторан на другом континенте, каким бы хорошим он ни был, не поможет. Скорее, было бы гораздо полезнее иметь возможность поиска, который спрашивает «покажи мне итальянские рестораны, но в пределах 5 миль». Или, альтернативно, «покажи мне десять ближайших итальянских ресторанов». Вот тут и начинается геопространственный поиск.

Геопространственный поиск и точки

В геопространственных приложениях мы часто говорим о «точках»; т.е. конкретное географическое положение. В частности, мы действительно говорим о паре широта и долгота. Широта и долгота определяют точку на земном шаре, возможно, с точностью до нескольких метров.

Одна из проблем, когда вы разрабатываете что-либо, связанное с географическими точками, заключается в том, что вам нужен какой-то способ осмыслить их для людей, которые не думают по широте и долготе — что, я уверен, большинство из нас. Геолокация здесь пригодится, потому что она может использоваться для определения широты и долготы «где вы находитесь», без двусмысленности географических названий. (Если вы хотите использовать последний подход, я уже писал об этом раньше .)

Итак, первая проблема, когда вы выполняете какую-либо гео-связанную работу, состоит в том, чтобы решить, как определить начальную точку — то есть, где искать. В нашем примере приложения мы будем хеджировать наши ставки и использовать три подхода. Мы будем использовать функцию геолокации HTML5, чтобы браузер пользователя мог их найти. Для удобства и простоты мы включим произвольный список некоторых крупных городов, который при нажатии будет заполнять широту и долготу из некоторых жестко заданных значений. И, наконец, точно так же у нас есть все базы, и для геоков среди нас мы включим текстовые поля, в которые пользователи могут вручную вводить свою широту и долготу.

Настройка схемы

Чтобы наша настройка ядра SOLR поддерживала географические местоположения, нам необходимо выполнить некоторые настройки схемы.

Первое, что нам нужно сделать, это добавить тип поля location в schema.xml :

 <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/> 

Обратите внимание, что это поле состоит из подполей; то есть широта и долгота. Мы должны убедиться, что у нас есть подходящий тип для тех:

 <fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/> 

Как вы можете видеть, это в основном поле типа double (в частности, tdouble , внутренне представленное классом Java solr.TrieDoubleField ).

Оба эти объявления <fieldType> должны быть помещены в элемент <fields> вашего schema.xml .

Теперь, когда типы установлены, вы можете определить новое поле для хранения широты и долготы. В следующем примере я называю это latlon :

 <field name="latlon" type="location" indexed="true" stored="true" multiValued="false" /> 

Важно, чтобы multiValued был установлен в false — несколько пар multiValued не поддерживаются.

Вам также необходимо настроить динамическое поле для компонентов; т.е. широта и долгота. _coordinate относится к суффиксу, который мы указали при определении типа поля нашего location выше.

 <dynamicField name="*_coordinate" type="tdouble" indexed="true" stored="false"/> 

Объявления <field> и <dynamicField> в разделе <fields> .

Ваша схема теперь настроена на поддержку пар широта / долгота, и мы добавили поле с именем latlon . Далее давайте посмотрим, как заполнить это поле.

Вы найдете пример файла schema.xml в репозитории примера приложения .

Назначение данных о местоположении

Когда дело доходит до присвоения значения полю местоположения, вам нужно сделать это:

 $doc = {lat},{long} 

Итак, используя солярий:

 $doc->latlon = doubleval($latitude) . "," . doubleval($longitude); 

Обратитесь к разделу «Заполнение данных» для конкретного примера.

Геопространственные запросы в SOLR с солярием

Возможно, вы помните, что в третьей части серии SOLR мы рассмотрели помощников Solarium. По сути, они действуют как синтаксический сахар, позволяя создавать более сложные запросы, не беспокоясь о синтаксисе запросов SOLR.

Вот пример того, как добавить дополнительный фильтр к поисковому запросу, который — с учетом $latitude и $longitude — ограничивает результаты с точностью до $distance километров:

 $query->createFilterQuery('distance')->setQuery( $helper->geofilt( 'latlon', doubleval($latitude), doubleval($longitude), doubleval($distance) ) ); 

Если вы предпочитаете работать в милях, вам просто нужно умножить $distance на 1.609344 :

 $query->createFilterQuery('distance')->setQuery( $helper->geofilt( 'latlon', doubleval($latitude), doubleval($longitude), doubleval($distance * 1.609344)) ) ); 

Если вы хотите вернуть расстояние с результатами поиска, вам нужно добавить функцию geodist в список полей, используя те же значения, что и фильтр geofilt . Опять же, вы можете использовать помощника:

 $query->addField($helper->geodist( 'latlon', doubleval($latitude), doubleval($longitude) ) ); 

Гораздо полезнее добавить псевдоним поля, как в SQL, который вы можете использовать для получения значения позже. Соглашение с псевдонимами заключается в добавлении префикса и суффикса с подчеркиванием, например:

 $query->addField('_distance_:' . $helper->geodist( 'latlon', doubleval($latitude), doubleval($longitude) ) ); 

Теперь вы можете отобразить расстояние в результатах поиска:

 <ul> <?php foreach ($resultset as $document): ?> <li><?php print $doc->title ?> (<?php print round($document->_distance_, 2) ?> kilometres away)</li> <?php endforeach; ?> </ul> 

Для того, чтобы отсортировать результаты по расстоянию, вам нужно применить небольшую хитрость. Вместо того, чтобы использовать setSort , вам действительно нужно использовать запрос; затем он используется для «оценки» результатов на основе расстояния. Основной запрос SOLR будет выглядеть так:

 {!func}geodist(fieldname,lat,lng) 

Чтобы сделать это с помощью Solarium, снова используйте помощник:

 $query->setQuery('{!func}' . $helper->geodist( 'latlon', doubleval($latitude), doubleval($longitude) )); 

Конечным результатом этого является то, что оценка будет отражать близость; чем ниже оценка, тем ближе она географически.

Итак, чтобы отсортировать результаты по расстоянию, сначала ближе всего:

 $query->addSort('score', 'asc'); 

Достаточно теории; давайте что-нибудь построим.

Создание нашего примера приложения

Я создал простой пример приложения, в котором люди могут искать ближайшие аэропорты, которые вы можете найти на Github в папке solr . Здесь есть онлайн демо .

Он использует Silex в качестве основы и Twig для шаблонов. Вам не нужно глубоко знать ни то, ни другое, чтобы следовать, поскольку большая часть сложности приложения проистекает из интеграции SOLR, которая описана здесь.

Заполнение данных

Данные, которые мы используем, взяты из превосходного сервиса OpenFlights.org . Вы найдете файл данных в хранилище вместе с простым скриптом для заполнения поискового индекса — запустите его следующим образом:

 php scripts/populate.php 

Вот соответствующий раздел:

 // Now let's start importing while (($row = fgetcsv($fp, 1000, ",")) !== FALSE) { // get an update query instance $update = $client->createUpdate(); // Create a document $doc = $update->createDocument(); $doc->id = $row[0]; $doc->name = $row[1]; $doc->city = $row[2]; $doc->country = $row[3]; $doc->faa_faa_code = $row[4]; $doc->icao_code = $row[5]; $doc->altitude = $row[8]; $doc->latlon = doubleval($row[6]) . "," . $row[7]; // Let's simply add and commit straight away. $update->addDocument($doc); $update->addCommit(); // this executes the query and returns the result $result = $client->update($update); $num_imported++; // Sleep for a couple of seconds, lest we go too fast for SOLR sleep(2); } 

Создание формы поиска

Мы начнем с простой формы с полями долготы и широты, а также с раскрывающегося списка, с помощью которого пользователь может указать расстояние, которое будет ограничено:

 <form method="get" action="/"> <div class="form-group"> <a href="#/" id="findme" class="btn btn-default"><i class="icon icon-target"></i> Find my location</a> </div> <div class="form-group"> <label for="form-lat">Latitude</label> <input type="text" name="lat" id="form-lat" class="form-control" /> </div> <div class="form-group"> <label for="form-lat">Longitude</label> <input type="text" name="lng" id="form-lat" class="form-control" /> </div> <div class="form-group"> <label for="form-dist">Within <em>x</em> kilometers</label> <select name="dist" id="form-dist" class="form-control"> <option value="50">50</option> <option value="100">100</option> <option value="250">250</option> <option value="500">500</option> </select> </div> <div class="form-group"> <button type="submit" class="btn btn-primary"><i class="icon icon-search"></i> Search</button> </div> </form> 

Далее, давайте реализуем кнопку «найти меня», которая использует геолокацию HTML5 — если браузер пользователя поддерживает ее — для заполнения формы поиска.

 function success(position) { $('input[name="lat"]').val(position.coords.latitude); $('input[name="lng"]').val(position.coords.longitude); } function error(msg) { alert(msg); } $('#findme').click(function(){ if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(success, error); } else { error('not supported'); } }); 

Пользователи должны будут предоставить нашему приложению разрешение на их обнаружение, поэтому на самом деле лучше запускать это при некотором взаимодействии с пользователем, например, по нажатию кнопки, а не при загрузке страницы.

Наконец, мы предоставим список городов по умолчанию; пользователь может щелкнуть один, чтобы заполнить поля широты и долготы автоматически.

Вот HTML-код, показывающий ограниченное количество городов для краткости:

 <ul id="cities"> <li><a href="#/" data-lat="52.51670" data-lng="13.33330">Berlin, Germany</a></li> <li><a href="#/" data-lat="-34.33320" data-lng="-58.49990">Buenos Aires, Argentina</a></li> 

Соответствующий JavaScript чрезвычайно прост:

 $('#cities a').click(function(e){ $('input[name="lat"]').val($(this).data('lat')); $('input[name="lng"]').val($(this).data('lng')); }); 

Далее мы собираемся реализовать поиск.

Страница поиска

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

 // Display the search form / run the search $app->get('/', function (Request $request) use ($app) { $resultset = null; $query = $app['solr']->createSelect(); $helper = $query->getHelper(); $query->setRows(100); $query->addSort('score', 'asc'); if (($request->get('lat')) && ($request->get('lng'))) { $latitude = $request->get('lat'); $longitude = $request->get('lng'); $distance = $request->get('dist'); $query->createFilterQuery('distance')->setQuery( $helper->geofilt( 'latlon', doubleval($latitude), doubleval($longitude), doubleval($distance) ) ); $query->setQuery('{!func}' . $helper->geodist( 'latlon', doubleval($latitude), doubleval($longitude) )); $query->addField('_distance_:' . $helper->geodist( 'latlon', doubleval($latitude), doubleval($longitude) ) ); $resultset = $app['solr']->select($query); } // Render the form / search results return $app['twig']->render('index.twig', array( 'resultset' => $resultset, )); }); 

Стандартный код — довольно простая вещь — определение маршрута, получение соответствующих параметров и рендеринг вида.

Код, который выполняет поиск, использует код, который мы рассмотрели ранее. По сути это делает следующее:

  1. Создает запрос фильтра, ограничивая поиск в пределах $distance км от точки, указанной значениями $latitude и $longitude ; все три представлены как параметры GET
  2. Использует помощника geodist чтобы сообщить geodist какое поле нас интересует (поле latlon, которое мы определили ранее), чтобы отсортировать результаты
  3. Добавляет псевдополь _distance_ чтобы мы могли включить его в результаты поиска
  4. Запускает запрос и присваивает его результат представлению.

Отображение результатов

Вот часть шаблона, которая отвечает за отображение результатов поиска:

 {% if resultset %} {% for doc in resultset %} <article> <h4><i class="icon icon-airplane"></i> {{ doc.name }}</h4> <p><strong>{{ doc.city }}</strong>, {{ doc.country}} ({{ doc._distance_|number_format }} km away)</p> </article> <hr /> {% endfor %} {% endif %} 

Это довольно просто; обратите внимание, как поле _distance_ доступно в нашем документе результатов поиска вместе с полями name и country . Мы используем фильтр Twig number_format для форматирования расстояния.

Вот и все, что нужно сделать — полный пример вы найдете в хранилище .

Конечно, этот пример только поиск по расстоянию. Конечно, вы можете комбинировать текстовый поиск с геопространственным поиском — я оставлю это в качестве упражнения.

Резюме

В этой статье я показал, как вы можете использовать SOLR — в сочетании с PHP-библиотекой Solarium — для выполнения геопространственных поисков. Мы рассмотрели некоторые из теорий, а затем погрузились в настройку нашей схемы, построение нашего запроса и применение его на практике.

Обратная связь? Комментарии? Оставь их ниже!