Поиск пустых папок. Идеи и реализации

function RemoveEmptySubFolders($path)
{
  $empty=true;
  foreach (glob($path.DIRECTORY_SEPARATOR."*") as $file)
  {
     if (is_dir($file))
     {
        if (!RemoveEmptySubFolders($file)) $empty=false;
     }
     else
     {
        $empty=false;
     }
  }
  if ($empty) rmdir($path);
  return $empty;
}

Добавлено: 21 Октября 2021 06:55:44 Добавил: Андрей Ковальчук

Сканер FTP или поиск по FTP-серверу

Часто требуется организовать поиск по FTP-серверу. Для того чтобы поиск осуществлялся быстро, FTP-сервер сканируют и помещают названия всех файлов и пути к ним в один файл или таблицу базы данных. При помощи такого списка осуществлять поиск гораздо проще, нежели сканировать каждый раз весь хост. С другой стороны вам может быть интересно будет просканировать свой собственный FTP-хост чтобы получить представление о его организации. Это позволит оптимизировать или реорганизовать его структуру.

Создадим небольшое Web-приложение, состоящее из двух файлов: config.php (конфигурационный файл, содержащий параметры соединения с FTP-сервером и осуществляющий установку соединения) и index.php, который представляет собой непосредственно FTP-сканер.

Конфигурационный файл config.php

<?php
// Адрес FTP-сервера 
  $ftp_server = "ftp.server.ru"; 
  // Порт соединения 
  $ftp_port = 21; 
  // Пользователь 
  $ftp_user = "softtime"; 
  // Пароль 
  $ftp_password = ""; 
  // Версия Web-приложение 
  $version = "1.0.0"; 
  // Устанавливаем время исполнения скрипта 120 с 
  set_time_limit(120); 
  // Пытаемся установить соединение с FTP-сервером 
  $link = ftp_connect($ftp_server); 
  if(!$link) puterror("К сожалению, не удаётся установить соединение с FTP-сервером $ftp_server"); 
  // Осуществляем регистрацию на сервере 
  $login = ftp_login($link, $ftp_user, $ftp_password); 
  //$login = ftp_login($conn_id, $ftp_user_name, $ftp_user_pass); 
  if(!$login) puterror("К сожалению, не удаётся зарегистрироваться на сервере"); 
  // Небольшая вспомогательная функция, которая выводит сообщение об ошибке 
  // в окно браузера 
  function puterror($message) 
  { 
    echo "<center><p class=help>$message</p>"; 
    exit(); 
  } 
?>

Теперь, когда соединение установлено, можно осущестить рекурсивный спуск, по дереву директорий FTP-каталога(index.php):
<?php
// Устанавливаем соединение с FTP-сервером 
  require_once("config.php"); 
  // Директория на сервере 
  // $dir = "/html/forum/"; 
  $dir = "/"; 
  // Запускаем сканер 
  scan_ftp($link, $dir); 
  // Закрываем соединение с FTP-сервером 
  ftp_close($link); 
  // Результат находится в глобальном массиве $filename 
  echo "<pre>"; 
  print_r($filename); 
  echo "</pre>
<div id='r7'>
</div>
"; 
  //////////////////////////////////////////////////////// 
  // Рекурсивная функция спуска по дереву 
  // директорий 
  //////////////////////////////////////////////////////// 
  function scan_ftp($link, $dir) 
  { 
    GLOBAL $filename; 
    // Получаем все файлы корневого каталога 
    // Дескриптор соединения $link получен в config.php 
    $file_list = ftp_rawlist($link, $dir); 
    // Выводим содержимое каталога 
    foreach($file_list as $file) 
    { 
      // Разбиваем строку по пробельным символам 
      list($acc, 
           $bloks, 
           $group, 
           $user, 
           $size, 
           $month, 
           $day, 
           $year, 
           $file) = preg_split("/[\s]+/", $file); 
      // Если файл начинается с точки - игнорируем его 
      if(substr($file, 0, 1) == '.') continue; 
      // Определяем является ли объект директорией 
      if(substr($acc, 0, 1) == 'd') 
      { 
        // Директория 
        scan_ftp($link, $dir.$file."/"); 
      } 
      // Определяем является ли объект файлом 
      if(substr($acc, 0, 1) == '-') 
      { 
        // Файл 
        $filename[] = $file." - ".$dir.$file; 
      } 
    } 
  } 
?>

Результат помещается в глобальный массив $filename - поместить содержимое которого в файл или таблицу MySQL не составит труда.

Добавлено: 29 Мая 2018 21:46:44 Добавил: Андрей Ковальчук

Собственная статистика поисковых слов (Яндекс, Рамблер, Google,...) на PHP

