Статьи

Использование Solarium с SOLR для поиска — реализация

Это третья статья в серии из четырех статей об использовании 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() и этими функциями, реализовать разбиение на страницы должно быть просто, но для краткости я не собираюсь останавливаться на этом здесь.

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

Давайте расширим наш поиск фильмов с базовым аспектом; мы позволим людям сузить поиск фильмов по рейтингу 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» во входном массиве.

У этого подхода есть некоторые ограничения — во-первых, нет способа «сбросить» фильтры, кроме как вручную изменить параметры запроса в адресной строке, но его цель — продемонстрировать использование нескольких аспектов. Я оставлю его вам в качестве дополнительного упражнения!