Главная страница статей --> Хитрости при программировании php, заметки по базам данных

Поиск по сайту - статичный контент (Perl)

Источник: realcoding.net

[2 страница]
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; в общем, те слова, которые не требуются для поиска.

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(100NOT NULL   default ,
             `
page`       int(11)       NOT NULL   default 0,
             `
relevanceint(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 ,
             `
descriptiontext           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;
* не учтена возможность создания фильтров для определенных файлов (кроме как по расширению);
* не учтена возможность создания фильтров для определенных вложенных папок (кроме как корневых);
* может что-то еще не учтено :)...

но, впрочем, скелет дан, нарастить мясо - на совести программиста...

Контакты
Редакция:
[0.002]