Семантический поиск текста с учетом морфологии - серьезная задача, с которой даже крупные поисковики справились с разной степенью успеха. Но частично реализовать поиск с учетом разных форм слов русского языка вы можете самостоятельно. Для этого лучше всего воспользоваться PHP-классом
phpMorphy. Это очень удобное средство для организации поиска с учетом морфологии русского, английского, украинского, эстонского или немецкого языков. Словари для каждого языка суммарно занимают около 10 Мб, а сам подключаемый скрипт - чуть меньше 30 Кб. При этом не требуется устанавливать на сервер дополнительное программное обеспечение, все будет работать на самом обычном хостинге.
Начнем с подключения phpMorphy. Это делается очень просто. Сперва скачайте дистрибутив,
словари, распакуйте все это в отдельную папку, соблюдая структуру вложенных каталогов. Словари разместите в папке
dict. Теперь откроем файл примера из папки
examples и посмотрим настройки:
// подключение движка phpMorphy
require_once('src/common.php');
// настройки движка
$opts = array(
...
);
// подключение словарей русского языка
$dir = 'dicts';
$dict_bundle = new phpMorphy_FilesBundle($dir, 'rus');
// создание класса
$morphy = new phpMorphy($dict_bundle, $opts);
В настройках используется важный параметр
storage, он может принимать одно из трех значений: PHPMORPHY_STORAGE_FILE (не загружать файлы словарей в память целиком, это самый медленный вариант, но самый экономный в плане работы с ресурсами сервера), PHPMORPHY_STORAGE_SHM (загружать файл словаря целиком в shared-память, требуется расширение PHP shmop) или PHPMORPHY_STORAGE_MEM (также загружать файл в память целиком если не используется shmop, по скорости работы ничем не отличается от предыдущего). На виртуальном хостинге, скорее всего, придется использовать первый вариант, а на выделенном сервере для большей скорости лучше применять варианты с использованием памяти. Выберите вариант под свои задачи. Остальные настройки и их значения подробно расписаны в
официальной документации.
Словари загружены, скрипт подключен, можно пробовать его в деле. Допустим, что надо найти текст "
Примерная строка поиска":
$bulk_words = array('ПРИМЕРНАЯ', 'СТРОКА', 'ПОИСКА');
// Получить нормализованные слова
$base_form = $morphy->getBaseForm($bulk_words);
// Получить все словоформы
$all_forms = $morphy->getAllForms($bulk_words);
// Получить корни слов
$pseudo_root = $morphy->getPseudoRoot($bulk_words);
Обратите внимание, что перед передачей phpMorphy поисковая строка переводится в верхний регистр и разделяется на отдельные слова. Вот что у нас получается. Нормализованные слова (массив
$base_form):
Array (
[СТРОКА] => Array (
[0] => СТРОКА
)
[ПРИМЕРНАЯ] => Array (
[0] => ПРИМЕРНЫЙ
)
[ПОИСКА] => Array (
[0] => ПОИСК
)
)
Все словоформы (массив
$all_forms):
Array (
[СТРОКА] => Array (
[0] => СТРОКА
[1] => СТРОКИ
[2] => СТРОКЕ
[3] => СТРОКУ
[4] => СТРОКОЙ
[5] => СТРОКОЮ
[6] => СТРОК
[7] => СТРОКАМ
[8] => СТРОКАМИ
[9] => СТРОКАХ
)
[ПРИМЕРНАЯ] => Array (
[0] => ПРИМЕРНЫЙ
[1] => ПРИМЕРНОГО
[2] => ПРИМЕРНОМУ
[3] => ПРИМЕРНЫМ
[4] => ПРИМЕРНОМ
[5] => ПРИМЕРНАЯ
[6] => ПРИМЕРНОЙ
[7] => ПРИМЕРНУЮ
[8] => ПРИМЕРНОЮ
[9] => ПРИМЕРНОЕ
[10] => ПРИМЕРНЫЕ
[11] => ПРИМЕРНЫХ
[12] => ПРИМЕРНЫМИ
[13] => ПРИМЕРЕН
[14] => ПРИМЕРНА
[15] => ПРИМЕРНО
[16] => ПРИМЕРНЫ
[17] => ПРИМЕРНЕЕ
[18] => ПРИМЕРНЕЙ
[19] => ПОПРИМЕРНЕЕ
[20] => ПОПРИМЕРНЕЙ
)
[ПОИСКА] => Array (
[0] => ПОИСК
[1] => ПОИСКА
[2] => ПОИСКУ
[3] => ПОИСКОМ
[4] => ПОИСКЕ
[5] => ПОИСКИ
[6] => ПОИСКОВ
[7] => ПОИСКАМ
[8] => ПОИСКАМИ
[9] => ПОИСКАХ
)
)
Корни слов (массив
$pseudo_root):
Array (
[СТРОКА] => Array (
[0] => СТРОК
)
[ПРИМЕРНАЯ] => Array (
[0] => ПРИМЕР
)
[ПОИСКА] => Array (
[0] => ПОИСК
)
)
Как нам это поможет в поиске? Тут есть несколько вариантов. Например, при загрузке или редактировании статьи ее текст при помощи phpMorphy разбирается на нормализованные слова (то есть начальная форма слова) и они сохраняются в базу. Это может быть дополнительное поле в таблице статей, для которого создается индекс
FULLTEXT.
SELECT * FROM `articles`
WHERE MATCH (`article_text_index`) AGAINST ('+СТРОКА +ПРИМЕРНЫЙ +ПОИСК')
Еще один вариант я встречал в движке форума punBB. Там текст каждого сообщения разделялся на отдельные слова, а затем они сохранялись в таблицу со ссылкой на сообщение. Поисковая строка также сперва разбивается по отдельным словам, и поиск в базе выполняется уже по этим данным, а не по исходному тексту. В нашем случае можно облегчить задачу и повысить точность поиска, если каждое слово перед занесением в базу и слова из поисковой строки будут преобразоваться к начальной форме.
SELECT DISTINCT(`index_article_id`) FROM `index_articles`
WHERE `index_word` IN ('СТРОКА', 'ПРИМЕРНЫЙ', 'ПОИСК')
Релевантность поиска в этом случае можно считать по количеству входящих слов, добавив в запрос команду
GROUP.
Для совсем небольших сайтов, где не планируется большая нагрузка, можно извлечь корни из слов искомой строки, а затем использовать эти данные в запросе:
SELECT * FROM `articles`
WHERE UPPER(`article_text`) LIKE ‘%СТРОК%’
OR UPPER(`article_text`) LIKE ‘%ПРИМЕР%’
OR UPPER(`article_text`) LIKE ‘%ПОИСК%’
Искать по всем словоформам в этом случае не имеет смысла, точность результата будет такая же, а нагрузка на базу возрастет.
Для серьезных проектов, конечно, потребуется и серьезная поисковая мощность, например,
Sphinx - Open Source Search Server. Но это уже выходит за рамки данной статьи.