2. Способ второй: "Изобретаем велосипед" или "Пляски с бубнами"
... А в PostrgeSQL FULLTEXT нету :(... (Цитата из ЖЖ)
Достаем из тумбочки старый любимый бубен, разжигаем костер и начинаем готовится к пляске.
Сразу хочу сказать, что данное решение мне нравится больше:
- во-первых - используются стандартные инструменты, что позволяет сделать поисковую систему максимально кроссплатформенной;
- во-вторых - возможность более "тонкой" настройки поисковой системы в целом и в частности.
По каким критериям производится поиск по сайту:
- совпадение слова - это само собой;
- "вес" слова на страницы, то есть количество повторов слова на странице.
При этом я совершенно не учитываю расположение слова на странице и то, находятся ли поисковые слова рядом, или же в разных частях документа. С одной стороны - это плохо, но с другой - мы же не пишем поисковую систему Google, нам нужно найти что-либо в пределах одного сайта, поэтому излишние критерии релевантности - ни к чему, только лишняя головная боль и бесполезная трата ресурсов.
Для нашей поисковой системы нужно будет создать три таблицы:
- слова (search_main) - таблица в которой хранятся (раздельно!) все поисковые слова сайта, страница к которой они относятся и их вес;
- страницы (search_page) - URL, заголовки и описания страницы. Хотя возможно эти данные хранить применительно к каждому поисковому слову, но это тоже лишняя трата ресурсов;
- фильтр (search_filter) - список слов не включаемых в поисковые - это имена стилей, некоторые теги, операторы JavaScript; в общем, те слова, которые не требуются для поиска.
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 которая будет у нас идентификатором страницы, вот почему мы не указали при создании таблиц автоматических счетчиков. Объясняю почему:
- во-первых, данные вставляются в базу данных не сразу, а после обработки всей информации, а нам нужно будет сразу определять связь слово->страница;
- во-вторых, даже при последовательном внесении информации в базу данных, прийдется делать дополнительный запрос для определения последнего идентификатора страницы;
- в-третьих, таблица базы данных пустая, и за уникальность идентификаторов можно не волноваться.
#!/usr/bin/perl
# Подключаем основные модули
use strict;
use warnings;
use DBI;
use locale;
use POSIX qw (locale_h);
setlocale(LC_CTYPE, 'ru_RU.CP1251');
setlocale(LC_ALL, 'ru_RU.CP1251');
# Обозначаем глобальные переменные
use vars '$dbh', '$url_start', '$dir_start', '@dir_filter', '@file_type', '$i', '%filter';
# Инициализируем идентификатор страниц
$i = 1;
# Директория DocumentsRoot сайта
$dir_start = '/var/www/sites/alfakmv/html';
# Домен сайта
$url_start = 'http://www.alfakmv.ru';
# Фильтр директорий (директории, которые исключаются из индексации)
@dir_filter = (
'cgi-bin',
'images',
'temp',
);
# Фильтр файлов (какие расширения файлов индексировать)
@file_type = (
'shtml',
'html',
'htm',
);
# Коннектимся
$dbh = 'DBI'->connect('DBI:mysql:database=search;host=localhost;port=3306', 'user', 'pass')
|| die $DBI::errstr;
# Выбираем слова - исключения
my $sql = 'SELECT word FROM search_filter';
my $sth = $dbh->prepare($sql);
$sth->execute() || die $DBI::errstr;
while (my $row = $sth->fetchrow_hashref()) {$filter{$$row{'word'}} = 1}
$sth->finish();
# Очищаем таблицы базы данных
$dbh->do('DELETE FROM search_main');
$dbh->do('DELETE FROM search_page');
# Сразу отправляем заголовок браузеру
print "Content-type: text/html; charset=windows-1251\n\n";
open (WORDS, '>>', '/var/www/my_sites/cgi-bin/search/words.txt');
flock WORDS, 2;
open (PAGES, '>>', '/var/www/my_sites/cgi-bin/search/words.txt');
flock PAGES, 2;
# Передаем управление процедуре рекурсии
&recursion();
close PAGES;
close WORDS;
&update_db;
exit;
2.3. Обновление блока данных
Определим основные действия процедуры:
- сформировать строку для блока данных страниц, и записать её в файл;
- обработать контент страницы, подсчитать вес слов и сформировать список;
- дописать список в блок данных (файл) слов;
sub update_data {
# Получаем данные
my ($content, $title, $description, $file) = @_;
# Формируем строку блока данных страниц и записываем её в файл
my $line = $i."\t".$url_start.$file."\t".$title."\t".$description;
print PAGES $line, "\n";
# Переводим текст, контент страницы, в нижний регистр
$$content =~tr /A-Z\xA8\xC0-\xDF/a-z\xB8\xE0-\xFF/;
# Определяем хеш для подстчета веса слов
my %words;
foreach my $word (split(' ', $$content)) {
# Фильтрация слов
next if length $word < 3; # Примечание*
next if exists $filter{$word};
# Формируем хеш слов и их вес
if (exists $words{$word}) {$words{$word}++} else {$words{$word} = 1}
}
# Формируем строки блока данных слов
foreach my $word (keys %words) {
my $line = $word."\t".$i."\t".$words{$word};
print WORDS $line, "\n";
}
# Обновляем идентификатор страницы
$i++;
return 1;
}
*ПРИМЕЧАНИЕ: Цифра 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\n\n";
# Форма запроса
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 /[^\w\s\-]/ /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
LIMIT 50";
my $sth = $dbh->prepare($sql);
$sth->execute() || die $DBI::errstr;
# Устанавливаем счетчик
my $i = 1;
while (my $row = $sth->fetchrow_hashref()) {
# Печатаем строку результата
print $i, ' - <a href="', $$row{'url'}, '">', $$row{'title'}, '<a><br>',$$row{'description'}, ' - ', $$row{'score'},'<br><br>';
$i++
}
$sth->finish();
# Отключаемся от базы данных
$dbh->disconnect();
if ($i == 1) {print 'Результатов запроса - 0'}
else {print 'Результатов запроса - ', $i - 1}
exit;
Вот и все, совсем все, осталось сравнить эти 2 способа.
3. Сравнение
Сравнение проводилось на одном и том же сервере, индексировался один и тот же сайт.
|
С использованием FULLTEXT |
Ручная обработка |
Объем занимаемых данных (относительно друг друга) |
1 |
0,43 |
Скорость индексации (относительно друг друга, средняя величина, индексация с помощью команды FULLTEXT) |
1 |
0,97 |
Скорость поискового запроса к базе данных |
0,02 сек (~3 300 записей) |
0,31 сек (~2 300 000 записей) |
В итоге мы видим, что несмотря на то что объем данных во втором способе гораздо меньше (индекс FULLTEXT довольно объемный), скорость индексации отличается незначительно (если совсем не отличается), а вот запрос для выборки результатов гораздо медленнее. Это связано с гораздо большим количеством записей, и более сложным запросом из двух таблиц. Можно, конечно, во втором способе данные о странице хранить в основной таблице, но при этом объем данных увеличивается в 5-6 раз, а скорость запроса убыстряется всего на 10-15 %, что, впрочем, не актуально. Впрочем, для небольших сайтов оба варианта будут одинаково приемлемы, так как все таки тестирование проводилось на сайте, имеющем более 3000 статичных страниц.
При этом результаты выполнения запроса практически идентичны в обоих случаях, различие было только в порядке вывода (релевантности), что никак не сказывалось на правильности поиска.
Заключение
Итак мы рассмотрели 2 способа организации поиска по сайту. Следует иметь в ввиду, что поиск осуществляется по статичным страницам сайта и никак не предназначен для динамичных сайтов. Естественно были рассмотрены практически идеальные варианты построения сайта:
- не учтена возможность существования символьных ссылок;
- не учтены вариации метатегов title и description;
- не учтены вариации использования SSI;
- не учтена возможность создания фильтров для определенных файлов (кроме как по расширению);
- не учтена возможность создания фильтров для определенных вложенных папок (кроме как корневых);
- может что-то еще не учтено :)...
но, впрочем, скелет дан, нарастить мясо - на совести программиста...
Другие статьи по теме
|