Search API Solr. Автоисправление ошибок с помощью Solr Spell Checking

На сайте имеется поиск созданный модулями Views и Search API Solr Search. Задача - автоматически исправлять ошибки в искомых словах и показывать результаты для исправленных слов.

Solr Spell Checking
Solr Spell Checking

Для решения напишем небольшой модуль, который будет зависить от модуля Search API Spellcheck. Устанавливаем его и переходим к написанию собственного модуля. Для примера я назову модуль "search_api_misspell". Создаем файл "search_api_misspell.info":

name = Search misspell
description = Spelling suggestions from Search API services.
core = 7.x
package = Custom

dependencies[] = search_api_spellcheck

files[] = views/views_handler_area_misspell.inc

Далее создаем файл "search_api_misspell.module". Его содержимое:

<?php

/**
 * @file
 * Main module file.
 */

/**
 * Implements hook_views_api().
 */
function search_api_misspell_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'search_api_misspell') . '/views',
  );
}

/**
 * Implements hook_theme().
 */
function search_api_misspell_theme() {
  return array(
    'search_api_misspell' => array(
      'variables' => array('spellcheck' => NULL, 'options' => array()),
      'file' => 'search_api_misspell.theme.inc',
    ),
  );
}

/**
 * Implements hook_search_api_solr_query_alter().
 */
function search_api_misspell_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query) {
  if ($query->getOption('search_api_spellcheck') && (!isset($_GET['spell']) || $_GET['spell'] != 0)) {
    $call_args['params']['spellcheck.accuracy'] = 0.7;
    $call_args['params']['spellcheck.collate'] = 'true';
    $call_args['params']['spellcheck.collateExtendedResults'] = 'true';
    $call_args['params']['spellcheck.onlyMorePopular'] = 'false';
    $call_args['params']['spellcheck.maxCollationTries'] = 1;
  }
}

/**
 * Implements hook_search_api_solr_search_results_alter().
 */
function search_api_misspell_search_api_solr_search_results_alter(array &$results, SearchApiQueryInterface $query, $response) {
  if ($query->getOption('search_api_spellcheck') && (!isset($_GET['spell']) || $_GET['spell'] != 0)) {
    if (!empty($response->spellcheck->suggestions->collation->collationQuery)) {
      $query->setOption('parse mode', 'direct');
      $query->keys($response->spellcheck->suggestions->collation->collationQuery);
      $collate_results = $query->execute();

      $results['results'] = $collate_results['results'];
      $results['result count'] = $collate_results['result count'];
      $results['miss_spell'] = TRUE;
    }
  }
}

В данном файле имеется два основных хука:

  1. hook_search_api_solr_query_alter - используется для того, что бы изменить запрос или добавить некоторые параметры перед отправкой в Solr. В данном случае он используется для добавления параметров необходимых для работы SpellCheck компонента. Ознакомиться с данным компонентом и посмотреть полный список параметров и настроек можно здесь Solr Spell Checking;

  2. hook_search_api_solr_search_results_alter - используется для модификации результатов полученных от Solr. В данном случае мы анализируем ответ и делаем еще один запрос с исправленными словами если пользователем была допущена ошибка.

Далее создаем Views handler который будет включать опцию search_api_spellcheck и выводить данные об исправленных словах. Внутри нашего модуля создаем 2 файла:

"views/search_api_misspell.views.inc":

<?php

/**
 * @file
 * Keeping views hooks in an include reduces the size of Drupal's bootstrap.
 */

/**
 * Implements hook_views_data_alter().
 */
function search_api_misspell_views_data_alter(&$data) {
  foreach (search_api_index_load_multiple(FALSE) as $index) {
    $key = "search_api_index_{$index->machine_name}";

    try {
      if ($index->server() && $index->server()->supportsFeature('search_api_spellcheck')) {
        $data[$key]['search_api_misspell'] = array(
          'title' => t('Misspell'),
          'group' => t('Search'),
          'help' => t('Suggestions for spellings.'),
          'area' => array('handler' => 'views_handler_area_misspell'),
        );
      }
    }
    catch (SearchApiException $e) {
    }
  }
}

"views/views_handler_area_misspell.inc":

<?php

/**
 * @file
 * Views Search API area handler.
 */

/**
 * Area handlers are available to be placed in a views header and footer.
 */
class views_handler_area_misspell extends views_handler_area {

  /**
   * Overrides views_handler::pre_query().
   */
  function pre_query() {
    $this->query->setOption('search_api_spellcheck', TRUE);
  }

  /**
   * Overrides views_handler_area::render().
   */
  function render($empty = FALSE) {
    $results = $this->query->getSearchApiResults();

    if (!isset($results['search_api_spellcheck']) || empty($results['results']) || empty($results['miss_spell'])) {
      return '';
    }

    $options = array();
    foreach ($this->view->filter as $key => $filter) {
      if ($filter instanceof SearchApiViewsHandlerFilterFulltext) {
        $options['get'][] = $filter->options['expose']['identifier'];
      }
    }

    $variables = array(
      'spellcheck' => $results['search_api_spellcheck'],
      'options' => $options,
    );

    return theme('search_api_misspell', $variables);
  }

}

Теперь создаем файл "search_api_misspell.theme.inc":

<?php

/**
 * @file
 * Contains theme functions.
 */

/**
 * Returns HTML for a search API spellcheck.
 *
 * @param array $variables
 *   An associative array contains:
 *   - spellcheck: SearchApiSpellcheckInterface instance
 *   - options: An associative array containing:
 *     - get: (optional) An array of query keys which should be spellchecked
 *
 * @return string
 *   HTML for spelling suggestions.
 *
 * @throws \Exception
 */
function theme_search_api_misspell($variables) {
  $options = $variables['options'];
  $spellcheck = $variables['spellcheck'];
  $suggestion_links = array();

  if (isset($options['get'])) {
    foreach ($options['get'] as $get) {
      if ($link = $spellcheck->getSuggestionLinkForGet($get)) {
        $suggestion_links[] = $link;
      }
    }
  }

  $output = '';

  if (count($suggestion_links)) {
    $query = drupal_get_query_parameters();
    $query['spell'] = 0;

    $output .= '<div>' . t('Showing results for "!link"', array('!link' => $suggestion_links[0]->link)) . '</div>';
    $output .= '<div>' . t('Search results for "!link"', array(
      '!link' => l($suggestion_links[0]->original, current_path(), array(
        'query' => $query,
      )),
    )) . '</div>';
  }

  return $output;
}

Включаем модуль и добавляем включенный handler в хедер вьюса:

Views Spell Checking handler
Views Spell Checking handler

Spell Checking может использовать разные словари или сразу несколько. Словари могут отличаться настройками и должны быть описаны в файле "solrconfig_extra.xml":

<lst name="spellchecker">
  <str name="name">wordbreak</str>
  <str name="field">spell</str>
  <str name="classname">solr.WordBreakSolrSpellChecker</str>
  <str name="combineWords">true</str>
  <str name="breakWords">true</str>
  <int name="maxChanges">10</int>
</lst>

Для того, что бы указать, какой словарь использовать, необходимо в hook_search_api_solr_query_alter добавить параметр spellcheck.dictionary, например:

$call_args['params']['spellcheck.dictionary'] = array('default', 'wordbreak');

В данном примере будет использовано два словаря. Если этот параметр не передать, то будет использоваться default словарь.

Benya