Реализация релевантного поиска с использованием phpmorphy

Всем добрый день (утро, вечер, ночь)
Много читал жалоб на поиск в друпале. И вот наконец с ним столкнулся. Да что уж тут скрывать поиск ужасный.
Видел много пожелание про phpmorphy, ну думаю а что бы нет, потыркаю.
Сразу говорю код привожу как пример реализации, всегда готов принять критику и пожелания по оптимизации.
Работу поиска можно посмотреть тут: Справочник Норильск

Поехали. Весь поиск у меня реализован одним PHP файлом+Вьюха, без модулей и прочего.
Распишу что да как.

Сначала я рисую формочку с поиском и расширенным фильтром:

<script type="text/javascript"> jQuery(document).ready(function(){ jQuery('.spoiler-body').hide() jQuery('.spoiler-head').click(function(){ jQuery(this).toggleClass("folded").toggleClass("unfolded").next().slideToggle() }) }) </script> <form> <span style="font-size:12px; padding-left:20px;"> <input type="checkbox" value="1" name="h" <?php if (isset($_GET['h'])) echo 'checked'; ?>>только дома <input type="checkbox" value="1" name="p" <?php if (isset($_GET['p'])) echo 'checked'; ?>>только организации </span> <div class="azure_block" style="margin-top:0; padding:10px;"> <input type="text" name="str" style="width:90%;" value="<?php print $_GET['str']; ?>"> <input type="submit" style="width:8%;" value="Найти"> </div> <div class="spoiler-head folded" style="cursor:pointer; color:#D63E03; font-weight:bold; padding-right:15px;" align=right>Расширенный поиск</div> <div class="spoiler-body"> <div class="yell_block"> <input type="checkbox" value="1" name="all" <?php if (isset($_GET['all'])) echo 'checked'; ?>> Искать только те материалы в которых присутствуют все поисковые слова <br /> <input type="checkbox" value="1" name="nomrph" <?php if (isset($_GET['nomrph'])) echo 'checked'; ?>> Искать только введенную форму слов (Не использовать склонения) <br /> <input type="checkbox" value="1" name="like" <?php if (isset($_GET['like'])) echo 'checked'; ?>> Искать по наличию части слова (Например: свал = самосвальный) </div> </div> </form>

Инициализируем phpmorphy. Скачать её можно по поиску в гугле, я взял версию словарей без Ё

<?php
$limit = 50;    
    
include_once('/бла-бла-бла/nprsu/phpmorphy/src/common.php');
$dir = '/бла-бла-бла/nprsu/phpmorphy/dicts';  
$lang = 'ru_RU';
$opts = array('storage' => PHPMORPHY_STORAGE_FILE,); #Экономим на ресурсах сервера не грузим в память.
try {$morphy = new phpMorphy($dir, $lang, $opts);} catch(phpMorphy_Exception $e) 
{die('Error occured while creating phpMorphy instance: ' . $e->getMessage());}

?>

