Это третья статья в серии из четырех статей об использовании Solarium в сочетании с поисковой реализацией Apache SOLR.
В первой части я представил ключевые концепции, и мы установили и настроили SOLR. Во второй части мы установили и настроили Solarium, библиотеку, которая позволяет нам использовать PHP для «общения» с SOLR, как если бы он был нативным компонентом.
Теперь мы наконец готовы приступить к созданию поискового механизма, который является предметом данной статьи.
Базовый поиск
Давайте посмотрим, как реализовать действительно простой поиск:
$query = $client->createSelect(); $query->setQuery(Input::get('q'));
Input::get('q')
— это просто способ Laravel получить переменнуюGET
илиPOST
именемq
которая, как вы помните, является именем нашего элемента формы поиска.
Или, что еще лучше, используйте заполнитель, чтобы избежать поисковой фразы:
$query->setQuery('%P1%', array(Input::get('q')));
Заполнитель указывается символами%. Буква «Р» означает «избегайте этого как фразы». Связанные переменные передаются в виде массива, а число указывает позицию в массиве аргумента, который вы хотите связать; учитывая, что (возможно, необычно) 1 указывает на первый пункт.
Чтобы запустить поиск:
$resultset = $client->select($query);
Теперь вы можете получить количество результатов, используя метод getNumFound()
, например:
printf('Your search yielded %d results:', $resultset->getNumFound());
$resultset
является экземпляром Solarium\QueryType\Select\Result\Result
, который реализует интерфейс Iterator
— так что вы можете перебирать результаты следующим образом:
foreach ($resultset as $document) { . . . }
Каждый результат является экземпляром Solarium\QueryType\Select\Result\Document
, который предоставляет два способа доступа к отдельным полям — как к общедоступным свойствам, например:
<h3><?php print $document->title ?></h3>
Или вы можете перебирать доступные поля:
foreach($document AS $field => $value) { // this converts multi-value fields to a comma-separated string if(is_array($value)) $value = implode(', ', $value); print '<strong>' . $field . '</strong>: ' . $value . '<br />'; }
Обратите внимание, что многозначные поля, такие как cast
будут возвращать массив; поэтому в приведенном выше примере они просто свернут эти поля в список через запятую.
Итак, вот обзор того, как это сделать — теперь давайте подключим его к нашему примеру приложения.
Мы сделаем так, чтобы поиск отвечал на запрос GET
а не на POST
потому что это облегчит задачу, когда мы начнем смотреть на многогранный поиск, хотя на самом деле поиск по сайту очень часто использует GET
.
Таким образом, индексный маршрут на домашнем контроллере (в конце концов, наше приложение имеет только одну страницу) становится следующим:
/** * Display the search form / run the search. */
public function getIndex ()
{
if ( Input :: has ( 'q' )) {
// Create a search query $query = $this -> client -> createSelect ();
// Set the query string $query -> setQuery ( '%P1%' , array ( Input :: get ( 'q' )));
// Execute the query and return the result $resultset = $this -> client -> select ( $query );
// Pass the resultset to the view and return.
return View :: make ( 'home.index' , array (
'q' => Input :: get ( 'q' ),
'resultset' => $resultset ,
));
}
// No query to execute, just return the search form.
return View :: make ( 'home.index' );
}
Теперь давайте app/views/home/index.blade.php
представление — app/views/home/index.blade.php
— так, чтобы оно отображало результаты поиска, а также счетчик результатов, добавив его под формой поиска:
@if (isset($resultset)) <header> <p>Your search yielded <strong>{{ $resultset->getNumFound() }}</strong> results:</p> <hr /> </header> @foreach ($resultset as $document) <h3>{{ $document->title }}</h3> <dl> <dt>Year</dt> <dd>{{ $document->year }}</dd> @if (is_array($document->cast)) <dt>Cast</dt> <dd>{{ implode(', ', $document->cast) }}</dd> @endif </dl> {{ $document->synopsis }} @endforeach @endif
Попробуйте выполнить несколько поисков. Довольно быстро вы можете заметить серьезное ограничение. Например, попробуйте поискать «Звездные войны», обратите внимание на первые несколько результатов, а затем выполните поиск «Марк Хэмилл». Результатов нет — похоже, что при поиске учитывается только атрибут title, но не актерский состав.
Чтобы изменить это поведение, нам нужно использовать компонент DisMax . DisMax — это сокращение от Disjunction Max. Разъединение означает, что он ищет в нескольких полях. Макс означает, что если запрос соответствует нескольким полям, максимальное количество баллов складывается вместе.
Чтобы указать, что мы хотим выполнить запрос DisMax:
$dismax = $query->getDisMax();
Затем мы можем указать поиску искать в нескольких полях — разделить их пробелом:
$dismax->setQueryFields('title cast synopsis');
Теперь, если вы попытаетесь снова найти «Mark Hamill», вы увидите, что поиск подбирает актерский состав, а также заголовок.
Мы можем сделать наш запрос DisMax еще одним шагом, добавив веса к полям. Это позволяет вам расставлять приоритеты для определенных полей над другими — например, вы, вероятно, хотите, чтобы совпадения по названию давали более высокий балл, чем сопоставление слов в кратком изложении. Взгляните на следующую строку:
$dismax->setQueryFields('title^3 cast^2 synopsis^1');
Это указывает на то, что мы хотим, чтобы совпадения на поле приведения были взвешены намного выше, чем краткий обзор — на величину два — и поле заголовка еще дальше. Для ваших собственных проектов вы, вероятно, захотите поиграть и поэкспериментировать с различными запросами, чтобы попытаться определить оптимальные веса, которые, вероятно, будут очень специфичными для рассматриваемого приложения.
Итак, просто подведем итог, мы можем реализовать поиск по нескольким полям, изменив app/controllers/HomeController.php
следующим образом:
// Set the query string $query->setQuery('%P1%', array(Input::get('q'))); // Create a DisMax query $dismax = $query->getDisMax(); // Set the fields to query, and their relative weights $dismax->setQueryFields('title^3 cast^2 synopsis^1'); // Execute the query and return the result $resultset = $this->client->select($query);
Определение полей для возврата
Если вы запустите поиск, то для каждого документа набора результатов итерируйте по полям, вы увидите, что по умолчанию возвращаются все поля, добавленные в индекс. Кроме того, SOLR добавляет поле _version_
и score
связанную с результатом поиска, вместе с уникальным идентификатором.
Оценка представляет собой числовое значение, которое выражает актуальность результата.
Если вы хотите изменить это поведение, вы можете использовать три метода:
$query->clearFields(); // return no fields $query->addField('title'); // add 'title' to the list of fields returned $query->addFields(array('title', 'cast')); // add several fields to the list of those returned
Обратите внимание, что вам, вероятно, нужно использовать clearFields()
в сочетании с addField()
или addFields()
:
$query->clearFields()->addFields(array('title', 'cast'));
Как и в SQL, вы можете использовать звездочку в качестве подстановочного знака, то есть выбрать все поля:
$query->clearFields()->addFields('*');
Сортировка результатов поиска
По умолчанию результаты поиска будут возвращаться в порядке убывания оценки. В большинстве случаев это, вероятно, то, что вы хотите; «Лучшие совпадения» появляются первыми.
Однако вы можете изменить это поведение, если хотите:
$query->addSort('title', 'asc');
Синтаксис, вероятно, будет выглядеть знакомо; это очень похоже на SQL.
пагинация
Вы можете указать start
позицию — то есть, где начать вывод результатов — и количество возвращаемых rows
. Думайте об этом как о предложении SQL LIMIT
. Например, чтобы взять первые сто результатов, вы должны сделать следующее:
$query->setStart(0); $query->setRows(200);
Вооружившись результатом getNumFound()
и этими функциями, реализовать разбиение на страницы должно быть просто, но для краткости я не собираюсь останавливаться на этом здесь.
Начало работы с SOLR Faceted Search
Фасетный поиск, по сути, позволяет «детализировать» результаты поиска на основе одного или нескольких критериев. Это, вероятно, лучше всего иллюстрируется онлайн-магазинами, где вы можете уточнить поиск товаров по категориям, форматам (например, в мягкой обложке или в твердом переплете против цифровых книг), будь то в наличии на складе или по ценовому диапазону.
Давайте расширим наш поиск фильмов с базовым аспектом; мы позволим людям сузить поиск фильмов по рейтингу MPGG (сертификат, указывающий соответствующий возрастной диапазон для фильма, например, «R» или «PG-13»).
Чтобы создать фасет на основе поля, вы делаете это:
$facetSet = $query->getFacetSet(); $facetSet->createFacetField('rating') ->setField('rating');
После запуска поиска набор результатов теперь можно разбить на основе значения поля — и вы также можете отобразить счетчик для этого конкретного значения.
$facet = $resultset->getFacetSet()->getFacet('rating'); foreach($facet as $value => $count) { echo $value . ' [' . $count . ']<br/>'; }
Это даст вам что-то вроде этого:
Unrated [193] PG [26] R [23] PG-13 [16] G [9] NC-17 [0]
Фасет не должен использовать отдельные, разные значения. Вы можете использовать диапазоны — например, у вас могут быть диапазоны цен на сайте электронной коммерции. Чтобы проиллюстрировать диапазоны аспектов в нашем поиске фильмов, мы позволим людям сузить свой поиск до фильмов определенного десятилетия.
Вот код для создания фасета:
$facet = $facetSet->createFacetRange('years') ->setField('year') ->setStart(1900) ->setGap(10) ->setEnd(2020);
Это указывает на то, что мы хотим создать фасет на основе диапазона в поле года. Нам нужно указать начальное значение — 1900 год — и конец; конец текущего десятилетия. Нам также необходимо установить разрыв; Другими словами, мы хотим получить прирост от десяти до десяти. Чтобы отобразить счетчик в наших результатах поиска, мы могли бы сделать что-то вроде этого:
$facet = $resultset->getFacetSet()->getFacet('years'); foreach($facet as $range => $count) { if ($count) { printf('%d's (%d)<br />', $range, $count); } }
Это приведет к чему-то вроде этого:
1970's (12) 1980's (6) 2000's (8)
Обратите внимание, что фасет будет содержать все возможные значения, поэтому важно проверить, что число не равно нулю, прежде чем отображать его.
Фасетный поиск: фильтрация
До сих пор мы использовали фасеты на странице результатов поиска для отображения количества, но это ограниченное использование, если мы не можем позволить пользователям фильтровать свои поиски по ним.
В обратном поиске давайте сначала проверим, был ли применен рейтинговый фильтр MPGG:
if (Input::has('rating')) { $query->createFilterQuery('rating')->setQuery(sprintf('rating:%s', Input::get('rating'))); }
На самом деле, как и в случае с основным поисковым запросом, мы можем солярием избежать поискового запроса, а не использовать sprintf
:
if (Input::has('rating')) { $query->createFilterQuery('rating')->setQuery('rating:%T1%', array(Input::get('rating'))); }
Помните, 1 означает, что мы хотим использовать первый элемент массива аргументов — это не массив с нулями. T
указывает на то, что мы хотим избежать значения в качестве термина (в отличие от P
для фразы).
Фильтрация по десятилетиям немного сложнее, потому что мы фильтруем по диапазону, а не по дискретному значению. У нас есть только одно указанное значение — в Input::get('decade')
— но мы знаем, что верхняя граница — это просто начало десятилетия плюс девять. Так, например, «Восьмидесятые» представлены значением 1980, а диапазон от 1980 до (1980 + 9) = 1989.
Запрос диапазона принимает следующую форму:
field: [x TO y]
Так было бы:
year: [1980 TO 1989]
Мы можем реализовать это следующим образом:
if (Input::has('decade')) { $query->createFilterQuery('years')->setQuery(sprintf('year:[%d TO %d]', Input::get('decade'), (Input::get('decade') + 9))); }
В качестве альтернативы мы можем использовать вместо помощника. Чтобы получить экземпляр вспомогательного класса:
$helper = $query->getHelper();
Чтобы использовать это:
if (Input::has('decade')) { $query->createFilterQuery('years')->setQuery($helper->rangeQuery('year', Input::get('decade'), (Input::get('decade') + 9))); }
Хотя это может показаться довольно академичным, стоит знать, как создать экземпляр помощника Solarium, потому что он очень полезен для других целей, таких как геопространственная поддержка.
Фасетный поиск: вид
Теперь, когда мы рассмотрели, как настроить фасетный поиск, как перечислить фасеты и как запускать фильтры на их основе, мы можем настроить соответствующее представление.
Откройте app/views/home/index.blade.php
и измените раздел результатов поиска, app/views/home/index.blade.php
в него дополнительный столбец, который будет содержать наши аспекты:
@if (isset($resultset)) <div class="results row" style="margin-top:1em;"> <div class="col-sm-4 col-md-4 col-lg-3"> <?php $facet = $resultset->getFacetSet()->getFacet('rating'); ?> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">By MPGG Rating</h3> </div> <ul class="list-group"> @foreach ($facet as $value => $count) @if ($count) <li class="list-group-item"> <a href="?{{ http_build_query(array_merge(Input::all(), array('rating' => $value))) }}">{{ $value }}</a> <span class="badge">{{ $count }}</span> </li> @endif @endforeach </ul> </div> <?php $facet = $resultset->getFacetSet()->getFacet('years'); ?> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">By Decade</h3> </div> <ul class="list-group"> @foreach ($facet as $value => $count) @if ($count) <li class="list-group-item"> <a href="?{{ http_build_query(array_merge(Input::all(), array('decade' => $value))) }}">{{ $value }}'s</a> <span class="badge">{{ $count }}</span> </li> @endif @endforeach </ul> </div> </div> <div class="col-sm-8 col-md-8 col-lg-9"> <!-- SEARCH RESULTS GO HERE, EXACTLY AS BEFORE --> </div> </div> @endif
Мы делаем то, что обсуждали в разделе о фасетном поиске; захват набора фасетов, итерация по каждому элементу и отображение его вместе со счетчиком количества результатов для этого конкретного значения.
Каждый элемент фасета является ссылкой, которая при нажатии обновляет страницу, но с применением этого фильтра. Это достигается путем слияния соответствующего значения с текущим «активным» набором параметров GET; поэтому, если вы уже отфильтровали один фасет, щелчок по элементу в другом наборе фасетов сохранит этот фильтр, включив соответствующие параметры запроса. Он также будет поддерживать ваш исходный запрос, который установлен как «q» во входном массиве.
У этого подхода есть некоторые ограничения — во-первых, нет способа «сбросить» фильтры, кроме как вручную изменить параметры запроса в адресной строке, но его цель — продемонстрировать использование нескольких аспектов. Я оставлю его вам в качестве дополнительного упражнения!