Blog. Just Blog
Релевантный поиск по базе MySQL

Релевантный поиск по базе MySQL
Я уже писал о возможностях поиска с учетом морфологии, а теперь обещанная статья о релевантном поиске по базе MySQL. Как разъясняют словари, релевантность - в поисковых системах - мера соответствия результатов поиска задаче поставленной в запросе. То есть чем ближе найденный результат соответствует искомому, тем выше в результатах поиска он должен находиться. Применительно к выборке из базы, в строках результата релевантность должна быть представлена неким числовым значением, по которому эта выборка должна быть отсортирована.
Начнем с теории. Если мы ищем строку из нескольких слов среди нескольких текстов, то наибольшей релевантностью обладает текст, в котором встречается вся эта строка целиком и точно в том виде, как ее задали к поиску. Затем идут тексты, где есть все слова из искомой фразы, но расположенные не по порядку. После них идут тексты, где встречаются только отдельные слова, и, чем меньше слов из фразы, тем ниже релевантность. К тому же слова из заголовка текста должны иметь поисковый вес больше, чем такие же слова из текста.
Теперь попробуем это реализовать описанный выше алгоритм на PHP и MySQL. На первом шаге надо разобрать поисковую строку на отдельные слова, при этом надо будет отбросить слова, короче трех символов. Если вы внимательно читали комментарии к статье о морфологическом поиске, то наверняка увидели там полезный совет по укорачиванию длинных слов. Его я тоже задействую.
Code (PHP) : Убрать нумерацию
- // Разобрать искомую строку $search на отдельные слова
- preg_match_all('/[[:alnum:]]{3,}/is',stripslashes($search),$matches);
- $words=array_unique($matches[0]);
- $true_words=Array();
- if (count($words)) {
- foreach($words as $word) {
- // Обрабатывать только слова длиннее 3 символов
- if (strlen($word)>3) {
- // От слов длиннее 7 символов отрезать 2 последних буквы
- if (strlen($word)>7) {
- $word=substr($word,0,(strlen($word)-2));
- }
- // От слов длиннее 5 символов отрезать последнюю букву
- elseif (strlen($word)>5) {
- $word=substr($word,0,(strlen($word)-1));
- }
- $true_words[]=addcslashes(addslashes($word),'%_');
- }
- }
- }
- // Список уникальных поисковых слов
- $true_words=array_unique($true_words);
Заголовок: Мама мыла раму или история успеха
Текст: Однажды мама мыла раму и заработала на этом кучу денег. Теперь у мамы своя фирма по мытью окон.
Как я уже говорил, максимальное значение релевантности будет у текстов, которые содержат искомую фразу целиком, поэтому возьмем фразу "мама мыла раму". Считаем релевантность текста:
Заголовок:
"мама мыла раму" = 60%
вес каждого отдельного слова = (20/3) = ~6.66
"мама" = 6.66%
"мыла" = 6.66%
"раму" = 6.66%
Текст:
"мама мыла раму" = 10%
вес каждого отдельного слова = (10/3) = ~3.33
"мама" = 3.33%
"мыла" = 3.33%
"раму" = 3.33%
Релевантность: ~100%
Небольшая погрешность возникла из-за округления результатов деления. Изменим заголовок текста:
Заголовок: Деловая мама или история успеха
Текст: Однажды мама мыла раму и заработала на этом кучу денег. Теперь у мамы своя фирма по мытью окон.
Смотрим как изменится релевантность при той же поисковой строке:
Заголовок:
вес каждого отдельного слова = (20/3) = ~6.66
"мама" = 6.66%
Текст:
"мама мыла раму" = 10%
вес каждого отдельного слова = (10/3) = ~3.33
"мама" = 3.33%
"мыла" = 3.33%
"раму" = 3.33%
Релевантность: ~26.7%
При этом запрос "история успеха" на том же тексте даст следующий результат:
Заголовок:
"история успеха" = 60%
вес каждого отдельного слова = (20/2) = 10
"история" = 10%
"успеха" = 10%
Текст:
ничего не найдено = 0%
Релевантность: 80%
Надеюсь, что теперь вам понятно для чего используются и как получаются эти цифры. Остался последний шаг - все вышеизложенное надо перевести на PHP и в итоге получить запрос к базе MySQL.
Code (PHP) : Убрать нумерацию
- // Вес отдельных слов в заголовке и тексте
- $coeff_title=round((20/count($true_words)),2);
- $coeff_text=round((10/count($true_words)),2);
- // Формируем запрос к базе
- $query = "SELECT *, ";
- // Условия для полного совпадения фразы в заголовке и тексте
- $query .= "( IF (`t_title` LIKE '%".$search."%', 60, 0)";
- $query .= "+ IF (`t_text` LIKE '%".$search."%', 10, 0)";
- // Условия для каждого из слов
- foreach($true_words as $word) {
- $query .= "+ IF (`t_title` LIKE '%".$word."%', ".$coeff_title.", 0)";
- $query .= "+ IF (`t_text` LIKE '%".$word."%', ".$coeff_text.", 0)";
- }
- $query.=") AS `relevant` FROM `articles`";
- // Условие выборки - вхождение фразы или отдельных слов в заголовок или текст
- $query .= " WHERE (";
- $query .= " `t_title` LIKE '%".$search."%' OR `t_text` LIKE '%".$search."%'";
- // Условия для каждого из слов
- foreach($true_words as $word) {
- $query .= " OR `t_title` LIKE '%".$word."%'";
- $query .= " OR `t_text` LIKE '%".$word."%'";
- }
- $query .= ") ORDER BY `relevant` DESC";
SELECT *,
(IF (`t_title` LIKE '%мама мыла раму%', 60, 0)
+ IF (`t_text` LIKE '%мама мыла раму%', 10, 0)
+ IF (`t_title` LIKE '%мама%', 6.67, 0)
+ IF (`t_text` LIKE '%мама%', 3.33, 0)
+ IF (`t_title` LIKE '%мыла%', 6.67, 0)
+ IF (`t_text` LIKE '%мыла%', 3.33, 0)
+ IF (`t_title` LIKE '%раму%', 6.67, 0)
+ IF (`t_text` LIKE '%раму%', 3.33, 0)) AS `relevant`
FROM `articles`
WHERE
(`t_title` LIKE '%мама мыла раму%'
OR `t_text` LIKE '%мама мыла раму%'
OR `t_title` LIKE '%мама%'
OR `t_text` LIKE '%мама%'
OR `t_title` LIKE '%мыла%'
OR `t_text` LIKE '%мыла%'
OR `t_title` LIKE '%раму%'
OR `t_text` LIKE '%раму%')
ORDER BY `relevant` DESCа для "история успеха" вот такой:
SELECT *,
(IF (`t_title` LIKE '%история успеха%', 60, 0)
+ IF (`t_text` LIKE '%история успеха%', 10, 0)
+ IF (`t_title` LIKE '%история%', 10, 0)
+ IF (`t_text` LIKE '%история%', 5, 0)
+ IF (`t_title` LIKE '%успеха%', 10, 0)
+ IF (`t_text` LIKE '%успеха%', 5, 0)) AS `relevant`
FROM `articles`
WHERE
(`t_title` LIKE '%история успеха%'
OR `t_text` LIKE '%история успеха%'
OR `t_title` LIKE '%история%'
OR `t_text` LIKE '%история%'
OR `t_title` LIKE '%успеха%'
OR `t_text` LIKE '%успеха%')
ORDER BY `relevant` DESCВыглядит страшновато, но такие запросы обрабатываются сравнительно быстро и их можно кэшировать. Конечно, для огромных объемов информации лучше поискать другие варианты поиска, но для небольших сайтов такое решение будет вполне уместно. Например, у меня на этом блоге используется очень похожий алгоритм поиска и скорость его работы меня полностью устраивает.
Просмотров: 2664 | Комментариев: 4
Комментарии
Отзывы посетителей сайта о статье
Nashev
(10.01.2012 в 18:19):
Респект!
Квази
(14.11.2011 в 14:47):
Спасибо за развёрнутый ответ!
ManHunter
(14.11.2011 в 11:23):
Не надо так категорично заявлять. FULLTEXT и MATCH-AGAINST - это хорошо и здорово, но они работают только на таблицах MyISAM, не говоря о том, что в базе придется еще хранить эти FULLTEXT-индексы.
Дальше открываем мануал MySQL: "Релевантность вычисляется на основе количества слов в данной строке столбца, количества уникальных слов в этой строке, общего количества слов в тексте и числа документов (строк), содержащих отдельное слово." Что это значит? Это значит, что короткая фраза в огромном тексте будет иметь крохотную релевантность, хотя фактически это не так.
И еще там же в мануале: "Описанная техника подсчета лучше всего работает для больших наборов текстов (фактически она именно для этого тщательно настраивалась). Для очень малых таблиц распределение слов не отражает адекватно их смысловое значение, и данная модель иногда может выдавать некорректные результаты." Для моего блога критично, чтобы поиск был максимально точный, и описанный способ мне подходит больше всего, к тому же в этой реализации я могу самостоятельно определять вес релевантности заголовка и текста.
Ну и наконец, если искомое слово встречается более чем в 50% статей, то оно вообще будет проигнорировано. Для огромной базы это, возможно, правильное поведение, но мне опять же нужна максимальная точность.
Поэтому для каждой задачи должно быть свое решение, а не закидоны "Только ZZZ - это хорошо, а все остальное хуйня".
Дальше открываем мануал MySQL: "Релевантность вычисляется на основе количества слов в данной строке столбца, количества уникальных слов в этой строке, общего количества слов в тексте и числа документов (строк), содержащих отдельное слово." Что это значит? Это значит, что короткая фраза в огромном тексте будет иметь крохотную релевантность, хотя фактически это не так.
И еще там же в мануале: "Описанная техника подсчета лучше всего работает для больших наборов текстов (фактически она именно для этого тщательно настраивалась). Для очень малых таблиц распределение слов не отражает адекватно их смысловое значение, и данная модель иногда может выдавать некорректные результаты." Для моего блога критично, чтобы поиск был максимально точный, и описанный способ мне подходит больше всего, к тому же в этой реализации я могу самостоятельно определять вес релевантности заголовка и текста.
Ну и наконец, если искомое слово встречается более чем в 50% статей, то оно вообще будет проигнорировано. Для огромной базы это, возможно, правильное поведение, но мне опять же нужна максимальная точность.
Поэтому для каждой задачи должно быть свое решение, а не закидоны "Только ZZZ - это хорошо, а все остальное хуйня".
Квази
(14.11.2011 в 10:40):
Вообще-то поиск с использованием LIKE - это не есть хорошо! Нужно использовать полнотекстовый поиск, вот это вариант.
Добавить комментарий
Заполните форму для добавления комментария
