1.8. Грабли, подводные камни и немного об оптимизации
К сожалению, рассказать обо всех нюансах я просто не в состоянии, на это не хватит времени, но какие-то основные описать могу:
* кодировка в базе данных, по умолчанию у меня стоит всегда cp1251, возможно поэтому я не испытывал особо трудностей во время поисков, менять на другую кодировку ради проверки - я не стал; * кодировка (локализация) в скрипте CP1251, не всегда эта кодировка по умолчанию установлена на сервере, если будет наблюдаться не адекватное поведение, то её требуется проверить (вообще, насчет кодировки cp1251 - это не панацея, просто я использую её); * полнотекстовый индекс по умолчанию индексирует слова размером более 3-х символов (слова из 3-х символов - не индексируются). Если требуется индексировать слова менее 4-х символов, то нужно будет настроить конфиг MySQL, как это сделано, прекрасно описано в главе <6.8.2. Тонкая настройка полнотекстового поиска в MySQL>. В связи с этим можно так же исключать из поля search таблицы слова размер которых менее индексного, для экономии места;
Вот, собственно, и все, просто и компактно. Пора заняться настоящим "весельем"... :-) 2. Способ второй: "Изобретаем велосипед" или "Пляски с бубнами"
... А в PostrgeSQL FULLTEXT нету :(... (Цитата из ЖЖ)
Достаем из тумбочки старый любимый бубен, разжигаем костер и начинаем готовится к пляске.
Сразу хочу сказать, что данное решение мне нравится больше:
* во-первых - используются стандартные инструменты, что позволяет сделать поисковую систему максимально кроссплатформенной; * во-вторых - возможность более "тонкой" настройки поисковой системы в целом и в частности.
По каким критериям производится поиск по сайту:
* совпадение слова - это само собой; * "вес" слова на страницы, то есть количество повторов слова на странице.
При этом я совершенно не учитываю расположение слова на странице и то, находятся ли поисковые слова рядом, или же в разных частях документа. С одной стороны - это плохо, но с другой - мы же не пишем поисковую систему Google, нам нужно найти что-либо в пределах одного сайта, поэтому излишние критерии релевантности - ни к чему, только лишняя головная боль и бесполезная трата ресурсов.
Для нашей поисковой системы нужно будет создать три таблицы:
* слова (search_main) - таблица в которой хранятся (раздельно!) все поисковые слова сайта, страница к которой они относятся и их вес; * страницы (search_page) - URL, заголовки и описания страницы. Хотя возможно эти данные хранить применительно к каждому поисковому слову, но это тоже лишняя трата ресурсов; * фильтр (search_filter) - список слов не включаемых в поисковые - это имена стилей, некоторые теги, операторы JavaScript; в общем, те слова, которые не требуются для поиска.
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 ;
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..$url_start.$file..$title..$description; print PAGES $line, ; # Переводим текст, контент страницы, в нижний регистр $$content =~tr /A-ZxA8xC0-xDF/a-zxB8xE0-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..$i..$words{$word}; print WORDS $line, ; } # Обновляем идентификатор страницы $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 ; # Форма запроса 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 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; * не учтена возможность создания фильтров для определенных файлов (кроме как по расширению); * не учтена возможность создания фильтров для определенных вложенных папок (кроме как корневых); * может что-то еще не учтено :)...
но, впрочем, скелет дан, нарастить мясо - на совести программиста...