В этой статья я бы хотел поделиться с вами своей новой разработкой – анализатором поисковых запросов с популярных поисковых систем, посмотреть, что это такое, и как работает можно здесь (http://wm-help.net/?module=word-stat). Поначалу данный анализатор поисковых запросов я писал исключительно для своих нужд, но по просьбам пользователей уже успевших посетить данную страницу решил написать статью и дать исходник PHP-класса, на котором основывается вся система статистики поисковых запросов из поисковых систем.

И так, основным источником информации служит переменная $HTTP_REFERER, в которой содержится ссылка на страницу, с которой пришли к вам на сайт. А так же переменная $_SERVER['REQUEST_URI'], в которой указан полный путь к текущей странице. Благодаря этим данным и работает данный класс, из $HTTP_REFERER мы получаем поисковый запрос, страницу результатов поиска и поисковый сервер. А из $_SERVER['REQUEST_URI'] точную ссылку на то, куда попал посетитель по данному запросу.

Основная сложность в разработке и реализации данного PHP-класса заключается в том, что каждый поисковый сервер использует свои названия для переменных, в которых содержатся все сведения о запросе пользователя (текст запроса, страница в результатах выдачи, другие параметры). Поэтому есть единственно правильное решение – писать для каждой поисковой системы свой анализатор поисковых запросов, отсюда и сложности – в мире существует более 50 популярных поисковых систем, а сколько их существует вообще – никто не знает.
Еще одной проблемой является невозможность получения точной страницы результатов поиска, т.к. количество ссылок в выдаче поисковой системы пользователь может определять сам, поэтому можно узнать только приблизительную страницу результатов поиска.
К проблемам можно еще отнести особенности некоторых поисковых систем, например Google и MSN – используют у себя UTF-8, а Яндекс – KOI8-R (при поиске не на первой странице результатов).

Несмотря на все сложности, которые описаны выше, у меня получился класс для более-менее корректного анализа поисковых запросов для следующих поисковых систем:

* Яндекс (http://yandex.ru/)
* Рамблер (http://rambler.ru/)
* Mail.Ru (http://go.mail.ru/)
* Апорт (http://aport.ru/)
* Google – со всех доменов (http://google.ru/)
* Yahoo (http://yahoo.com/)
* MSN (http://search.msn.com/)
* Aol

PHP-класс SearchWordимеет следующую структуру:

class SearchWord
{
    var $SearchSite; // Адрес поисковика
    var $SearchWord; // Поисковый запрос (фраза по которой вас нашли)
    var $SearchRefer; // Ссылка на страницу, откуда пришли
    var $SearchRPage; // Ссылка на страницу куда попали
    var $SearchPage; // Номер страницы в результатах выдачи (примерно)
    var $SearchTime; // Время обращения к вашему сайту
 
function Yandex_str($str) // анализатор для Яндекса
{…}
function Rambler_str($str) // для Рамблера
{…}
… // функции анализа других поисковиков
function Msn_str($str) // для MSN
{…}
 
function SearchWords($SRefer, $SPage, $STime) // производит анализ
{…}
 
function SearchWordsFromClass($SSW) // производит анализ, получая все данные из класса SaveSearchWord (смотрите ниже)
{…}
 
function Load($arr) // загружает сохраненный результат анализа из массива (для ведения статистики)
{…}
}
Класс SaveSearchWord – для хранения запросов к сайту:

class SaveSearchWord
{
    var $SRefer; // Откуда пришли
    var $SPage; // Куда попали
    var $STime; // Время обращения к вашему сайту
 
function Load($arr) // загрузка значений из массива
{
$this->SRefer     = trim(@$arr[0]);
$this->SPage      = trim("http://".$_SERVER['HTTP_HOST'].trim(@$arr[1]));
$this->STime      = trim(@$arr[2]);
}
 
function Set($SRefer, $SPage, $STime) // установка значений
{
$this->SRefer     = trim($SRefer);
$this->SPage      = trim($SPage);
$this->STime      = trim($STime);
}
}

Для того, что бы понять, как пользоваться классом SearchWord создадим небольшой PHP-скрипт, в котором реализуем базовые возможности данного класса:
<?php
include 'word-stat.class.php';
if (isset($HTTP_REFERER)) // установлена ли переменная $HTTP_REFERER
{
    $new_record = new SaveSearchWord(); // Создание нового экземпляра класса SaveSearchWord
    $new_record->Set($HTTP_REFERER, $_SERVER['REQUEST_URI'], time()); // Установка значений
    $word_stat = new SearchWord(); // Создание нового экземпляра класса SearchWord
    $word_stat->SearchWordsFromClass($new_record); // Анализ значений
    if (strlen($word_stat->SearchWord)>2) // Если длинна поискового запроса больше двух
    {
        echo "<pre>";
        print_r($word_stat); // Пачать всех значений объекта
        echo "</pre>";
    }
}
?>

Скачать PHP-класс данного анализатора поисковых запросов можно по ссылке: http://wm-help.net/download/word-stat.class.php.zip
Посмотреть, как работает статистика, основанная на данном классе: http://wm-help.net/?module=word-stat

Добавлено: 29 Мая 2018 18:36:05 Добавил: Андрей Ковальчук

Поиск подстроки в строке

Очень часто есть необходимость проверить наличие одной строки в другой.Для этих целей можно использовать регулярные выражения, но если у вас с ними туго-воспользуйтесь простой php функцией strpos.
Эта функция найдет искомую подстроку в строке и даже вернет её позицию.
Рассмотрим на примере использование этой функции,это совсем просто.

$main_str='Это строка, в которой необходимо найти подстроку.';
 
//искомый текст
$my_str='найти';
 
$pos = strpos($main_str, $my_str);
if ($pos === false) {
	echo 'Подстрока не найдена';
}else{
	echo 'Подстрока найдена в позиции: '.$pos;
}

Очень удобно использовать эту функцию для проверки правильности URL. Например вы делаете сервис,в котором клиент должен указать адрес своей странички во вконтакте.
Тогда проверка будет такой:
$main_str='http://vk.com';
 
$my_str='то что ввёл пользователь';
 
$pos = strpos($main_str, $my_str);
if ($pos === false) {
	echo 'Некорректно указан адрес страницы';
}else{
	echo 'Всё верно,ваша ссылка принята';
}

Добавлено: 29 Мая 2018 09:04:30 Добавил: Андрей Ковальчук

Наконец-то заработал поиск

Вот я и сделал наконец поиск по сайту, вообще еще много чего надо сделать, а то я даже страницы добавляю через MySQL. Сейчас поиск ищет по заголовку, описанию и собственно телу страницы, самое приятное в нем то, что в нем есть ранжирование результатов. Если кому-то интересно как такое сделать то читайте дальше.

MATCH() … AGAINST() первое, что приходит в голову, когда говорят о поиске с релевантностью, забудьте об этом бреде!!! Во-первых, это накладывает очень два ограничений: первое и главное — надо делать полнотекстовые индексы, без них никак, второе — индексы можно сделать только на таблицах MyISAM. Во вторых это просто глючно, потому что при поиске небольших фраз по большой базе их релевантность близка к абсолютному нулю, или же если слово присутствует в более 50% строк, то оно не учитывается.

SELECT 
MATCH('Content') AGAINST ('keyword1 keyword2') as Relevance 
FROM table 
WHERE MATCH ('Content') AGAINST('+keyword1 +keyword2' IN BOOLEAN MODE) 
HAVING Relevance > 0.2 
ORDER BY Relevance DESC

Это так сказать классический способ поиска, взятый с сайта самого мускула, но ИМХО это большая ошибка, тут первый MATCH выдает релевантность, а второй который IN BOOLEAN MODE используется для поиска обоих слов сразу в одной строке, если есть только одно то он выдаст 0. Если слов больше двух то это начинает давать сильную погрешность, выдавая только те ряды, где есть все слова фразы. Более того, куча MATCH сильно тормозит запрос.

Поэтому лучше ввести весовые коэффициенты в запрос. Коэффициенты вы раздаете сами, поэтому их общее число всегда может быть в пределах 100 и к нему можно смело добавлять знак %. Итак у нас есть три колонки title, description и text. Если в каждом из трех полей есть искомое словосочетание то релевантность 100%, при этом каждое поле имеет свой вес: заголовок — самый большой(40%), потом описание(20%) а потом уже текст(10%). Я считаю, что полное словосочетание это 70% релевантности и если есть все слова из него, то это еще 30%. немного запутал, сейчас объясню. Есть словосочетание «мама мыла раму», если оно дословно есть во всех полях то это 70%, а если в каждом из них есть «раму мыла мама», то это только 30%. В результате мы получаем либо 100% (70% все вместе и 30% каждое по отдельности) совпадение со строкой либо только 30% (каждое по отдельности) совпадение, ну это лично мое мнение.

Итак алгоритм будет выгладить следующим образом:
$text = "мама мыла раму";
$query = "SELECT *, 
IF (title like '%".$text."%', 40, 0) + IF (title LIKE '%".str_replace(" ", "%', 5.71, 0) + IF (title LIKE '%", $text)."%', 5.71, 0) + 
IF (description like '%".$text."%', 20, 0) + IF (description LIKE '%".str_replace(" ", "%', 2.86, 0) + IF (description LIKE '%", $text)."%', 2.86, 0) +
IF (text like '%".$text."%', 10, 0) + IF (text LIKE '%".str_replace(" ", "%', 1.43, 0) + IF (text LIKE '%", $text)."%', 1.43, 0) AS rel
FROM pages 
WHERE 
(title LIKE '%".str_replace(" ", "%' OR title LIKE '%", $text)."%') OR
(description LIKE '%".str_replace(" ", "%' OR description LIKE '%", $text)."%') OR
(text LIKE '%".str_replace(" ", "%' OR text LIKE '%", $text)."%') 
ORDER BY rel DESC";

Если подставить значения будет выглядеть менее запутано но все равно непонятно
SELECT *, 
IF (title LIKE '%мама мыла раму%', 40, 0) + IF (title LIKE '%мама%', 5.71, 0) + IF (title LIKE '%мыла%', 5.71, 0) + IF (title LIKE '%раму%', 5.71, 0)  + 
IF (description LIKE '%мама мыла раму%', 20, 0) + IF (description LIKE '%мама%', 2.86, 0) + IF (description LIKE '%мыла%', 2.86, 0) + IF (description LIKE '%раму%', 2.86, 0)  + 
IF (text LIKE '%мама мыла раму%', 10, 0) + IF (text LIKE '%мама%', 1.43, 0) + IF (text LIKE '%мыла%', 1.43, 0) + IF (text LIKE '%раму%', 1.43, 0) AS rel
FROM pages 
WHERE 
(title LIKE '%мама%' OR title LIKE '%мыла%' OR title LIKE '%раму%') OR
(description LIKE '%мама%' OR description LIKE '%мыла%' OR description LIKE '%раму%') OR
(text LIKE '%мама%' OR text LIKE '%мыла%' OR text LIKE '%раму%')
ORDER BY rel DESC

Вот такой немаленький запросик получился))) Теперь попробуем разобрать что значат все эти непонятные коэффициенты 5.71, 2.86 и 1.43. Для этого я нарисую маленькую табличку.
\	Все	мама	мыла	раму
title	40	5.71	5.71	5.71
description	20	2.86	2.86	2.86
text	10	1.43	1.43	1.43

Сумма первого столбика получается как раз 70%, сумма всех остальных ячеек 30% в результате искомые 100% релевантности. Сумма каждого следующего столбика 10%. А сумма каждой ячейки высчитывается по формуле (вес поля) * (общее число процентов 30) / (общее число коэффициентов 7) / (количество слов), если подставить поле title то выйдет 4*30/7/3 = 5.71. В общем, я думаю, поняли, кто не понял, возьмите калькулятор!

В том что такой запрос отрабатывает весьма шустро вы можете перейдя на страницу поиска, там выводиться время потраченное на запрос.

Кстати в моем поиске еще наложена логика отключения полей, если отключено одно поле то коэффициенты становятся 3 и 4 , а если отключено два поля то оставшееся имеет коэффициент 7. И еще не забудьте удалить пробелы в начале и конце строки, а также двойные пробелы в середине. Все!

Добавлено: 25 Мая 2018 10:00:51 Добавил: Андрей Ковальчук

Пишем поиск по сайту на PHP + MySQL

Сегодня мы напишем собственный поиск по сайту с использованием PHP и MySQL. Первым делом рассмотрим краткий алгоритм.

Пользователь выполняет POST запрос из формы поиска, этот запрос передается специальному скрипту-обработчику, который должен обработать поисковый запрос пользователя и возвратить результат.

Сначала скрипт должен обработать должным образом запрос пользователя для обеспечения безопасности, затем выполняется запрос к базе данных, который возвращает в ассоциативном массиве результаты, которые должны будут выводиться на экран. Итак, приступим.

Для начала создадим форму поиска на нужной нам странице:

<form name="search" method="post" action="search.php"> 
    <input type="aearch" name="query" placeholder="Поиск" > 
    <button type="submit">Найти</button> 
</form>

Эта форма и будет отправлять сам поисковый запрос скрипту search.php. Теперь создадим сам скрипт-обработчик.
<?php 
function search ($query) 
{ 
    $query = trim($query); 
    $query = htmlspecialchars($query); 
    $query = mysql_real_escape_string($query); 
    $query = stripslashes($query); 
    if (!empty($query)) 
    { 
        if (strlen($query) < 3) 
            $text = '<p>Слишком короткий поисковый запрос.</p>
            '; 
        else if (strlen($query) > 128) 
            $text = '<p>Слишком длинный поисковый запрос.</p>
            '; 
        else 
        { 
            $q = "SELECT `page_id`, `title`, `desc`, `title_link`, `category`, `uniq_id` FROM `table_name` WHERE `text` LIKE '%$query%' OR `title` LIKE '%$query%' OR `meta_k` LIKE '%$query%' OR `meta_d` LIKE '%$query%'"; 
            $result = mysql_query($q); 
            if (mysql_affected_rows() > 0) 
            { 
                $row = mysql_fetch_assoc($result); 
                $num = mysql_num_rows($result); 
                $text = '<p>По запросу <b>'.$query.'</b> найдено совпадений: '.$num.'</p>
                '; 
                do 
                { 
                    $q1 = "SELECT `link` FROM `table_name` WHERE `uniq_id` = '$row[page_id]'"; // Делаем запрос, получающий ссылки на статьи. 
                    $result1 = mysql_query($q1); 
                    if (mysql_affected_rows() > 0) 
                        $row1 = mysql_fetch_assoc($result1); 
                    $text .= '
                    <section class="article_description">
                    <p><a> href="'.$row1['link'].'/'.$row['category'].'/'.$row['uniq_id'].'" title="'.$row['title_link'].'">'.$row['title'].'</a></p>
                    <p>'.$row['desc'].'</p>
                    </section>
                    '; 
                } 
                while ($row = mysql_fetch_assoc($result)); 
            } 
            else 
                $text = '<p>По запросу <b>'.$query.'</b> Ничего не найдено.</p>
                '; 
        } 
    } 
    else 
        $text = '<p>Задан пустой поисковый запрос.</p>
        '; 
    return $text; 
} 
?>

Естественно, данные таблиц БД нужно задать собственные. Рассмотрим, что делает эта функция. Первые 4 строчки обрабатывают запрос, чтобы он стал безопасным для базы. Такую обработку нужно делать обязательно, т.к. любая форма на Вашем сайте - это потенциальная уязвимость для злоумышленников. Затем идет проверка, не пустой ли запрос. Если запрос пустой, то возвращаем соответствующее сообщение пользователю. Если запрос не пустой, проверяем его на размер. Если поисковый запрос имеет длину менее 3 или более 128 символов, также выводим соответствующие сообщения пользователю. Иначе, выполняем запрос к базе данных, который делает выборку идентификатора страницы, ее заголовка, описания, описания ссылки, категорию, если она есть и идентификатор самой статьи, в которой найдены совпадения нужных нам полей с поисковым запросом. В данном случае мы делаем сравнение с текстом статьи, ее заголовком, ключевыми словами и описанием. Если ничего не найдено, выводим пользователю сообщение об этом. Если запрос возвратил хотя бы одну запись, выполняем в цикле еще один запрос, который делает выборку из таблицы со страницами ссылку на страницу, на которой находится статья. Если у Вас все статьи на одной странице, вы можете опустить этот шаг. После выполнения запроса при каждой итерации цикла в переменную $text Дозаписываем одну найденную статью. После завершения цикла, возвращаем переменную $text, Которая и будет выводиться на нашей странице пользователю.

Теперь осталось на этой же странице search.php сделать вызов этой функции и вывести ее результат пользователю.
<?php 
if (!empty($_POST['query'])) 
{ 
    $search_result = search ($_POST['query']); 
    echo $search_result; 
} 
?>

Также Вы можете упростить скрипт поиска по Вашему усмотрению. Желательно создать стиль в таблице css для выводимой информации, чтобы выводимая информация смотрелась более красиво и читабельно. Все замечания вопросы по скрипту можете задавать в комментариях.

Добавлено: 12 Мая 2018 07:57:27 Добавил: Андрей Ковальчук

Функция проверки орфографии на PHP

Функция проверки орфографии на PHP (на входе проверяемый текст, на выходе список слов с ошибками):

function spell_check ( $str ){
        $str = stripSlashes($str);
        $tocheck = strtr($str, "\n", ' ');
        $tocheck = escapeShellCmd($tocheck);
	exec("echo $tocheck | /usr/bin/ispell -d russian -l", $warnings);
	sort($warnings);
	$sp_prev = '';
	$sp_errors = '';		
	while (list($sp_key, $sp_val) = each($warnings)) {
           if ($sp_val != $sp_prev) {
              $sp_errors = $sp_errors . "<a
href=\"/vhq/info_spell.php3?spell=" . urlencode($sp_val) . "\"
target=_blank>$sp_val</a>, ";
    	   }
           $sp_prev = $sp_val;
	}
	return $sp_errors;
    }

Добавлено: 11 Мая 2018 11:34:51 Добавил: Андрей Ковальчук

Пишем собственное поисковое ядро. Часть 2.

Продолжаем разработку нашего поискового ядра. В данном уроке мы напишем еще несколько функций и рассмотрим работу с шаблоном.

1. Форма поиска и передаваемые данные

Небольшое отступление сделаем и посмотрим на нашу форму отправки данных:

<form action="search_result.php" method="post">
    <p><input id="search_input" name="search_word" type="text"><input id="search_button" type="submit"></p>
</form>

Как видите тут ничего необычного нет. Мы передаем значение search_word файлу search_result.php. Т.е. у нас в нашей системе добавился файл куда и будут выводиться результаты поиска.

Т.е. в файле search_result.php мы будем получать значение запроса и обрабатывать его примерно таким образом:
if(isset($_REQUEST["search_word"])) {
    $query = trim($_REQUEST["search_word"]); //делаем небольшую чистку, можете добавить еще защиту от различных инъекций и подозрительных переменных, которые могут ввести вам вредные пользователи
    $keywords = explodeQuery($query); //тут наша функция с первой части урока
     
    $sql = "SELECT id, title, keywords, text, category FROM materials WHERE public=1"; //ищем только опубликованные материалы
    $result = mysql_query($sql);
    if ($mysql_num_rows($result)!=0) { //проверяем наличие записей, если нет, то естественно ничего не выведем и прекратим любую работу системы
        while($row = mysql_fetch_assoc($result))
        {
            $materials[$row[id]] = $row; //Формируем массив $materials с найденными материалами
        }
        echo searchResult($materials,$keywords); //выводим наш результат поиска, функцию мы рассмотрим ниже
    }
    else {
        echo "напишем тут какую-нибудь ошибку для пользователя";
    }
}

Как видите ничего сложного тут нет. У нас как видите появилась одна необъявленная функция searchResult(). Ее написанием сейчас мы и займемся.

2. Результат поиска.

Рассмотрим немного схему того, как система будет искать необходимые пользователю материалы:

Составитель схем из меня никудышный, но думаю смысл понятен. Поэтому приступим к написанию функции.
function searchResult($materials, $keywords) {
    foreach ($materials as $material) { //Выше мы сформировали массив $materials который мы теперь выводим разбивая на элементы массива $material
        $title = htmlspecialchars(strip_tags($material[title]));    //Тут мы чистим все значения массива - title, text и keywords от тегов и посторонних символов
        $text = htmlspecialchars(strip_tags($material[text]));      //как вариант можно еще все слова перевести в нижний регистр
        $key = htmlspecialchars(strip_tags($material[keywords]));
        $wordWeight =0; //вес слова запроса приравниваем к 0
        foreach ($keywords as $word) {  //теперь для каждого поискового слова из запроса ищем совпадения в тексте
            $reg = "/(".$word.")/";     //маска поиска для регулярной функции
            /* 
                Автоматически наращиваем вес слова для каждого элемента массива.
                Так же сюда можно включить например поле description если оно у вас есть.
                Оставляем переменную $out, которая выводит значение поиска. Она нам может и не пригодится, но пусть будет, может быть вы найдете ей применение.
            */
            $wordWeight = preg_match_all($reg, $title, $out);   //как вариант можно еще для слов в заголовке вес увеличивать в два раза
            $wordWeight += preg_match_all($reg, $text, $out);   //но это вам понадобиться если вы будете выводить материалы в порядке убывания по релевантности
            $wordWeight += preg_match_all($reg, $key, $out);    //мы же пока этого делать не будем
            $material[relevation] += $wordWeight; //увеличиваем вес всего материала на вес поискового слова
             
            //раскрашиваем найденные слова функцией, которую мы писали в первой части урока
            $title = colorSearchWord($word, $title, "violet");
            $text = colorSearchWord($word, $text, "violet");
            $key = colorSearchWord($word, $key, "violet"); //незнаю зачем ключевые слова окрасил, их ведь не обязательно выводить пользователю :)
        }
        //Теперь ищем те материалы, у которых временный атрибут relevation не равен 0
        if($material[relevation]!=0) {
            //Возвращаем массивы в нормальное состояние с уже обработанными данными
            $material[title] = $title;
            $material[text] = $text;
            $material[keywords] = $key;
            echo simpleToTemplate($material, "search_result"); //новая функция, которая вернет нам шаблон с результатами поиска
        }
        //Иначе просто грохаем весь элемент material за ненадобностью
        else {
            unset($material);
        }
    }
}

Вот такая сложная функция, в которой у нас появилась еще одна новая функция и мы ее сейчас рассмотрим отдельно. Что касательно этой функции то смысл ее прост, она просто проверяет вхождение каждого слова в три атрибута материала и увеличивает временно создаваемый атрибут (его нет в базе данных!) для того, чтобы определить, какие материалы нам подходят.

3. Шаблон и функция его вызова.

Ну вот собственно мы подходим к функции визуального формирования результата запроса на основании шаблона search_result.tpl. Для начала рассмотрим функцию, которая при помощи буфера позволит нам динамически сформировать всю структуру HTML.
function simpleToTemplate($value, $template) {
        ob_start(); // Включаем буферизацию вывода, чтобы шаблон не вывелся в месте вызова функции
        // Подключаем необходимый нам шаблон, который просто ждет наш массив
        include('templates/'.$template.'.tpl');
        return ob_get_clean(); //Возвращаем результат буфера и очищаем его
    }

Функция достаточно небольшая, она передает нашему шаблону массив данных и материала, а тот в свою очередь собирает нам вменяемую HTML верстку, которую мы определим в самом шаблоне result_search.tpl.
<div id="search_result_element<? echo $value[id] ?>">
    <h4><a href='index.php?cat=<?=$value[category] ?>&amp;mat=<?=$value[id] ?>&amp;style=1'><?=$value[title] ?></a></h4>
    <? $text = "<p>".substr($value[text],0, 430)."</span>...</p>"; echo $text; ?>
    <p>Количество совпадений: <?=$value[relevation];?></p>

Вот собственно и готов поиск. В следующих уроках мы подумаем как улучшить наше ядро, рассмотрим новые приемы работы с массивами и улучшим в целом всю систему. Пример работы поиска можно посмотреть например на сайте www.protege-star.com

Спасибо за внимание.

Добавлено: 02 Мая 2018 19:16:25 Добавил: Андрей Ковальчук

Пишем собственное поисковое ядро. Часть 1.

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

Приступим.

1. Структура.
Для начала хочу показать в какой таблице у меня хранятся материалы:

Как видите у меня достаточно много полей, большинство я не стал показывать они предназначены чисто для технических моментов. Нас интересуют по сути первые 4 поля: id, title, keywords, text. Если вы как то по другому храните свои материалы, и у вас например есть еще поле description то необходимо учесть этот момент и соответственно внести необходимые корректировки.

Далее нас интересует какие файлы у нас будут в системе:

Тут тоже ничего сложного, самое поисковое ядро search_core.php и шаблон search_result.tpl. В чем суть? В том, что все отправляемые пользователем данные будут обрабатываться в поисковом ядре и формировать вывод из файла шаблона в нужное нам место на сайте.

Сам же файл search_core.php будет построен у нас на функциях, работе с массивами и регулярных функциях.

2. Особенности русского языка.
Ни для кого не секрет, что русский язык наверное самый сложный из всех существующих в мире, т.к. столько словоформ сколько у нас в языке нет ни в одном языке мира. Почему? Да потому что у нас в языке существуют такие вещи как приставки, окончания, суффиксы и т.д. Что невероятно сильно усложняет для иностранцев, и для нас - программистов в частности, процесс изучения и обучения великого и могучего. Нам придется научить нашу систему хотя бы понимать разные падежи слов.

Определимся с тем какие у нас есть окончания у слов и напишем первую функцию в наше ядро, которая будет отрезать у слов запроса окончания для повышения уровня релевантности:

function dropBackWords($word) { //тут мы обрабатываем одно слово
    $reg = "/(ый|ой|ая|ое|ые|ому|а|о|у|е|ого|ему|и|ство|ых|ох|ия|ий|ь|я|он|ют|ат)$/i"; //данная регулярная функция будет искать совпадения окончаний
    $word = preg_replace($reg,'',$word); //убиваем окончания
    return $word;
}

Как видите в общем пока ничего сложного. Т.е. если пользователь введет например слово: "Песни", то система откинет букву "и" и оставит нам только "Песн", а это согласитесь позволит найти уже такие слова, как песня, песню, песнями и т.д. Т.е. семантика поиска у нас увеличилась.

Обратите внимание на то, что я указал не все существующие окончания их может быть больше, тут вам придется поучить систему. Возможно даже придется несколько переделать регулярную функцию и написать дополнительные правила.

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

Другой необходимостью является то, что в русском языке, ну собственно как и в любом другом, есть масса стоп-слов, которые нам не нужны в поиске и которые встречаются сплошь и рядом в любом тексте. Поэтому нам надо написать еще функцию, которая убивает все стоп-слова.
function stopWords($query) { //тут мы обрабатываем весь поисковый запрос
    $reg = "/\s(под|много|что|когда|где|или|которые|поэтому|все|будем|как)\s/gim"; //данная регулярка отрежет все стоп-слова отбитые пробелами
    $query = preg_replace($reg,'',$query); //убиваем стоп-слова
    return $query;
}

Отлично, еще одна функция ядра готова, где я опять же взял немного слов для примера. Она позволит нам создать более осмысленный поисковый запрос.

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

Как вариант для обеих функций можно формировать массив таких значений и брать уже из него для сравнения, но не будем тут усложнять себе жизнь.

3. Обработка поискового запроса
Теперь нам надо наконец получить все поисковые слова из общего поискового запроса, который задал пользователь:
function explodeQuery($query) {     //функция вызова поисковой строки
    $query = stopWords($query);     //используем написанную нами ранее функцию для удаления стоп-слов
    $words = explode(" ",$query);   //разбиваем поисковый запрос на слова через пробел и заносим все слова в массив
    $i = 0;                         //устанавливаем начало массива в 0, помним что нумерация в массивах начинается с 0
    $keywords = "";                 //создаем пустой массив
    foreach ($words as $word) {     //в цикле для массива words создаем элемент word
        $word = trim($word);       
        if (strlen($word)<6) {       //если слово короче 6 символов то убиваем его
            unset($word);
        }
        else {                      //иначе выполняем следующее
            if (strlen($word)>8) {
                $keywords[$i]=dropBackWords($word); //наша функция чистки окончаний для слов длинее 8 символов и занесение их в созданный нами массив
                $i++;                               //наращиваем значение i для того чтобы перейти к следующему элементу
            }
            else {
                $keywords[$i]=$word;                //если короче 8 символов то просто добавляем в массив
                $i++;
            }
        }
    }
    return $keywords; //возвращаем полученный массив
}

Думаю стоит объяснить почему мы слова короче 6 символов убиваем. Это правило распространяется на кодировку UTF-8, особенность которой в том, что каждый русский символ идет за два. Попробуйте ради интереса вывести на экран длину 1 русского символа и вы будете удивлены тем, что она равна 2. Таким образом если мы пишем короче 6 символов, то для русского это значит короче 3 символов. Вот такой нюанс, который следует помнить. Сейчас наверное каждый сразу вспомнил недавнее возмущение нашего правительства на тему, почему SMS-сообщения в русской раскладке в два раза короче, чем английские.

Таким образом если вывести наш массив то мы получим примерно следующее:
//поисковая фраза: как написать поисковый модуль своими силами
array (
    [0]=>написат,
    [1]=>поисков,
    [2]=>модул,
    [3]=>своим,
    [4]=>силам
)

Уже хорошо и практически то, что нам нужно. С этим мы уже можем спокойно работать и получить больше материалов из базы, чем если бы мы искали по целой фразе.

Напишем еще одну небольшую функцию чисто визуализационную и на этом закончим первую часть урока.
function colorSearchWord($word, $string, $color) {
    $replacement = "<span style='color:".$color."; border-bottom:1px dashed ".$color.";'>".$word."</span>";
    $result = str_replace($word, $replacement, $string);
    return $result;
}

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

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

Добавлено: 02 Мая 2018 19:14:48 Добавил: Андрей Ковальчук

Поиск в MySQL через match() against()

Здравия желаю!

У меня есть поиск который ищет с помощью match() against(). Но проблема в том что он не совсем точно дает результаты. Например если делать поиск «cat» он выводит на первую полосу не кошек, а то что содержит «Category».

Вопрос простой — Как мне сделать LIKE '% cat %' OR tags LIKE '% cat' OR tags LIKE 'cat %' в match() against()

Более подробно:

Теги хранятся в виде «animal cat happy» или "" «inventory categories»

Вот sql на поиск compact disc

SELECT *, MATCH (tags) AGAINST('>>"compact disc" >(+compact* +disc* ) <(compact* disc* )' IN BOOLEAN MODE) as rel FROM icons 
WHERE MATCH (tags) AGAINST('>>"compact disc" >(+compact* +disc* ) <(compact* disc* )' IN BOOLEAN MODE) 
ORDER BY rel DESC


если делать запрос через LIKE приходится делать UNION чтобы распределить данные по релевантности (в случае со словом «cat»)

(SELECT * FROM icons WHERE tags LIKE '% cat %' OR tags LIKE '% cat' OR tags LIKE 'cat %')
UNION
(SELECT * FROM icons WHERE tags LIKE '%cat%')


В этом случае результат выходит получше, но больше нагрузка на сервер если поиск идет по 2м и более словам.

Я пробовал вытащить тэги в отдельную таблицу и создать отношение «many-to-many», но и это загружает сервер больше чем вариант с match against.

Добавлено: 20 Апреля 2018 20:57:42 Добавил: Андрей Ковальчук

Как ускорить работу поисковой системы сайта в несколько раз

Когда ресурс вырастает из объема одной страницы, уже целесообразно, для удобства использования, добавить на сайт поиск. Поисковые механизмы бывают разные. Для блога, типа этого, вам вполне хватит

select * from material where innertext like '%xdan.ru%' or title like '%xdan.ru%' limit 10;
Она отлично работает, на сайтах с небольшой аудиторией и небольшим объемом информации. Но что если сделать то же самое для поиска из таблицы в несколько тысяч записей?

Если вы вспомнили про индексы, то они вам тут не помогут. Тот факт, что поиск ведется по всему полю %, а не с его начала, уже делает использование индексов невозможным. Т.е. запрос выше, тупо перебирает все строки и ищет соответствие. В больших таблицах это может занимать длительное время. Как же быть? Google нам сообщает, что есть такой зверь: полнотекстовый индекс и полнотекстовый поиск.

Но работают они только таблицах на движке MyISAM. В вышедшей ранее статье, мы конвертировали одну медленную таблицу в этот формат. Проделаем с таблице material тоже самое.

ALTER TABLE material ENGINE = MYISAM

А потом добавим пару полнотекстовых индексов

create FULLTEXT index full_index_title on `material`(`title`);
create FULLTEXT index full_index_text on `material`(`innertext`);

Если вы делаете эти запросы в phpmyadmin, то не пугайтесь тому, что она показывает количество записей в индексе равное 1. Это глюк.

Сам запрос поиска тоже изменится

select * from material where
MATCH (title) AGAINST ('*xdan.ru*'  IN BOOLEAN MODE)
or
MATCH (innertext) AGAINST ('*xdan.ru*'  IN BOOLEAN MODE)
limit 10

Такой запрос отработает на ура. И в таблицах с более чем 100000 записей срабатывает за каких-нибудь 0.0062 сек..

Если в вашем запросе используется оператор and то имеет смысл объединить два индекса в один.

create FULLTEXT index full_index_title_text on `material`(`title`,'innertext');

А в выборке делаем

select * from material where
MATCH (title,innertext) AGAINST ('*цукеавы*'  IN BOOLEAN MODE)
limit 10

Такой запрос отработает быстрее.

Назначение звездочек аналогичны %xdan.ru%. Кроме них у полнотекстового поиска есть еще другие операторы (+,- и т.д.) По этому, и во избежание SQL инъекций, всегда делайте экранирование. SQL инъекции головная боль не только web программистов. У программистов мобильных приложений на такие платформы как Android или IOS, бывают схожие проблемы. Так как эти системы в своем нутре, имеют полноценную поддержку SQL запросов и для хранения данных использую некое подобие СУБД. И приложения соответственно тоже бывают разные. Если вы решили скачать карточные игры на андроид, то лучше сделать это из известных источников.

Полнотекстовый поиск, в отличии от LIKE ищет по своему алгоритму и не всегда это простое совпадение. Кроме того, в большинстве случаев на запрос улицы

ленин
Нужно вывести сперва Ленина, а уже потом Проспект Петленина. Как это сделать? Нужно после оператора select добавить сравнение строк и если строки совпадают то возвращать 1 иначе 0. А в запросе сделать order by эту величину desc

select *,IF(LEFT(LOWER(TRIM(addr)),7)="цукеавы",1, 0) as srt from material where
MATCH (title,innertext) AGAINST ('*цукеавы*'  IN BOOLEAN MODE)
ORDER BY srt desc
limit 10

Цифра 7 это длина фразы, которую мы ищем

"цукеавы"
В реальных запросах ее нужно будет динамически вычислять при помощи mb_strlen

Добавлено: 20 Апреля 2018 20:34:54 Добавил: Андрей Ковальчук

Простой и эффективный поиск на PHP и MySQL

Хочу поиск как в гугле! Эту фразу заказчики повторяют словно мантру, когда речь заходит о реализации поиска в рамках разрабатываемого проекта. Давайте быстро и просто определимся с поиском по тексту. Во-первых — будем использовать БД (совершенно неожиданно, остановим свой выбор на MySQL). Во-вторых — никаких LIKE. Несостоятельность LIKE-поиска доказана тысячами лежачих серверов. Доказывать несостоятельность получения всех ста тысяч записей из БД и поиска с помощью strstr() и подобных, надеюсь, нет необходимости.

Остался единственный вариант - полнотекстовый поиск. Что нам необходимо для полнотекстового поиска? Разумеется, полнотекстовый индекс (он называется FULLTEXT). Индексации данного типа подлежат поля VARCHAR и TEXT на движке MyISAM. В индекс может входить сразу несколько полей. Например, тема форумного поста и его текст. Давайте на примере постов и продолжим.

Допустим имеется примитивная таблица, содержащая посты:

 	CREATE TABLE `posts` (
 	     `post_id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
 	     `user_id` BIGINT(10) UNSIGNED NOT NULL,
 	     `time` INT(10) UNSIGNED NOT NULL,
 	     `subject` VARCHAR(500) NOT NULL,
 	     `text` TEXT NOT NULL,
 	      PRIMARY KEY (`post_id`)
 	);

Сейчас в таблице имеется только первичный индекс. На самом деле, при решении реальной задачи мы создали бы ещё несколько индексов для сортировки записей по времени поста, например. Однако, не будем отвлекаться от основной задачи и перейдём к созданию индекса FULLTEXT. Заранее хочу обратить внимание читателя, что вставка данных (INSERT) в таблицу с полнотекстовым индексом будет происходить медленнее. Следовательно, если мы вдруг захотим залить в таблицу много данных, будет эффективнее, если мы перед заливкой прибьём полнотекстовый индекс, а после выполнения жирных инсертов опять добавим.

Итак, добавляем в таблицу полнотекстовый индекс. Для начала, определим по каким полям мы будем в будущем искать. Очевидно, что это поля subject (тема поста) и text (сам текст поста).

Добавляем FULLTEXT-индекс:

 	ALTER TABLE `posts` ADD FULLTEXT (
 	     `subject` ,
 	     `text` 
 	);


Можно было это сделать и непосредственно при создании таблицы:

 	CREATE TABLE `posts` (
 	     `post_id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
 	     `user_id` BIGINT(10) UNSIGNED NOT NULL,
 	     `time` INT(10) UNSIGNED NOT NULL,
 	     `subject` VARCHAR(500) NOT NULL,
 	     `text` TEXT NOT NULL,
 	      PRIMARY KEY (`post_id`),
 	      FULLTEXT KEY (`subject`,`text`)
 	);


Вот и всё, что необходимо выполнить на стороне MySQL. Получили таблицу, в которой поля `subject` VARCHAR и `text` TEXT имеют полнотекстовый индекс. Важно помнить, что по умолчанию FULLTEXT индексирует слова длиной не менее 4-x символов. Для настоящего национального поиска, обычно, необходимо индексировать слова от 3-х символов (причина, я думаю, всем понятна). Для этого нам необходим доступ к файлу конфигурации MySQL (файл my.ini). Ищем в нём параметр ft_min_word_len, который и задаёт минимальное число символов для попадающих в полнотекстовый индекс слов. Устанавливаем значение в 3 и перестраиваем индексы.

В официальной документации предлагается перестраивать полнотекстовые индексы так:

 	REPAIR TABLE `posts` QUICK;


Фактически, уже сейчас можно выполнять запросы на полнотекстовый поиск. Давайте найдём все посты, в теме и/или содержании которых имеется фраза "привет":

 	SELECT * FROM `posts` WHERE MATCH (`subject`, `text`) AGAINST ('привет');


У MySQL своё видение на алгоритм определения релевантности. Так, слова, плотность которых в общем объёме полей записей превышает 50%, игнорируются. Это значит, что если в нашей таблице будет 100 записей, и в поле «subject» каждой записи будет содержаться слово «привет», поиск ни к чему не приведёт. Считается (и не без основания), что слово, которое повторяется в половине поисковых единиц всего поискового объёма, не может нести смысловой нагрузки.

Осталось реализовать простой класс для поиска.

На стороне PHP:

 	/**
 	 * Класс для работы с полнотекстовым поиском.
 	 */
 	final class Search {
 	     private $countStat;
 	     private $resStat;
 	     private $handler;
 	     private $query;
 	     private $offset = 0;
 	     private $count = 1000;
 	     /**
 	      * Конструктор. Получаем экземпляр PDO, имя таблицы и поля.
 	      */
 	     public function __construct(PDO $PDO, $Table, $Fields) {
 	          $this->countStat = $PDO->prepare('SELECT COUNT(*) FROM '.$Table.' WHERE MATCH (`'.implode('`, `', $Fields).'`) AGAINST (:query);');
 	          $this->resStat = $PDO->prepare('SELECT * FROM '.$Table.' WHERE MATCH (`'.implode('`, `', $Fields).'`) AGAINST (:query) LIMIT :offset, :count;');
	     }
 	     /**
 	      * Сеттер объекта-обработчика.
 	      */
 	     public function handler($Handler) {
 	          $this->handler = $Handler;
 	          return $this;
 	     }
 	     /**
 	      * Сеттер строки поискового запроса.
 	      */
	     public function query($Query) {
 	          $this->query = $this->prepQuery($Query);
 	          return $this;
 	     }
 	     /**
 	      * Ограничения выборки.
 	      */
 	     public function limit($Offset, $Count = false) {
 	          if (!$Count) {
 	               $this->offset = 0;
 	               $this->count = $Offset;
 	          }
 	      else {
 	               $this->offset = $Offset;
 	               $this->count = $Count;               
 	          }
 	          return $this;
 	     }
 	     /**
 	      * Возвращает число найденных записей.
 	      */
 	     public function count() {
 	          $this->countStat->bindParam(':query', $this->query, PDO::PARAM_STR);
 	          $this->countStat->execute();
 	          return $this->countStat->fetchColumn();
 	     }
 	     /**
 	      * Возвращает результат поиска, представленный виде, заданном в логике объекта-обработчика.
 	      */
 	     public function exec() {
 	          $this->resStat->bindParam(':offset', $this->offset, PDO::PARAM_INT);
 	          $this->resStat->bindParam(':count', $this->count, PDO::PARAM_INT);
 	          $this->resStat->bindParam(':query', $this->query, PDO::PARAM_STR);
 	          $this->resStat->execute();
 	          $Result = array();
 	          while ($QRow = $this->resStat->fetch(PDO::FETCH_ASSOC)) {
 	               $Result[] = $this->handler->exec($QRow, $this->query);
 	          }
 	          return $Result;
 	     }
 	     /**
 	      * Предварительная подготовка строки поискового запроса.
 	      */
 	     private function prepQuery($Query) {
 	          // Тут мы делаем необходимые перед началом поиска
 	          // действия со строкой запроса. Например, можно намеренно 
 	          // удалять из строки запроса все слова короче 3-х символов, 
 	          // использовать стеммер и тому подобное.
 	          return $Query;
 	     }
 	}


Пользоваться всем этим безобразием мы будем так:

 	// Предположим, что $PDO — уже инициализированный экземпляр класса PDO.
 	// Искать будем в полях subject и text таблицы posts. Получаем экземпляр нашего объекта-поисковика.
 	$Search = new Search($PDO, 'posts', array('subject', 'text'));
 	// Выведем число найденных записей, по запросу "привет".
 	echo $Search->query('привет')->count();


Поиск уже работает, число найденных записей мы получать научились. Но что делать с самими найденными записями? Ведь в зависимости от ситуации необходимо выводить в браузер некоторые поля найденных записей, инициализировать объекты "сущьностей" на основе данных в полях и выполнять ещё много разных действий, предугадать которые практически невозможно. Именно для этого, предусмотрен объект-обработчик:

 	interface SearchHandler {
	     public function exec($Result, $Query);
 	}


Совершенно ни к чему не обязывающий интерфейс, не правда ли? Единственная задача такого объекта — получить fetch-результат каждой найденной записи и, при необходимости, произвести над ним определённые действия.

Получаем массив тем всех постов, которые будут найдены по запросу "mysql":

 	/**
 	 * Обработчик. Он получает массив значений всех полей каждой
 	 * найденной записи и возвращает то, что  * будет сохранено в результирующем массиве.
 	 */
 	final class ShowResults implements SearchHandler {
 	     public function exec($Result, $Query) {
 	          return $Result['subject'];
 	     }
 	}
 	
 	$Handler = new ShowResults();
 	$Search = new Search($PDO, 'posts', array('subject', 'text'));
 	$Result = $Search->query('привет')->handler($Handler)->exec();


В результате, $Result — массив тем всех найденных записей.

Получаем массив всех полей первых 10-и найденных записей.

 	final class ShowResults implements SearchHandler {
 	     public function exec($Result, $Query) {
 	          return $Result;
 	     }
 	}
 	
 	$Handler = new ShowResults();
 	$Search = new Search($PDO, 'posts', array('subject', 'text'));
 	$Result = $Search->query('привет')->handler($Handler)->limit(10)->exec();


Получаем массив полей text первых 10-и найденных записей, выделяя тегами "<strong>" слова, которые содержатся в строке поискового запроса.

 	final class ShowResults implements SearchHandler {
 	     public function exec($Result, $Query) {
 	             $Text = $Result['text'];
 	            $Words = explode(' ', $Query);
 	            foreach ($Words as $Word) {
 	                 $Text = preg_replace('|('.$Word.')|', '<strong>$1</strong>', $Text);
 	            }
 	            return $Text;
 	     }
 	}
 	
 	$Handler = new ShowResults();
 	$Search = new Search($PDO, 'posts', array('subject', 'text'));
 	$Result = $Search->query('привет')->handler($Handler)->limit(10)->exec();


Вот, собственно, и всё, что я хотел бы продемонстрировать в качестве базиса для поисковых решений на PHP и MySQL. Обозначенный подход не представляет из себя нечто совершенно новое и оригинальное. Большинство разработчиков, рано или поздно, приходят к подобным поисковым решениям. Надеюсь, что данная работа поможет некоторым из них сделать это быстрее и проще (если вообще имеются те, кто дочитал до конца). Ещё раз обращаю внимание на то, что приведённый пример — лишь базис, начальный шаблон для разработчиков. Не смотря на то, что он работает даже в текущем своём виде, для использования в действующих коммерческих системах требуется существенная доработка и переосмысление.

Добавлено: 18 Апреля 2018 08:35:58 Добавил: Андрей Ковальчук

Релевантный поиск с морфологией (без индексации)

Часто перед веб-разработчиками встает задача реализации поиска по сайту. Попробуем сконструировать «велосипед», обладающий следующими характеристиками:

Отсутствие индексной таблицы (индексы — тема отдельной статьи);
Работа с кодировкой UTF-8;
Удаление коротких и стоп-слов;
Морфологическое преобразование русских словоформ;
Сортировка результатов с учетом их релевантности;
Постраничная выдача результатов.
Что нужно?
Нам понадобится рабочий веб-сервер с поддержкой PHP. Для морфологического анализа мы будем использовать русскоязычный стеммер Портера. Кроме этого, нам потребуется список стоп-слов и некоторые функции из проекта PHP UTF-8. Поскольку мы договорились, что не будем использовать индексирование, то в базе данных достаточно одной таблицы приблизительно такого вида:

CREATE TABLE `pages` (
   `id` mediumint(8) unsigned AUTO_INCREMENT,
   `title` text NOT NULL,
   `description` text NOT NULL,
   `content` mediumtext NOT NULL,
   `url` varchar(128) NOT NULL,
   PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

Искать страницы будем по их содержимому (content), а в сниппетах выводить название (title) и описание (description). Однако при необходимости вы легко сможете расширить поиск до нескольких полей и даже учитывать это при подсчете релевантности.

Предварительная обработка строки
Сперва инициализируем переменные со строкой запроса, номером текущей страницы и количеством записей, отображаемым на одной странице. Последние две переменные нужны для организации постраничной выдачи результатов с соответствующей навигацией.

$request = !empty($_GET['search']) ? $_GET['search'] : false;
$p = !empty($_GET['p']) ? (int)$_GET['p'] : 0;
$rpp = 10;

Сократим поисковый запрос до 64 символов (такой длины вполне достаточно) и приведем его к нижнему регистру. Для этого воспользуемся функциями из упомянутого выше проекта PHP UTF-8, архив с которым мы распаковали в директорию utf8. Если ваш рабочий сервер поддерживает mbstring, можете воспользоваться аналогичными функциями оттуда.

header('content-type:text/html; charset=utf-8');
require_once 'utf8/utf8.php';
require_once 'utf8/utils/unicode.php';
require_once 'utf8/utils/specials.php';
$q = utf8_substr($request, 0, 64);
$q = utf8_strtolower($q);

Для корректной работы стеммера необходимо заменить буквы «ё» на «е». Далее удаляем из строки все HTML сущности и специальные символы, а идущие подряд пробелы заменяем единичными экземплярами. Таким образом, получаем подготовленную строку из слов, разделенных пробелами.

$q = strtr($q, array('ё' => 'e'));
$q = preg_replace('/&([a-zA-Z0-9]+);/', ' ', $q);
$q = utf8_strip_specials($q, ' ');
$q = trim(preg_replace('/ +/', ' ', $q));

Морфологический анализатор
В русском языке ярко выражена морфологическая изменяемость слов (например, «дерево, деревьями, деревянным»), поэтому в целях улучшения качества поиска имеет смысл проводить морфологический анализ поисковых запросов. Целью такого анализа будет получение из слова его основы, или леммы. Поиск по леммам увеличивает полноту получаемых результатов за счет документов, в которых встречаются другие словоформы с той же основой. Морфологический анализ положительно влияет и на релевантность (точность) поиска, поскольку использование лемм придает гибкости механизму поиска и избавляет пользователя от необходимости указывать словоформу в нужном падеже и т. д.

Есть два основных подхода к морфологическому анализу. Первый и наиболее точный — применение алгоритмов с использованием словарей. В качестве примеров можно привести Ispell и AOT. Минусом такого подхода является более высокая нагрузка на сервер, поскольку объем словарей может достигать десятков мегабайт.

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

В качестве стеммера мы воспользуемся реализацией алгоритма Портера на PHP. Однако вы можете использовать пакет PECL::stem или любой другой по своему усмотрению. Стеммер Портера выглядит таким образом:

define('CHAR_LENGTH', 2);
function stem($word){
   $a = rv($word);
   return $a[0].step4(step3(step2(step1($a[1]))));
}

function rv($word){
   $vowels = array('а','е','и','о','у','ы','э','ю','я');
   $flag = 0;
   $rv = $start='';
   for ($i=0; $i<strlen($word); $i+=CHAR_LENGTH){
      if ($flag == 1) $rv .= substr($word, $i, CHAR_LENGTH); else $start .= substr($word, $i, CHAR_LENGTH);
      if (array_search(substr($word,$i,CHAR_LENGTH), $vowels) !== FALSE) $flag = 1;
   }
   return array($start,$rv);
}

function step1($word){
   $perfective1 = array('в', 'вши', 'вшись');
   foreach ($perfective1 as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix && (substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'а' || substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'я')) 
         return substr($word, 0, strlen($word)-strlen($suffix));
   $perfective2 = array('ив','ивши','ившись','ывши','ывшись');
   foreach ($perfective2 as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix) 
         return substr($word, 0, strlen($word) - strlen($suffix));
   $reflexive = array('ся', 'сь');
   foreach ($reflexive as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix) 
         $word = substr($word, 0, strlen($word) - strlen($suffix));
   $adjective = array('ее','ие','ые','ое','ими','ыми','ей','ий','ый','ой','ем','им','ым','ом','его','ого','ему','ому','их','ых','ую','юю','ая','яя','ою','ею');
   $participle2 = array('ем','нн','вш','ющ','щ');
   $participle1 = array('ивш','ывш','ующ');
   foreach ($adjective as $suffix) if (substr($word, -(strlen($suffix))) == $suffix){
      $word = substr($word, 0, strlen($word) - strlen($suffix));
      foreach ($participle1 as $suffix) 
         if (substr($word, -(strlen($suffix))) == $suffix && (substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'а' || substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'я')) 
            $word = substr($word, 0, strlen($word) - strlen($suffix));
      foreach ($participle2 as $suffix) 
         if (substr($word, -(strlen($suffix))) == $suffix) 
            $word = substr($word, 0, strlen($word) - strlen($suffix));
      return $word;
   }
   $verb1 = array('ла','на','ете','йте','ли','й','л','ем','н','ло','но','ет','ют','ны','ть','ешь','нно');
   foreach ($verb1 as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix && (substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'а' || substr($word, -strlen($suffix) - CHAR_LENGTH, CHAR_LENGTH) == 'я')) 
         return substr($word, 0, strlen($word) - strlen($suffix));
   $verb2 = array('ила','ыла','ена','ейте','уйте','ите','или','ыли','ей','уй','ил','ыл','им','ым','ен','ило','ыло','ено','ят','ует','уют','ит','ыт','ены','ить','ыть','ишь','ую','ю');
   foreach ($verb2 as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix) 
         return substr($word, 0, strlen($word) - strlen($suffix));
   $noun = array('а','ев','ов','ие','ье','е','иями','ями','ами','еи','ии','и','ией','ей','ой','ий','й','иям','ям','ием','ем','ам','ом','о','у','ах','иях','ях','ы','ь','ию','ью','ю','ия','ья','я');
   foreach ($noun as $suffix) 
      if (substr($word, -(strlen($suffix))) == $suffix) 
         return substr($word, 0, strlen($word) - strlen($suffix));
   return $word;
} 

function step2($word){
   return substr($word, -CHAR_LENGTH, CHAR_LENGTH) == 'и' ? substr($word, 0, strlen($word) - CHAR_LENGTH) : $word;
}

function step3($word){
   $vowels = array('а','е','и','о','у','ы','э','ю','я');
   $flag = 0;
   $r1 = $r2 = '';
   for ($i=0; $i<strlen($word); $i+=CHAR_LENGTH){
      if ($flag==2) $r1 .= substr($word, $i, CHAR_LENGTH);
        if (array_search(substr($word, $i, CHAR_LENGTH), $vowels) !== FALSE) $flag = 1;
      if ($flag = 1 && array_search(substr($word, $i, CHAR_LENGTH), $vowels) === FALSE) $flag = 2;
   }
   $flag = 0;
   for ($i=0; $i<strlen($r1); $i+=CHAR_LENGTH){
      if ($flag == 2) $r2 .= substr($r1, $i, CHAR_LENGTH);
        if (array_search(substr($r1, $i, CHAR_LENGTH), $vowels) !== FALSE) $flag = 1;
        if ($flag = 1 && array_search(substr($r1, $i, CHAR_LENGTH), $vowels) === FALSE) $flag = 2;
    }
   $derivational = array('ост', 'ость');
   foreach ($derivational as $suffix) 
      if (substr($r2, -(strlen($suffix))) == $suffix) 
         $word = substr($word, 0, strlen($r2) - strlen($suffix));
   return $word;
}

function step4($word){
   if (substr($word, -CHAR_LENGTH * 2) == 'нн') $word = substr($word, 0, strlen($word) - CHAR_LENGTH);
   else {
      $superlative = array('ейш', 'ейше');
      foreach ($superlative as $suffix) 
         if (substr($word, -(strlen($suffix))) == $suffix) 
            $word = substr($word, 0, strlen($word) - strlen($suffix));
      if (substr($word, -CHAR_LENGTH * 2) == 'нн') $word = substr($word, 0, strlen($word) - CHAR_LENGTH);
   }
   if (substr($word, -CHAR_LENGTH, CHAR_LENGTH) == 'ь') $word = substr($word, 0, strlen($word) - CHAR_LENGTH);
   return $word;
}

Нам также понадобится массив стоп-слов, не несущих значительной смысловой нагрузки и подлежащих удалению. В него не вошли слова короче трех букв, поскольку их мы удалим в любом случае (существенно снижают точность поиска).

$stopWords = array(
   'что', 'как', 'все', 'она', 'так', 'его', 'только', 'мне', 'было', 'вот',
   'меня', 'еще', 'нет', 'ему', 'теперь', 'когда', 'даже', 'вдруг', 'если',
   'уже', 'или', 'быть', 'был', 'него', 'вас', 'нибудь', 'опять', 'вам', 'ведь',
   'там', 'потом', 'себя', 'может', 'они', 'тут', 'где', 'есть', 'надо', 'ней',
   'для', 'тебя', 'чем', 'была', 'сам', 'чтоб', 'без', 'будто', 'чего', 'раз',
   'тоже', 'себе', 'под', 'будет', 'тогда', 'кто', 'этот', 'того', 'потому',
   'этого', 'какой', 'ним', 'этом', 'один', 'почти', 'мой', 'тем', 'чтобы',
   'нее', 'были', 'куда', 'зачем', 'всех', 'можно', 'при', 'два', 'другой',
   'хоть', 'после', 'над', 'больше', 'тот', 'через', 'эти', 'нас', 'про', 'них',
   'какая', 'много', 'разве', 'три', 'эту', 'моя', 'свою', 'этой', 'перед',
   'чуть', 'том', 'такой', 'более', 'всю'
);

Построение SQL-запроса
Прежде чем получить леммы и построить из них SQL-запрос, вспомним о релевантности. Логично, что страница тем больше соответствует запросу, чем больше в ее тексте запрашиваемых слов (лемм). SQL позволяет определить количество вхождений слова в строку (текст документа) с помощью такой конструкции:

(LENGTH(field) - LENGTH(REPLACE(field, word, ""))) / LENGTH(word)

Конечно, правильнее было бы нормализовать количество вхождений длиной текста, так как налицо разница между десятью повторениями в целой книге и пятью — в одном абзаце (второй случай более релевантен, несмотря на меньшее количество совпадений). Однако средствами SQL без применения хранимых процедур невозможно определить общее количество слов в строке. Поэтому придется пренебречь амплитудой объема страниц, считая их приблизительно одинаковыми.

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

Предварительно будем отсеивать короткие и стоп-слова, а для оставшихся определим леммы, которые и будем искать. В целях дополнительной защиты от слишком «тяжелых» запросов ограничимся пятью первыми словами.

$where = 'false';
$relevance = '0';
$count = 0;
$words = explode(' ', $q);
foreach($words as $w){
   if (utf8_strlen($w) < 3 || in_array($w, $stopWords)) continue;
   if ($count++ > 4) break;
   $w = stem($w);
   $where .= ' OR content LIKE "%'.$w.'%"';
   $relevance .= ' + ((LENGTH(content) -
      LENGTH(REPLACE(content, "'.$w.'", ""))) / LENGTH("'.$w.'"))';
}
$relevance .= ' as relevance';

Поскольку мы намереваемся получать результаты постранично, нам понадобятся два SQL-запроса. Из первого мы узнаем общее количество искомых страниц, а с помощью второго выберем необходимые для отображения (в количестве, указанном в $rpp), отсортировав их по убыванию релевантности.

$tq = mysql_query('SELECT count(*) as n FROM pages WHERE '.$where.' LIMIT 1');
$tr = mysql_fetch_array($tq);
$total = $tr[0];
$rows = mysql_query('SELECT
      title as `title`,
      description as `description`,
      url as `url`,
      LOWER(content) as `content`,
      '.$relevance.'
   FROM pages WHERE '.$where.' ORDER BY relevance DESC
   LIMIT '.($p * $rpp).', '.$rpp);

Вывод результатов
Остается только отобразить форму ввода запроса вместе с результатами поиска:

echo '<form action="">';
echo '<input type="search" name="search" value="'.stripslashes($request).'" />';
echo '<input type="submit" value="Искать" />';
echo '</form>';
echo '<h2>'.($total ? 'Найдено документов: '.$total : 'Ничего не найдено').'</h2>';
echo '<section>';
while($r = mysql_fetch_array($rows)){
   echo '<article>';
   echo '<h3><a href="'.$r['url'].'">'.$r['title'].'</a></h3>';
   echo '<p>'.$r['description'].'</p>';
   echo '</article>';
}
echo '</section>';

И наконец, добавим ссылки для перемещения между страницами результатов:

if ($total){ 
   $url = '?search='.$request; 
   $radius = 5; 
   $current = $p; 
   $offset = $current * $rpp; 
   $lastPage = ceil($total / $rpp) - 1; 
   if ($lastPage){ 
      echo '<nav>'; 
      if ($current > $radius) echo '<a href="'.$url.'">1</a>&nbsp;'; 
      for($i = max(0, $current - $radius);  
         $i <= min($lastPage, $current + $radius); $i++) 
         if ($current == $i) echo '&nbsp;<b>'.($i+1).'</b>&nbsp;'; 
         else { 
            echo '&nbsp;<a href="'.$url.($i ? '&amp;p='.$i : '').'">'; 
            if (($i == $current - $radius && $i > 0) ||  
               ($i == $current + $radius && $i < $lastPage)) echo '&hellip;'; 
               else echo $i+1; 
            echo '</a>&nbsp;'; 
         } 
      if ($current < $lastPage - $radius)  
         echo '&nbsp;<a href="'.$url.'&amp;p='.$i.'/">'.($lastPage+1).'</a>'; 
      echo '</nav>'; 
   } 
}

Исключение тегов и атрибутов
Серьезным минусом безындексной реализации является поиск по всему документу, включая теги и их атрибуты. Например, намереваясь найти страницы со словом «article», вы найдете также и все документы с одноименным тегом. Это не страшно, если у вас русскоязычный сайт о цветах или автомобилях. Хуже, если ваши пользователи действительно часто ищут слова вроде «article», «section», «script» и т. д. Помочь в этом случае может хранимая процедура MySQL, удаляющая из строки все теги. Выглядит она следующим образом:

SET GLOBAL log_bin_trust_function_creators=1; 
DROP FUNCTION IF EXISTS fnStripTags; 
DELIMITER | 
CREATE FUNCTION fnStripTags( Dirty varchar(4000) ) 
RETURNS varchar(4000) 
DETERMINISTIC 
BEGIN 
   DECLARE iStart, iEnd, iLength int; 
   WHILE Locate( '<', Dirty ) > 0 And Locate( '>', Dirty, Locate( '<', Dirty )) > 0 DO 
      BEGIN 
         SET iStart = Locate( '<', Dirty ), iEnd = Locate( '>', Dirty, Locate('<', Dirty )); 
         SET iLength = ( iEnd - iStart) + 1; 
         IF iLength > 0 THEN 
            BEGIN 
               SET Dirty = Insert( Dirty, iStart, iLength, ''); 
            END; 
         END IF; 
      END; 
   END WHILE; 
   RETURN Dirty; 
END; 
| 
DELIMITER ;

Хранимые процедуры поддерживаются в системе InnoDB, но не в MyISAM. Поэтому использование этой возможности остается на ваше усмотрение. В следующей статье мы рассмотрим поиск с индексацией, а также формирование сниппетов на основе поискового запроса с подсветкой найденных слов.

Добавлено: 17 Апреля 2018 21:21:15 Добавил: Андрей Ковальчук

Поиск с подсветкой

<?php
header('Content-Type: text/html;charset=utf8');
$mysql=mysql_pconnect('localhost','root','');
mysql_select_db('tests',$mysql) or die(mysql_error());
mysql_query('set charset utf8') or die(mysql_error());

?>

<form method="post">
<input type="text" name="stext" <?php if($_POST){echo $_POST['stext'];} ?> />
<input type="submit" value="поиск" />
</form>
<style>
   .slovo{
   display: inline;
   color: black;
   background-color: #FC0;
   border-radius: 4px;
   padding: 3px;
   }

</style>

<?php
if($_POST){
	$stext=$_POST['stext'];
	$sl = explode("",$stext);
	$tosearch;
	$all=count($s1)-1;
	foreach($sl as $index=>$val) {
		if($all!=$index){
			$tosearch.="'text' LIKE '%$val%' OR";
		} else {
			$tosearch.="'text' LIKE '%$val%'";
		}
	}
	
	$sql="SELECT * FROM 'search' WHERE (".$tosearch.")";
	$q=mysql_query($sql) or die(mysql_error());
	echo '<ol>';
	while ($arr=mysql_fetch_array($q)) {
		$text=$arr['text'];
		foreach($s1 as $index=>$val) {
			$text=str_replace($val, '<div class="slovo">'.$val.'</div>', $text);
		}
		echo '<li>'.$text.'</li>';
	}
	echo '</ol>';
}
mysql_close($mysql);
?>

Добавлено: 11 Апреля 2018 07:41:01 Добавил: Андрей Ковальчук

Поиск на AJAX (аяксе), 'Живой поиск' - поиск без перезагрузки страницы

По просьбе одного из пользователей я решил сделать небольшой ликбез про то как создать своего рода "Живой поиск" на php и avascript, т.е. используя систему AJAX.

Почему "своего рода", а не просто "Живой поиск" - потому как настоящий "Живой поиск" требует больше критериев для проверки запроса на стороне клиента.

Но в данном случае я решил, что достаточно будет все лишь проверки на отсутствие строки состоящей из пробелов.

Так же кто не знает, что такое "Живой поиск" - это поиск осуществляющийся без перезагрузки страницы, с незамедлительной выдачей результатов.

Первое, что мы сделаем - это создадим файлик index.php с 3-мя полями которые нам будут служить:
1. Для ввода поискового запроса (input типа text);
2. Для подсчитывания количество символов в тексте (скрытый input);
3. Для дублирования текста поискового запроса (скрытый input);
а также создаем блок div с id = search_result для результатов поиска и подключим в шапке библиотеку jquery.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru-ru" lang="ru-ru" >
<head>
<script type="text/javascript" src="http://yandex.st/jquery/1.8.1/jquery.min.js"></script>
</head>
<body>
<input id="search_words" type="text" />
<input id="count_chars" type="hidden" value="0" />
<input id="re_chars" type="hidden" value="" />
<div id="search_result"> </div>
</body>
</html>

Следующим шагом будет javascript, объясню кратко логику работы скрипта. После загрузки страницы, а именно после загрузки DOM - модели страницы у нас запускается таймер который с периодичностью (сами можете указать в микросекундах) 1000 микросекунд - 1 секунда, будет запускать проверку на наличие текста в поле ввода поискового запроса. В скрытых полях с id = count_chars будет храниться количество символов введенных поле (после загрузки страницы равен 0), в поле с id = re_chars хранится копия запроса потому как только по количеству символов мы не сможем определить изменился ли запрос или нет. После изменения в поле запроса проверяется и сравнивается длина и содержимое текста с выше указанными полями, если изменился - отправляем запрос серверу.

Скрипт для обработки данных введенных в поле поиска:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru-ru" lang="ru-ru" >
<head>
<script type="text/javascript" src="http://yandex.st/jquery/1.8.1/jquery.min.js"></script>
<script type="text/javascript">
function getSearchInput(){
var maskInput = $("#search_words").val();
var cStart = $("#count_chars").val();
var countChars = maskInput.replace(/\s+/g, "").length;
if(countChars != 0)
{
var cStartw = $("#re_chars").val();
var searchInput = $("#search_words").val();
}
else
{
var cStartw = ""
var searchInput = ""
}
if(cStart != countChars || cStartw != searchInput)
{
$("#count_chars").val(countChars);
$("#re_chars").val(searchInput);
}
}
$(document).ready(function(){
setInterval(getSearchInput,1000);
});
</script>
</head>
<body>
<input id="search_words" type="text" />
<input id="count_chars" type="hidden" value="0" />
<input id="re_chars" type="hidden" value="" />
<div id="search_result"> </div>
</body>
</html>

Дальше подготовим базу данных для поиска нужного нам значения выполнив запрос в вашем phpMyAdmin или любом другом MySQL клиенте (Естественно если вы будете использовать данную схему в реальных проектах то данные базы данных будут другие и скорее всего вам не потребуется создавать базу отдельно!!!):
CREATE DATABASE metal;
USE metal;
CREATE TABLE metal_name (name_metal VARCHAR(255));
INSERT INTO metal_name (name_metal) VALUES ('iron'),('good iron'),('bad iron'),('cloud metal'),('better metal'),('loopers'),('all metal'),('pre_metal');

Следующим шагом будет дополнение нашего файла php кодом который будет обрабатывать запрос и приведем его в такой вид:
<?php 
   if(isset($_GET["search_words"])){ 
      $db_name = "metal";//Здесь название вашей базы данных 
      $db_user = "a";//Здесь имя пользователя базы данных 
      $db_password = "a";//Здесь пароль вашего пользователя 
      $db_host = "localhost";//Здесь хост или IP адрес серевера MySQL 
      $html = ""; 
      $search_words = $_GET["search_words"]; 
      if(!mysql_connect($db_host,$db_user,$db_password)) 
      exit("NO CONNECT TO MYSQL"); 
      $db = mysql_select_db($db_name); 
      $query = "SELECT * FROM metal_name WHERE name_metal LIKE '%{$search_words}%'"; 
      $pre_result = mysql_query($query); 
      while($result = mysql_fetch_assoc($pre_result)){ 
         $result = str_replace($search_words,"<b><i>".$search_words."</i></b>",$result["name_metal"]); 
         $html .= "<p>".$result."</p>"; 
      } 
      echo !empty($html)?$html:"No result"; 
   } 
   else { 
?> 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru-ru" lang="ru-ru" > 
<head> 
   <script type="text/javascript" src="http://yandex.st/jquery/1.8.1/jquery.min.js"></script> 
   <script type="text/javascript"> 
      function getSearchInput(){ 
         var maskInput = $("#search_words").val(); 
         var cStart = $("#count_chars").val(); 
         var countChars = maskInput.replace(/\s+/g, "").length; 
         if(countChars != 0) 
         { 
            var cStartw = $("#re_chars").val(); 
            var searchInput = $("#search_words").val(); 
         } 
         else 
         { 
            var cStartw = ""; 
            var searchInput = ""; 
         } 
         if(cStart != countChars || cStartw != searchInput) 
         { 
            $("#count_chars").val(countChars); 
            $("#re_chars").val(searchInput); 
         } 
      } 
      $(document).ready(function(){ 
         setInterval(getSearchInput,1000); 
      }); 
   </script> 
</head> 
<body> 
   <input id="search_words" type="text" /> 
   <input id="count_chars" type="hidden" value="0" /> 
   <input id="re_chars" type="hidden" value="" /> 
   <div id="search_result"></div> 
</body> 
</html> 
<?php } ?>

Ну и на конец добавим javascript AJAX запрос который будет выгружать результат на страницу:
$("#search_result").load("index.php?search_words=" + searchInput);

В итоге имеем файл с таким содержимым:
<?php 
   if(isset($_GET["search_words"])){ 
      $db_name = "metal";//Здесь название вашей базы данных 
      $db_user = "a";//Здесь имя пользователя базы данных 
      $db_password = "a";//Здесь пароль вашего пользователя 
      $db_host = "localhost";//Здесь хост или IP адрес серевера MySQL 
      $html = ""; 
      $search_words = $_GET["search_words"]; 
      if(!mysql_connect($db_host,$db_user,$db_password)) 
      exit("NO CONNECT TO MYSQL"); 
      $db = mysql_select_db($db_name); 
      $query = "SELECT * FROM metal_name WHERE name_metal LIKE '%{$search_words}%'"; 
      $pre_result = mysql_query($query); 
      while($result = mysql_fetch_assoc($pre_result)){ 
         $result = str_replace($search_words,"<b><i>".$search_words."</i></b>",$result["name_metal"]); 
         $html .= "<p>".$result."</p>"; 
      } 
      echo !empty($html)?$html:"No result"; 
   } 
   else { 
?> 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru-ru" lang="ru-ru" > 
<head> 
   <script type="text/javascript" src="http://yandex.st/jquery/1.8.1/jquery.min.js"></script> 
   <script type="text/javascript"> 
      function getSearchInput(){ 
         var maskInput = $("#search_words").val(); 
         var cStart = $("#count_chars").val(); 
         var countChars = maskInput.replace(/\s+/g, "").length; 
         if(countChars != 0) 
         { 
            var cStartw = $("#re_chars").val(); 
            var searchInput = $("#search_words").val(); 
         } 
         else 
         { 
            var cStartw = ""; 
            var searchInput = ""; 
         } 
         if(cStart != countChars || cStartw != searchInput) 
         { 
            $("#search_result").load("index.php?search_words=" + searchInput); 
            $("#count_chars").val(countChars); 
            $("#re_chars").val(searchInput); 
         } 
      } 
      $(document).ready(function(){ 
         setInterval(getSearchInput,1000); 
      }); 
   </script> 
</head> 
<body> 
   <input id="search_words" type="text" /> 
   <input id="count_chars" type="hidden" value="0" /> 
   <input id="re_chars" type="hidden" value="" /> 
   <div id="search_result"></div> 
</body> 
</html> 
<?php } ?>

Это не идеальный код для "Живого поиска" - но мои заказчики и пользователи остались довольны!

Добавлено: 21 Августа 2013 12:55:55 Добавил: Андрей Ковальчук