Поиск с учетом релевантности

215
15 декабря 2016, 15:59

Хотелось бы самому написать небольшой скрипт поиска по БД. Все это хорошо реализовать с помощью LIKE %abc%, но как быть с релевантностью? Чтобы полное совпадение было на первом месте, все остальные - на втором. Совет нужен в одном - для этого используется 2 запроса ? Один с точным значением, второй - с LIKE? Или можно как-то все это объединить в 1 запрос?

Answer 1

Для таких целей, лучше бы использовать что-то вроде Sphinx-а, вместо LIKE - MATCH AGAINST (зависит от определенных условий). Да и статей по этой теме уже достаточно. Вот, например, "Релевантный поиск на php"

Answer 2

Я такой велик делал. Ход работы:

  1. Подготовить таблицу к поиску.
  2. Преобразовать поисковую фразу в слова/морфемы.
  3. Найти все совпадения, то есть, даже совпадения по одному слову из фразы.
  4. Отсортировать по релевантности.

Под «подготовить таблицу к поиску» я подразумеваю вынос всего контента, по которому мы будем искать, в отдельное поле и правильную расстановку индексов. К примеру, у записи есть заголовок и содержание. Новый столбец обзовем searchable. Значит, в новом поле будет храниться CONCAT_WS(" ", "заголовок", "содержание"):

`title` : "Трое в лодке, не считая собаки"
`content` : "Представляет собой отчет о лодочной поездке по реке Темзе между Кингстоном и Оксфордом..."
->
`searchable` : "Трое в лодке, не считая собаки Представляет собой отчет о лодочной поездке по реке Темзе между Кингстоном и Оксфордом..."

Это нужно для оптимизации процесса сортировки.

Преобразование поисковой фразы в слова я делал таким образом, чтобы предлоги и частицы (1-3 буквы; в зависимости от языка) сливались со следующим словом. Это, на мой взгляд, лучший вариант, так как в тексте предлоги и частицы в основном относятся с последующему за ними слову. Но и проблема тоже есть: союз «и», к примеру, нельзя отнести ни к предыдущему, ни к последующему слову. Поэтому, я вырезал все «пустые» слова из поисковой фразы (для каждого языка). Ну и, конечно, знаки препинания — тоже вырезал. Получал, такие «слова»: "Трое в лодке, не считая собаки" -> "Трое | в лодке | не считая | собаки". Или "mysql search in table" -> "mysql | search | in table".

Еще очень важно для нормального поиска — знать, какая часть слова не имеет важности: это приставка и окончание. Суффикс тоже можно убрать. То есть, для нормального-ala-гугол-поиска желательно брать только корни слов. Для этого понадобится какой-то скрипт, который бы смог выдергивать морфемы. И это, хотя и достаточно сложно, но гуглится.

Искать проще всего:

<?
$words = search_query_get_words('Трое в лодке, не считая собаки');
// $words : array('Трое', 'в лодке', 'не считая', 'собаки');
$query = 'SELECT * FROM `table` WHERE ';
$query .= '`searchable` LIKE "%' . implode('%" OR `searchable` LIKE "%', $words) . '%"';
?>

Ищем по одному только полю — сгенерированному ранее <code>searchable</code>.

Хитрость заключается в том что, если уже предусмотрен морфологический разбор слов, то колонка searchable должна хранить не тупую склейку заголовка и содержания, а только уникальные корни слов из полей title и content — это сэкономит и объем физической памяти, занимаемой таблицей, и, что важнее — скорость выполнения запроса.

Морфологический разбор слов текста нужно будет производить значительно реже — при редактировании или добавлении новой статьи. Это значительно проще, чем при каждом поиске пытаться адаптировать каждое запрошенное слово с учетом всех склонений-падежей.

Самое главное — правильно отсортировать. Я прикинул, что самым важным в определении того, насколько результат соответствует запросу, является расстояние в символах между найденными словами. Но это оказалось и самым сложным, поэтому я опустил это. Вторым по важности я считаю количество слов поисковой строки, присутствующих в записи. А вот количество вхождений одного и того же слова в записи — наименее значимо.

Кроме того, очень важно, где именно найдены слова: в заголовке или в содержании.

Итак, есть 2 фактора:

  1. количество слов;
  2. количество вхождений одного и того же слова.

Получить количество несложно:

<?
$words_in_field = '';
foreach ($words as $word)
    $words_in_field .= ' + IF(`searchable` LIKE "%'.$word.'%", 1, 0)';
$words_in_field = substr($words_in_field, 3);
$query = 'SELECT * FROM `table` WHERE '. $words_in_field .' > 0 ORDER BY '. $words_in_field. ' DESC';
?>

Получить количество вхождений можно вот как:

mysql> SELECT (LENGTH("Шел-шел, да упал") - LENGTH(REPLACE(LOWER("Шел-шел, да упал"), "шел", ""))) / LENGTH("шел")

Итак, есть результаты по двум «факторам» и есть 2 «настоящих» колонки title и content. Нужно определиться, какое поле важнее. Я считаю, что заголовок — самое важное. Предположим, у нас есть 2 записи (статьи). При запросе, состоящем из 4 слов найдется:

  1. Первая статья: в заголовке — 2 слова; в теле — 4 слова
  2. Вторая статья: в заголовке — 3 слова; в теле — 2 слова

В этом случае в начале выдачи нужно показать «Вторую статью», а ниже — «Первую статью». Значит, колонкам нужно указать веса. Пусть это будут 1 и 0.5 для title и content соответственно. При подсчете индекса соответствия нужно умножать количество найденных слов на вес данной колонки.

Конечно, этим не обойтись. Нужно предусмотреть и упростить процесс поиска одного-единственного слова. Рано или поздно встанет вопрос о том, как же сортировать одинаковые по релевантности результаты (ага: PR, тИЦ) или как сделать так, чтобы таких ситуаций не возникало...

Конечно, для огромного объема данных этот вариант очень слаб. Но для внутреннего поиска на средненьком сайте этого вполне достаточно.

  • Пример таблицы
  • Генерировать SQL-запрос для поисковой фразы
  • Исходник
READ ALSO
Объединение запросов UPDATE

Объединение запросов UPDATE

Мне нужно объединить два sql update запроса в один

157
Составить запрос для таблиц со связью many-to-one

Составить запрос для таблиц со связью many-to-one

Здравствуйте! Помогите, пожалуйста, с запросомЕсть таблица Orders и Locations, между ними связь many-to-one

176
C#  и Mysql

C# и Mysql

Подскажите как работать в C# с MysqlЧто подключать, как подключатся к mysql и как искать в бд

140