PHP

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



Я уже писал о возможностях поиска с учетом морфологии, а теперь обещанная статья о релевантном поиске по базе MySQL. Как разъясняют словари, релевантность - в поисковых системах - мера соответствия результатов поиска задаче поставленной в запросе. То есть чем ближе найденный результат соответствует искомому, тем выше в результатах поиска он должен находиться. Применительно к выборке из базы, в строках результата релевантность должна быть представлена неким числовым значением, по которому эта выборка должна быть отсортирована.

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

Теперь попробуем это реализовать описанный выше алгоритм на PHP и MySQL. На первом шаге надо разобрать поисковую строку на отдельные слова, при этом надо будет отбросить слова, короче трех символов. Если вы внимательно читали комментарии к статье о морфологическом поиске, то наверняка увидели там полезный совет по укорачиванию длинных слов. Его я тоже задействую.
// Разобрать искомую строку $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);

Теперь надо распределить весовые коэффициенты между полной фразой и отдельными словами, а также между заголовком и текстом. Значимость заголовка и текста я установил как пропорцию 70/30, мне кажется это наиболее оптимальное соотношение. Для полной фразы в заголовке и тексте я выбрал соотношение 60/10, а для отдельных слов 20/10. Последние коэффициенты 20 и 10 будут еще дополнительно распределяться на каждое входящее слово. Пока не очень понятно, для чего это все сделано, поэтому поясню на примере. Предположим, у нас есть текст:

Заголовок: Мама мыла раму или история успеха

Текст: Однажды мама мыла раму и заработала на этом кучу денег. Теперь у мамы своя фирма по мытью окон.

Как я уже говорил, максимальное значение релевантности будет у текстов, которые содержат искомую фразу целиком, поэтому возьмем фразу "мама мыла раму". Считаем релевантность текста:

Заголовок:
"мама мыла раму" = 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.
// Вес отдельных слов в заголовке и тексте
$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


Выглядит страшновато, но такие запросы обрабатываются сравнительно быстро и их можно кэшировать. Конечно, для огромных объемов информации лучше поискать другие варианты поиска, но для небольших сайтов такое решение будет вполне уместно. Например, у меня на этом блоге используется очень похожий алгоритм поиска и скорость его работы меня полностью устраивает.
Добавлено: 13 Сентября 2013 12:53:29 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...