Переменную $limit я используя для вывода только 50 результатов, но вот не надо мне больше, не хочу =( на самом деле грабли это из-за views по коду ниже будет пояснение, очень буду рад подсказке как обойти эти грабли.

Сам код и комментарии:

<?php
if ($_GET['str'] != '') #поисковая строка
{
#Фильтр по типам материалов у меня для вьюхи используется
$filter = 'all'; 
if (isset($_GET['h'])) $filter = 'house';
if (isset($_GET['p'])) $filter = 'place';
if (isset($_GET['h']) && isset($_GET['p'])) $filter = 'all';

$words = $_GET['str'];
#заменяем все Ё на Е, так как качали пхпморфи без Ё
$words = mb_strtoupper ( str_ireplace ( "ё", "е", $words ), "UTF-8" );
#Здесь мы делим строку в массив на слова либо цифры, остальное нам не нужно вообще, 
#плюс к этому в sql запрос не попадет грязи.
preg_match_all ( '/([a-zа-яё0-9]+)/ui', $words, $word_pma); 
#Получаем все формы слов из массива
$words_mor_all = $morphy->getAllForms($word_pma[1]); 
$sSQL_where = array();
$arMorphy = array(); #это массив в котором ключ слово а значение его ID 
if ($words_mor_all) #поехали
    {
    #Здесь мы заменяем равно на лайк если выбрана галочка искать по наличию части слова.    
    if (isset($_GET['like'])) {$sep = "LIKE"; $pr = '%';} else {$sep = "="; $pr = '';}
    #вот это порядковый номер который будет у всех словоформ образованных от одного слова 
        #и у него включительно равен 
    $vv = 0;
    foreach ($words_mor_all as $word=>$word_mor) #крутим все слова
    {        
        #если в словаре нашлись словообразования или нет галочки не использовать склонения то далее
        if (is_array($word_mor) && !isset($_GET['nomrph']) )
        {
            $sSQL_or = array();
            foreach ($word_mor as $word_m)
            {
            $arMorphy[$word_m]=$vv; #собираем массив слов и их ID
            $sSQL_or[] = "word {$sep} '{$pr}{$word_m}{$pr}'"; #собираем условие для запроса
            }
        $sSQL_where[] = "
                        (".join(' OR ', $sSQL_or).")"; #собираем where
        }
        else 
        {
            #В этом блоке обрабатываются слова у которых не нашлось словоформ
            $arMorphy[$word]=$vv; 
            $sSQL_where[] = "
                        (word {$sep} '{$pr}{$word}{$pr}')"; #собираем where
        }
    $vv++; #Увеличиваем ID слова, т.к. для этого слова условие выборки готово
    };
    $sSQL = "SELECT sid, word FROM {search_index} WHERE type = 'node' AND 
            ".join(" OR ", $sSQL_where)."
            "; #вот сам запрос. Таблицу я использую от стандартного поиска, значит нужен рабочий Cron
    #echo "<pre>".$sSQL."</pre>";
    $arIdsV = array();
    $arId2Mor = array();
    $arIdAll = array();
    $search_result = db_query($sSQL);
    #Крутим результаты и вычисляем вес материалов по релевантности
    while($ar_ids = db_fetch_object($search_result))
        {
            if (isset($arIdsV[$ar_ids->sid])) #Вот сам массив ID нод
            { #Все подымаем в верхний регистр т.к. пхпморфи работает с ним
                if (isset($arId2Mor[$ar_ids->sid][$arMorphy[mb_strtoupper($ar_ids->word, "UTF-8")]]))
                {
                    $arIdsV[$ar_ids->sid]++;
                }
                else 
                {
                    #Вообщем вот тут идет увлечение релевантности ноды в 10 раз, если она(нода) 
                    #содержит в себе несколько разных слов в любой форме склонения 
                    $arIdsV[$ar_ids->sid] = ($arIdsV[$ar_ids->sid]+1)*10; 
                    #Этот массив будет использоваться только в том случае, если стоит галочка 
                    #искать только те материалы в которых есть все слова из запроса.
                    $arIdAll[$ar_ids->sid]++;
                }
            } else
            {
                $arId2Mor[$ar_ids->sid][$arMorphy[mb_strtoupper($ar_ids->word, "UTF-8")]] = 1;
                $arIdsV[$ar_ids->sid]++;
            }        
        }
    #Если есть галочка искать только те материалы в которых есть все слова    
    if (isset($_GET['all'])) 
    {
        arsort($arIdAll); #Вот тут массив упорядочивается по убыванию веса релевантности нод. 
        $arIdsVAll = array();
        foreach ($arIdAll as $k=>$v)
        {
            if (($v+1) == $vv) #проверяем все ли слова есть в ноде
            {
            $arIdsVAll[$k] = $v;     
            }
        }
        $arIdsV = array_slice($arIdsVAll,0,$limit,true); #Я режу результаты до 50-ти
    }
    else 
    {
        arsort($arIdsV); #Вот тут массив упорядочивается по убыванию веса релевантности нод.
        $arIdsV = array_slice($arIdsV,0,$limit,true); #Я режу результаты до 50-ти
    }
    
#Вот и все мы имеем массив упорядоченный по убыванию веса нод и с ним уже можно делать что угодна. 
#Можно извлекать прям запросом и выводить. Я пошел по не совсем правильному пути и использовал вьюху. смотрим далее
    
    if ($arIdsV)
    {
        $ids = join(',', array_keys($arIdsV));
    
           $v_nprsearch = views_get_view ('nprsearch');
           $v_nprsearch->set_display('default');
        #Отдаю вьюхе ID нод через запятую. и тип материала по которому ищу, либо all если по всем.
           $v_nprsearch->set_arguments(array($ids,$filter));
        #буду выводить только 50, на всякий случай, хотя массив я итак порезал.
           $v_nprsearch->display_handler->set_option('items_per_page', $limit);
        $v_nprsearch->display_handler->set_option('use_pager', false);
        $v_nprsearch->preview();              
           $v_nprsearch->execute();

           if ($v_nprsearch->result) 
            {
                #Вот внимание сами грабли. Отдаю я значить вьюхе ID нод и он по ним выдаем 
                #мне ноды применяя к ним фильтр по типу материала из аргумента. 
                #Использует views их в запросе как where nid in (1,2,3 и т.п.) и соответствено сортировка
                 #то теряется и мне приходиться крутить еще раз два массива. Чтобы востановить искомую сортировку.
                $isset_result = array(); 
                   foreach ($v_nprsearch->result as $v_result)
                {
                    #Берем Ноду и говорим что она есть в фильтре.
                    $isset_result[$v_result->nid] = 1;
                }
                $new_result = array();
                #Крутим наши массив отсортированных нод
                foreach (array_keys($arIdsV) as $pre_ids)
                {
                    #и если нода в нашем массиве, есть в массиве результатов вьюхи 
                    #загоняем её в массив объектов как у вьюхи
                    if (isset($isset_result[$pre_ids])) 
                        $new_result[]->nid = $pre_ids;
                }
                #Подменяем массив уже с нашей сортировкой.
                   $v_nprsearch->result = $new_result;
           
           
                echo "<hr />";
                print $v_nprsearch->render(); #Рисуем вьюху. Вуаля!
                echo "<i>Показано до {$limit}-ти результатов.</i>";
            }
    } else echo "Нет результатов.";
} else echo "Нет результатов.";
} else echo "Введите поисковой запрос.";
?>

Немного о вьюхе.
Она простая до безобразия в аргументах Материала: Nid с функцией “Разрешить несколько терминов в аргументе” и Материал: Тип.
Стиль строки просто материал. по дефолту. можно по полям выводить ели кому надо.
И в фильтрах на Материал Тип чтобы искать только по тому по чему мне надо. Ну вот и все.

Итоги.
Поиск мне в принципе нравиться, еще бы замутить “возможно вы имели ввиду”, если есть орфографическая ошибка в словах.
Поиск я думаю будет весьма неплохой заменой стандартному, на не крупных проектах, у которых нет root доступа к серверу и прочее. виртуальных хостинг короче.
Писав код старался избегать большого количества итераций при вращении циков, использовал isset для проверки в массивах, так как он обращаеться напрямую к ячейке.
Готов выслушать критику и пожелания, дороботать я его всегда рад.

Источник: http://www.drupal.ru/node/89392

Работа с Facet API и Apache Solr. Часть 3

Привет.

Продолжаю свои заметки про работу с поисковиком Solr. В прошлых постах я писал о том, как добавлять в индекс дополнительные поля, как управлять процессом индексации определенного поля и как научить solr искать в этих полях. Сегодня расскажу о том, как можно договориться с solr’ом о том, что индексировать, а что нет. Оговорюсь, что нижеприведенный способ работает адекватно только начиная с версии apachesolr-1.x-beta19, раньше этого не было. Итак, рассказываю.

На самом деле рассказывать то особо и нечего, всё потрясающе просто. Представим, что по определенным признакам мы не хотим индексировать ноду, для этого у apachesolr есть status callback. В моем случае у типа материалов “публикация” есть чекбокс “Индексируемая”, по умолчанию отмеченный, но если галочку снимут, то публикация не будет ни индексироваться, ни, само собой, выводиться в результатах поиска. Сделано это следующим образом:

<?php
// Для начала мы должны сообщить модулю apachesolr о использовании кастомного коллбэка. Делается это с помощью
// реализации хука <strong>hook_apachesolr_entity_info_alter()</strong>, описанного в файле apachesolr.api.php.
// Таких коллбэков может быть несколько.
function inti_apachesolr_entity_info_alter(&$entity_info) {
  // Способ, как видите, работает не только для нод, а для любых сущностей.
  $entity_info['node']['status callback'][] = 'inti_index_node_status_callback';
}

// Эта функция должна возвращать TRUE, если публикация индексируемая, иначе FALSE.
// Коллбэк по умолчанию проверяет, опубликована ли нода. Учитывая, что коллбэков может быть
// несколько, как я написал выше, здесь мне это проверять нет нужды. 
function inti_index_node_status_callback($entity_id, $entity_type) {
  $node = node_load($entity_id, NULL, TRUE);
  $status = TRUE;
  if (($node->type == 'publication') && !empty($node->field_do_search)) {
    $status = ($node->field_do_search['und'][0]['value']) ? TRUE : FALSE;
  }
  return $status;
}
?>

Вот и всё. Важное примечание: как я понял, после изменения функции, реализующей hook_apachesolr_entity_info_alter(), нужно очистить кэш.

В следующий раз будет пост (тоже небольшой) о том, как научить Apache Solr искать с использованием wildcard(*). Как ни странно, но по умолчанию, поддержки поиска по частям слов нет.

Ссылки:
Часть 1. О добавлении полей в индекс и фасетах.
Часть 2. О подмене коллбэка для индексации определенного полям и о том, как искать по дополнительным полям.
Часть 4. Установка Solr 3.x и поиск с использованием *

Источник: http://www.drupal.ru/node/80497

Работа с Facet API и Apache Solr. Часть 2

Всем привет. В предыдущем посте я рассказал, как можно “повлиять” на процесс индексации, добавляя дополнительные поля в индекс, и создавать свои фасеты (фильтры) с помощью Facet API. Сегодня я хочу рассказать об альтернативном способе индексирования полей и возможности поиска по дополнительным полям. Дело в том, что Solr не ищет по всем полям, а только по тем, о которых его просят. Итак, поехали.

Представим задачу, когда нам нужно не добавить какое-то поле из связанных материалов, а изменить способ индексации какого-то текущего поля. Например, есть такой модуль Field Collection, позволяющий сделать поле-контейнер, содержащее в себе несколько полей. В моей структуре с помощью него у публикации указывается автор, а к автору – организация, в которой он работает:

В базе данных у меня есть таблица field_data_field_author_org, в которой есть поле value, которое указывает на entity_id поле таблиц field_data_field_author и field_data_field_jobs. Таким образом в индексированном документе публикации я получаю поле im_field_author_org, значение которого мне абсолютно не нужно. Вот для того, чтобы указать, как будет индексироваться поле, мне нужно определить indexing_callback для этого поля в функции, реализующей хук hook_apachesolr_field_mappings(), находящийся в файле apachesolr.api.inc:

<?php

function inti_apachesolr_field_mappings() {
  // Функция должна вернуть массив, в котором ключем может быть либо тип поля, либо значение 'per_field'. 
  // В первом случае мы указываем, как будут индексироваться поля этого типа, во втором - значением будет массив,
  // у которого ключем будет имя поля. Настоятельно рекомендую ознакомиться с документацией ко всем хукам, которые
  // я указываю в заметках, для этого я пишу, в каких файлах они находятся. Дело в том, что в документации
  // это описано довольно подробно, а я лишь хочу описать сам принцип.
  $mappings = array();
  $mappings['per-field']['field_author_org'] = array(
    'indexing_callback' => 'inti_field_author_org_indexing_callback',
    // В это поле я хочу записать имена авторов и организаций, поэтому меняю тип с int на text,
    // это повлияет на имя поля (см. мой предыдущий пост), которое в данном случае генерируется 
    // автоматически
    'index_type' => 'text',
    // Фасеты по этому полю мне не нужны, потому что их я формирую вручную (описано также в предыдщем посте)
    'facets' => FALSE,  
  );
  return $mappings;
}

function inti_field_author_org_indexing_callback($entity, $field_name, $index_key, $field_info) {
  $fields = array();

  // Получаем все значения нашего поля
  $field_values = array_map(function($n) { return $n['value']; }, $entity->{$field_name}['und']);  

  // По этим значениям достаем всех авторов и организации прямо из базы данных
  // В момент написания заметки я предположил, что можно было бы воспользоваться функционалом
  // самого модуля Field Collection, но это предположение я проверю позже. :)
  $select = db_select('node', 'n');
  $select->join('field_data_field_author', 'fdfa', 'fdfa.field_author_nid = n.nid');
  $select->condition('fdfa.entity_id', $field_values, 'IN');
  $select->fields('n', array('title'));
  $authors = $select->execute()->fetchCol();
  
  $select = db_select('node', 'n');
  $select->join('field_data_field_jobs', 'fdfj', 'fdfj.field_jobs_nid = n.nid');
  $select->condition('fdfj.entity_id', $field_values, 'IN');
  $select->fields('n', array('title'));
  $orgs = $select->execute()->fetchCol();

  // Формируем массив с $index_key в кач-ве ключей, и 
  // именами авторов и названиями всех организаций в кач-ве значений.
  $fields[] = array(
    'key' => $index_key,
    'value' => implode(' ', $authors),
  );
  $fields[] = array(
      'key' => $index_key,
      'value' => implode(' ', $orgs),
  );
  return $fields;
}

?>

Таким образом в результате индексирования поле у меня выглядит следующим образом:

[tm_field_author_org] => Array ( [0] => Иванов Е. С. Петрова Р. Ш. // Авторы [1] => Рога и копыта Министерство образования и науки // Организации )

Теперь мне нужно сообщить Solr’у, что по этому полю тоже необходимо производить поиск. Делается это с помощью реализации хука hook_apachesolr_query_alter(), описанном всё в том же чудесном файле apachesolr.api.php:

<?php

function inti_apachesolr_query_alter($query) {
  // Поля, в которых нужно искать, должны быть добавлены в параметр 'qf' поискового запроса.
  // Формат параметров - обычный массив, значения которого выглядят как fieldname^boost, т.е. тут же поисковик
  // будет уведомлен о приоритетах. В моём случае поле достаточно важное, поэтому приоритет делаю 
  // высоким. Напомню, что имя поля должно быть указано не то, которое в Drupal'е, а то, которое
  // в индексированном документе.
  
  $params = array('tm_field_author_org^25.0');
  $query->addParam('qf', $params);
}

?>

Кстати, $query – это интерфейс DrupalSolrQueryInterface, описанный в файле apachesolr.interface.inc, с которым рекомендую ознакомиться, если вы хотите узнать, как еще можно работать с запросом перед его отправкой.

Собственно, вот и всё. В следующем посте расскажу о status_callback и индексировании определенных нод.

Примечание: вышенаписанное актуально для текущих версий модулей apachesolr (7.x-1.0-beta19) и facetapi (7.x-1.0-rc4). Они активно развиваются, поэтому если у вас другая версия и что-то не работает, читайте release notes.

Ссылки:
Часть 1. О добавлении полей в индекс и фасетах.
Часть 3. О том, как не индексировать, если не хочется.
Часть 4. Установка Solr 3.x и поиск с использованием *

Источник: http://www.drupal.ru/node/79948

[Модуль] Advanced sphinx

Сделала на базе  sphinxsearch небольшой поисковый модуль. Предложения и тестирование весьма желательны. Первоначально модуль делала под свои нужды, так что все лишнее, на мой взгляд, отсутствует . Если понадобятся какие-то доработки, буду рада помочь. На d.org будет выложен позднее. Портирование на 7 будет, после отлова мелких багов, при наличии таковых.

Основные отличия от базового:

возможность автоматической генерации файла конфигурации; mysql вместо xmlpipe2; минимальные настройки файла конфигурации через админку (работает при автоматической генерации); UPD. Добавлено управление индексацией и демоном через админку. Функции внедрены по просьбе IT-patrol; может еще что-то…

Настройка:

Копируем модуль в “sites/all/modules”. Включаем в админке. Настройка модуля на странице “admin/settings/advanced_sphinx”. Если вы хотите автоматически сгенерировать конфиг, то поставьте галочку “Generate a configuration file”. Далее необходимо указать полный путь к папке, в которой храниться sphinx.conf или папку в которой могут быть созданы конфиг и другие папки. Права на запись обязательны. Также на этой странице можете выбрать типы материалов по которым будет осуществлен поиск, если не выбрано, то ищет по всем. Обратите внимание, что при каждом сохранении настроек модуля конфиг будет обновлен, если включена его генерация. запускаем индексатор /usr/bin/indexer –config /home/user/sphinx/config/sphinx.conf –all . Потом демон сфинкса /usr/sbin/searchd –config /home/user/sphinx/config/sphinx.conf . Все пути подставляйте свои. проверяем работоспособность на странице “admin/settings/advanced_sphinx/check-connection”. Добавляем для нужных ролей право на использование страницы поиска (”use advanced_sphinx”) в “admin/user/permissions”

Страница Advanced sphinx на github.

Источник: http://www.drupal.ru/node/65009

Модуль для создания семантического ядра сайта

Опубликовал месяца 3 назад на drupal.org модуль и забыл сделать тут анонс…
И нигде не делал анонс, но западные товарищи таки модуль нашли и уже стали постить баги и просить новые фичи.
Короче, – пришло время снова вернуться к модулю и ещё больше его улучшить, поэтому хочу услышать пожелания/баги от русского сообщества, а затем я уже возьмусь там что-то править.

Итак, модуль формирует семантическое ядро сайта.

Как он это делает?
При сохранении ноды её текст (боди), анонс, заголовок парсятся на предмет ключевых слов, которые вычисляются на основании частоты вхождений.
То есть в настройках модуля задаётся порог повторов, а также количество слов в ключевых фразах. Таким образом каждая нода получает вкладку (”таб”), который называется “Ключевые слова” и есть список этих самых ключевых слов и ключевых фраз (словосочетаний).

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

Вроде бы все описал. Писал по памяти – подробности на странице модуля keywords на drupal.org

Из-за того, что парсить ноды дело достаточно хлопотное, то в модуле есть запуск этого парсера (назовем его “индексация”) по крону для нод, у которых ещё нет списка ключевых слов – т.е., которые не проиндексированы модулем.

Кроме того, там реализована система хранения истории настроек модуля, потому что при смене параметров индексации нужно всю работу переделывать заново, а это не так быстро и просто. Работает так: при смене параметров нод, которые были индексированы со старыми параметрами считаются не имеющими ключевых слов и будут по крону заново проиндексированы. Либо это произойдет при открытии таба с ключевыми словами ноды.

Есть список стоп-слов, которые исключаются из индексации, но только русские и английские. Западные товарищи просили дать возможность добавлять другие языки… Думаю, что это нужно будет сделать.

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

PS. Забыл сказать, что найденные ключевые слова и фразы можно сохранять в любой словарь таксономии, который выбирается в админке модуля. Далее этот словарь можно сделать скрытым (он не показывается), а модуль NodeWords (Metatags) настроить на использование этого словаря для формирования мета тега description.
В общем, – есть место для творчества.

Источник: http://www.drupal.ru/node/38580

© 2009 Обзор CMS