Морфологический поиск на русском языке на PHP и MySQL
Важной функцией на сайте является поиск по разделу или по всему сайту. Это позволяет пользователю быстро найти необходимую информацию. Чаще всего этот вопрос решается с помощью полнотекстового поиска или поиска по базе через выражение LIKE. Полнотекстовый поиск удобен тем, что перекладывает вопрос ранжирования страниц по релевантности на СУБД, позволяет искать фразы.
Но вместе с тем накладывает некоторые ограничения, в частности, заставляет использовать таблицы типа MyISAM, не осуществляет поиск слов, короче 4-ех символов, создает дополнительные индексы. Поиск через выражение LIKE менее удобен для разработчика, так как работает медленнее, осложняет поиск фраз. Общим недостатком обоих подходов является невозможность морфологического поиска. Например, в каталоге товаров присутствет товар "Букет из желтых роз", при поиске по фразе "красные розы" пользователь не увидит этот товар в результатах. А хотелось бы, чтобы машина хоть немного понимала естественный язык. Решить эту проблему несложно на PHP и в этом нам поможет phpMorphy. phpMorphy позволяет решать следующие задачи:
- Лемматизация (получение нормальной формы слова)
- Получение всех форм слова
- Получение грамматической информации для слова (часть речи, падеж, спряжение и т.д.)
- Изменение формы слова в соответствии с заданными грамматическими характеристиками
- Изменение формы слова по заданному образцу
Поддерживаемые языки: Русский, Английский, Немецкий (AOT). Украинский, Эстонский (на основе ispell). Есть возможность добавить поддержку других языков при помощи myspell словаря. Словари можно взять здесь (http://sourceforge.net/projects/phpmorphy/files/phpmorphy-dictionaries/). Выбирайте в зависимости от версии библиотеки и кодировки.
Поддерживаются различные кодировки:
- все однобайтовые (windows-1251, iso-8859-* и т.п.)
- Unicode кодировки - utf-8, utf-16le/be, utf-32, ucs2, ucs4.
Следует учесть что использование этой библиотеки создаст некоторую дополнительную нагрузку на сервер при инициализации и работе.
Для начала скачайте архив библиотеки и распакуйте его, например, в папку "phpmorphy" . По возможности в директории недоступной web серверу. Далее, скачайте словари с определенным языком и кодировкой и распакуйте их в папку "phpmorphy/dicts" данной библиотеки. Для инициализации библиотеки подключите файл common.php из папки "phpmorphy/src" и выставите настройки:
С возможными опциями настройки библиотеки можно ознакомиться здесь (http://phpmorphy.sourceforge.net/dokuwiki/manual).
Для поиска мы будем использовать следующий подход: слова в поисковой фразе будем приводить к их начальной форме, индексировать контент для поиска также, приводя отдельные слова к начальной форме, и в зависимости от частоты повторения и местонахождения слова — задавать ему вес, далее — искать по таблице индексов поисковую фразу и ранжировать результаты по весу.
Для начала проиндексируем контент, по которому будет происходить поиск. В нашем примере поиск происходит по цветам в каталоге товаров. У товара есть идентификатор, наименование, краткое и полное описание. Кодировка — UTF-8, collate - utf8_bin . Создадим таблицу с товарами:
и таблицу, в которой будут храниться индексы:
здесь `word` - ключевое слово, `prod_id` - идентификатор товара, `weight` - вес слова. Каждая запись характеризуется уникальной парой слово-товар.
Далее возьмем из базы все записи о товарах:
При работе с библиотекой PhpMorphy cледует помнить, что термины в словарях хранятся в верхнем регистре, для удобства переведем контент в верхний регистр. Также возможно появление слов с буквой "ё" , collate utf8_bin гарантирует нам отсутствие ошибок, связанных с уникальностью ключей в таблице `prod2search` , так как буквы "е" и "ё" не будут считаться одним символом, однако будет неправильным если слова "ЗЕЛЕНЫЙ" и "ЗЕЛЁНЫЙ" будут индексироваться отдельно. Поэтому мы заменяем букву "ё" на "е" . В подробном описании могут содержаться html-теги, их мы не будем индексировать и от них следует избавиться.
Здесь приведем скрипт, индексирующий товары, а далее дадим пояснения.
Данное регулярное выражение разбивает контент на отдельные слова. Для кириллицы используется модификатор "u" . Следует помнить, что буква "ё" не попадает в диапазон "а-я" . Регулярное выражение корректно обработает такую строку:
Массив $word_pma[1] содержит список отдельных слов.
под $morphy мы подразумеваем экземпляр класса phpMorphy , $morphy->lemmatize() приводит слово в строке или массив слов к начальной форме и возвращает массив вида "Исходное слово" => Массив со списком слов в начальной форме . Метод lemmatize() по умолчанию сравнивает слово по словарям, при отрицательном результате пытается образовать начальную форму по внутренним правилам, если и это не удается вместо массива возвращается FALSE . В нашем примере мы использовали только русский словарь, но выбирали также и английские слова. Поэтому при невозможности образовать начальную форму мы индексировали входящее слово:
повторяется в коде несколько раз потому что, нам необходимо выставить разный вес словам в зависимости от их местонахождения. Словам в заголовке мы задаем вес 3, словам в кратком описании — 2, в полном — 1. При повторе слова увеличиваем его вес.
В итоге у нас получается массив $word для одного товара, который имеет вид "Слово" => "Вес" . Эти данные заносим в таблицу `prod2search` :
Скрипт еще возможно улучшить, например, запретить индексирование определенных предлогов; индексировать определенные части речи, например, только существительные и прилагательные; исключать поиск по словам, которые присутствуют более чем в половине записей, к примеру слово "описание" входит в полное описание каждого товара и поиск по этому слову и никак не характеризует конкретный товар.
Данный скрипт позволяет с нуля проиндексировать весь контент, и служит лишь дополнительной утилитой. В реальном приложении необходимо индексировать текстовый контент только при добавлении или редактировании конкретной записи. Для индексации контента воспользуемся приведенным выше скриптом, но только не будем использовать цикл и будем работать с одной записью. При редактировании, до внесения индексов в таблицу `prod2search` или при удалении товара - удалять неактуальные записи:
где $prod_id — идентификатор редактируемого или удаляемого товара.
Можно произвести некоторую оптимизацию. Допустим, кроме наименования, краткого и полного описания товар обладает еще несколькими характеристиками, и при редактировании пользователь меняет только цену. В этом случае индексированный контент не изменился и не требуется его повторная индексация. Этого можно избежать сравнив хеши индексируемого контента до и после редактирования товара. Сольем индексируемые поля в одну строку и получим ее хеш через md5($str) , далее вставим полученное значение в форму и передав данные на сервер сравним хеш до и после редактирования. Если хеши совпадут, значит контент не менялся и не требуется его переиндексация.
После того как проиндексирован весь контент и таблица `prod2search` заполнена данными можно приступать к реализации поиска.
Разберем запрос. Мы находим записи в таблице `prod2search` , в которых слова из списка слов в поисковой фразе WHERE `s`.`word` IN (" . implode ( ",", $sql ) . ") . При поиске по нескольким словам, несколько слов относятся к одному и тому же товару, чтобы товары в результате не дублировались мы группируем результат по товару GROUP BY `s`.`prod_id` . В этом же случае мы суммируем вес найденных слов для одного товара SUM(`weight`) AS `weight_sum` . Например, мы ищем «красные розы» - у товара, у которого оба слова встречаются в описании, вес будет больше, чем у товара с одним словом. Результаты выборки мы ранжируем по релевантности, то есть весу ORDER BY `weight_sum` DESC . Для того чтобы при поиске нескольких слов в результате мы получили только те товары, в описании которых встречаются ВСЕ попавшие в запрос слова, мы учитываем сколько слов для одного товара найдено COUNT(`s`.`prod_id`) AS `num` и ставим условие HAVING `num` = ".count ( $sql )." . В данном случае реализуется логическое «И», соединяющие слова в поисковой фразе, для логического «ИЛИ» условие следует упустить. В итоге мы получаем список идентификаторов товаров, которые подходят под критерий поиска. Для наглядности записи с товарами мы берем вложенным запросом, однако на практике, возможно, лучшим решением будет взять список товаров отдельным запросом по их идентификаторам. Вариант запроса с логическим «ИЛИ» и лимитами будет работать значительно быстрее.
Таким образом, на примере каталога цветов мы реализовали морфологический поиск на русском языке с использованием PHP, MySQL и PhpMorphy. Данный метод можно применить к любым разделам вашего сайта и сблизить посетителя и вас, просто выдав пользователю информацию, которую он искал.