Поиск по сайту, не самый сложный элемент, но довольно муторный. Так не хочется его делать, а надо. Я не буду рассматривать возможности внедрения в сайт поисковых форм Яндекса или Google, про это можно почитать у них самих. Будем делать собственный поиск по сайту.
Итак, что у нас дано:
Для того что бы у нас осуществлялся поиск нужно будет собрать "поисковые индексы". Я использую для этого два способа (способов, на самом деле, гораздо больше): простой и немного сложнее. В первом я использую встроенные функции MySQL базы данных, во втором - собственный велосипед.
Определим алгоритм работы скрипта индексирования поисковой машины (основные подпрограммы):

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

Данный скрипт нужно максимально упростить, так как индексацию мы запускаем максимум раз в сутки, то поисковый скрипт запускается на несколько порядков больше раз и тратить ресурсы во время поиска - нецелесообразно. Для этого требуется максимально оптимизировать информацию в базе данных, что бы она возвращала нам максимально подготовленную информацию для вывода, чтобы не производить лишних манипуляций.
Какая информация нужна нам для вывода результатов запроса:
В качестве "подопытного кролика" я выбрал портал АльфаКМВ. Этот ресурс имеет в своем составе немногим более 3000 страниц разной вложенности в папках и можно спокойно оценить скорость работы нашей поисковой системы.
Хоть MySQL считается не особо "навороченной" базой данных (хотя я лично так не считаю), у неё есть неоспоримые плюсы - это простота использования, а основной, в нашем случае, индекс FULLTEXT, который без особых сложностей организует нам прекрасный поиск. нужно просто приложить к этому небольшие усилия:
1.1. Организация таблицы
Индексная таблица состоит всего из четырех полей - ссылка на страницу (url), заголовок страницы (title), описание страницы (description) и текстовая часть (полнотекстовый индекс):
1.2. Рекурсия
Вторым этапом нам нужно пройтись по всем папкам и файлам сайта для индексации, для чего воспользуемся рекурсией.
...
Рекурсия - вызов функции или процедуры из неё же самой (обычно с другими значениями входных параметров), непосредственно или через другие функции (например, функция А вызывает функцию B, а функция B — функцию A). Количество вложенных вызовов функции или процедуры называется глубиной рекурсии.
...
Следует избегать избыточной глубины рекурсии, так как это может вызвать переполнение стека.
...
Задумчиво, но так как мы не знаем глубину папок в которых могут лежать файлы сайта, то прийдется использовать её, хотя можно поискать на CPAN, но мне кажется, это лишняя трата времени, быстрее написать самому.
Создаем скрипт, который будет индексировать наш сайт, назовем его index.pl.
Как видно - никаких сложностей. Однако хочу заметить, что в глубь рекурсии мы уходим только для директорий, а не символьных ссылок, причем, я бы и не рекомендовал использовать символьные ссылки, чтобы рекурсия не зациклилась во время обработки.
1.3. Предварительное формирование данных или просто формирование данных
Третий этап - подготовка файла и индексации. Так как очень часто на страницах сайта используются SSI внедрения, то их нужно будет включить в основное тело страницы.
...
в одном разделе сайта дизайнер внедрил красивый заголовок через SSI, когда поисковая система проиндексировала страницы, то ключевые слова заголовка были пропущены, и поиск осуществлялся "криво"
...
В данной процедуре, производится обработка контента файла. Хочу заметить, что SSI я обрабатываю только для директивы include virtual, при этом не проверяю внедряемый файл, если же через include virtual внедряются скрипты или используются дополнительные директивы, то данный код нужно будет соответственно доработать. Так же может возникнуть вопрос, почему я разбиваю скрипт на такие маленькие процедуры, когда, по большому счету, достаточно было бы описать это в одной процедуре - все это только лишь для того что бы облегчить понимание предмета, а последнее вынесение процедуры update_data - потому что дальше способы индексации разнятся между собой.
1.4. Обновление блока данных
В общем, в эту процедуру мы передаем уже практически готовые данные для вставки в базу данных, поэтому:
Для варианта с LOAD DATA:
для варианта с INSERT INTO:
Правда, во втором варианте нужно не забыть предварительно подключится к базе данных.
1.5. Обновление базы данных
Можно рассмотреть два варианта обновления данных:
Если мы обновлять данные будем с помощью LOAD DATA. Информация уже сформирована и требуется только обновить базу данных:
Хочу обратить внимание на то что я указываю абсолютные пути к временному файлу. Это условие обязательное, так как скрипт рассчитывается на запуск с помощью cron.
Теперь рассмотрим особенности обновления данных, с помощью INSERT, и с помощью LOAD DATA. Довольно противоречивое мнение у меня сложилось по поводу выбора способа обновления. С одной стороны команда INSERT очень медленная, но с другой, тратится меньше ресурсов. Я протестировал оба варианта, благо изменения скриптов для этого не большие (вместо дописывания данных во временный файл вставляем запись в таблицу, а процедуру обновления базы данных опускаем). Итак, что получилось:
Тестирование производилось, на одном и том же сайте но на разных серверах (более и менее мощном), сайт все тот же ~3000 статичных страниц:
Более мощный сервер - P4 2.8 (HyperThreading), 800 Mhz FSB, память двухканальная 400 Mhz Kingston 512 MB, Promise UltraDMA133, 2 х 40Gb (Seagate Barracuda) зеркало (на нем сайт) и еще 120 Gb (Maxtor) SATA (на нем ядро SuSE 9.2, MySQL 4.0.18).
Прирост производительности не большой - 15%, но во время обновления базы данных сама база находилась "в трансе", т.е. другие обращения к базе данных происходили с большой задержкой. Отсюда можно сказать - быстрее, но не рациональнее.
Более слабый сервер - P4 2.4, 533 Mhz FSB, память 333 Mhz 1024 MB, 20 Gb (Samsung 7200) на нем ядро Red Hat 7.3, MySQL 4.0.18 и сайт.
Конечно, большой разброс по времени дало количество текущих процессов (видимо сказалось отсутствие HyperThreading), но результат показывает, что прирост производительности составил, как минимум 100%, и хотя база данных была дольше "в трансе", но не в таком глубоком (почему - сложно сказать конфиги MySQL идентичны, может большее количество оперативной памяти сказалось).
Итак - решать Вам по какому пити идти, все зависит от сервера, его возможностей и ограничений.
1.6. Скрипт вывода результатов поиска
Вот теперь самое интересное, зачем мы собственно делали столько манипуляций. Я не буду особо расписывать данный скрипт: формировать постраничный вывод, "наводить красоту" и так далее... просто сделаю скелет:
Практически наш скрипт готов. Он прекрасно отрабатывает полнотекстовый поиск, при этом без особых сложностей.
1.7. Дополнительные возможности
Для начала, (хотя это нужно было сделать в самом начале) ознакомимся с документацией MySQL - <6.8. Полнотекстовый поиск в MySQL> в данном документе сказано, что существует возможность усложнения поискового запроса по индексу, то есть определить "вес" слов, а так же использовать их "усечение". Доработав немного скрипт поиска можно создать "сносную" поисковую машину, которая учитывает морфологию. Для этого сделаем следующее:
а). в переменной поискового запроса заменим два-три последних символа в каждом слове, а так же добавим разрешенные символы:
б). в запросе к базе данных укажем IN BOOLEAN MODE:
Но, хочу сразу оговорится: при использовании BOOLEAN MODE на редкость плохо считается релевантность* и результаты запроса не сортируются, поэтому использовать эту функцию - IMHO не стоит. И на помощь приходит "солдатская смекалка", что нам мешает во время индексации формировать двойной контент с полными словами и с "обрезанными" и так же расширить подобным образом запрос?
а). в скрипте индексации, после "чистки" контента файла:
б). в поисковом скрипте, после "передачи" формы:
И "о чудо!!!" скрипт начал искать, то что раньше его было не заставить. Конечно имеет смысл еще "поиграть" с количеством обрезаемых символов в словах и формировать не "двойной" поисковый запрос, а "тройной" и более... но это дело техники...
Так же, не нужно забывать о том, что именно ищут пользователи на Вашем сайте. Для того, что бы это определись, достаточно сохранять поисковые запросы пользователей, а потом анализировать их. В соответствии с анализом корректировать контент на страницах для более "правильного" поиска (например: пришлось включить в контент страниц слово "анегдот", потому как половина пользователей искала именно его).
*ПРИМЕЧАНИЕ: Это не написано в официальной документации, но как показывает практика, при использовании IN BOOLEAN MODE, отключается критерий поиска фразы, то есть, если в поисковом запросе несколько слов, то они ищутся не как фраза, а каждое слово отдельно, при этом совпадение слова определяется как 1, коэффициент релевантности в итоге получается целое число, варьировать дробной частью которого возможно только "весом" слов, что не приемлемо для большинства пользователей.
1.8. "Грабли", "подводные камни" и немного об оптимизации
К сожалению, рассказать обо всех нюансах я просто не в состоянии, на это не хватит времени, но какие-то основные описать могу:
Вот, собственно, и все, просто и компактно. Пора заняться настоящим "весельем"... :-)
... А в PostrgeSQL FULLTEXT нету :(... (Цитата из ЖЖ)
Достаем из тумбочки старый любимый бубен, разжигаем костер и начинаем готовится к пляске.
Сразу хочу сказать, что данное решение мне нравится больше:
По каким критериям производится поиск по сайту:
При этом я совершенно не учитываю расположение слова на странице и то, находятся ли поисковые слова рядом, или же в разных частях документа. С одной стороны - это плохо, но с другой - мы же не пишем поисковую систему Google, нам нужно найти что-либо в пределах одного сайта, поэтому излишние критерии релевантности - ни к чему, только лишняя головная боль и бесполезная трата ресурсов.
Для нашей поисковой системы нужно будет создать три таблицы:
2.1. Организация таблиц
Структура таблиц и связей выглядит так:

Команды на создание таблиц:
CREATE TABLE `search_filter` (
`word` varchar(100) NOT NULL,
`note` varchar(100) NULL,
PRIMARY KEY (`word`)
) TYPE=MyISAM;
CREATE TABLE `search_main` (
`word` varchar(100) NOT NULL default '',
`page` int(11) NOT NULL default '0',
`relevance` int(11) NOT NULL default '0',
KEY `word` (`word`,`page`)
) TYPE=MyISAM;
CREATE TABLE `search_page` (
`id` int(11) NOT NULL,
`url` varchar(200) NOT NULL default '',
`title` varchar(200) NOT NULL default '',
`description` text NOT NULL,
PRIMARY KEY (`id`)
) TYPE=MyISAM;
2.2. Предварительное формирование данных или просто формирование данных
Не будем возвращаться к рекурсии и обработке файла, так как они идентичны (о чем было сказано выше).
Итак, что мы должны сделать в этой процедуре. Контент практически подготовлен, нужно сформировать 2 блока (файла) данных. Для этого в самом начале скрипта откроем для последовательной записи (если они не были заранее очищены, то их очищаем) и выберем слова исключения (search_filter). Так же в начале скрипта мы определяем глобальную переменную $i =1 которая будет у нас идентификатором страницы, вот почему мы не указали при создании таблиц автоматических счетчиков. Объясняю почему:
2.3. Обновление блока данных
Определим основные действия процедуры:
*ПРИМЕЧАНИЕ: Цифра 3 как раз и отвечает за размер слова, которые разрешены для индексации
2.4. Обновление базы данных
Данная процедура просто выгружает в базу данных наши два файла, после чего их удаляет
sub update_db {
# Загружаем данные
$dbh->do("LOAD DATA INFILE "/var/www/sites/alfakmv/cgi-bin/search2/words.txt" INTO TABLE search_main;")
|| die "ERROR!!! $DBI::errstr <br>";
$dbh->do("LOAD DATA INFILE "/var/www/sites/alfakmv/cgi-bin/search2/pages.txt" INTO TABLE search_page;")
|| die "ERROR!!! $DBI::errstr <br>";
# Удаляем временные файлы
unlink '/var/www/sites/alfakmv/cgi-bin/search2/words.txt';
unlink '/var/www/sites/alfakmv/cgi-bin/search2/pages.txt';
return 1;
}
Правда еще хотел оговориться, что при индексации формируются файлы по объему соразмерные с объемом текстовой части сайта, поэтому могут возникнуть проблемы с лимитом дискового пространства на хостинге.
На этом, с индексацией все. Я даже не рассматриваю варианты обновления данных с помощью команд LOAD DATA и INSERT так как, в таблицу слов вставляется записей не на один порядок больше чем в первом варианте с FULLTEXT (~3000 против ~2000000), а таблицу страниц - ровно такое же количество, правда в гораздо меньшем объеме.
2.5. Скрипт вывода результатов поиска
В данный скрипт особо не отличается от скрипта первого варианта, единственное радикальное различие - запрос к базе данных, он более сложный, так как выборка производится из двух таблиц, условие основано на списке слов поискового запроса и прочие мелочи...
#!/usr/bin/perl
# Подключаем основные модули
use strict;
use warnings;
use DBI;
use CGI qw(param);
use locale;
use POSIX qw(locale_h);
setlocale(LC_CTYPE, 'ru_RU.CP1251');
setlocale(LC_ALL, "ru_RU.CP1251");
# Получаем поисковый запрос
my $search = param('search') || undef;
# Сразу отправляем заголовок браузеру
print "Content-type: text/html; charset=windows-1251 ";
# Форма запроса
print '<form action='' method=get>';
print '<input type=text name=search value="'.($search || '').'">';
print '<input type=submit value=search>';
print '</form>';
# Если запрос пустой, то останавливаем скрипт
unless ($search) {print 'Результатов запроса - 0'; exit}
# На всякий случай "чистим" полученные данные
$search =~s /[^ws-]/ /g;
# "Сжимаем" пробельные символы
$search =~s /s+/ /g;
# Подключаемся к базе данных
my $dbh = 'DBI'->connect('DBI:mysql:database=search;host=localhost;port=3306', 'root', 'dfkmrbhbz')
|| die $DBI::errstr;
# Формируем запрос
my @search = split(' ', $search);
my $sql = "SELECT
t2.url, t2.title, t2.description, SUM(t1.relevance) AS score
FROM search_main AS t1, search_page AS t2
WHERE t1.word IN ('".join("','",@search)."') AND t1.page = t2.id
GROUP BY t1.id
ORDER BY score DESC
&nb