====== Пример интеграции Zend_Search с Limb3 ======
В [[http://framework.zend.com|ZendFramework]] есть довольно мощный модуль поиска [[http://framework.zend.com/manual/en/zend.search.html|Zend_Search]], который позволяет индексировать контент и производить по нему поиск с приличной скоростью и без участия БД или каких-либо нестандартных PHP расширений. Дабы не изобретать колесо, имеет смысл использовать этот модуль совместно с Limb3.
:!: В примере используются Limb3-2007.1 и %%ZendFramework%%-0.8
Ниже приведено описание возможного использование модуля поиска Zend_Search совместно с пакетоми WEB_SPIDER и SEARCH в некотором абстрактном проекте.
===== Постановка задачи =====
Что в итоге должно получиться:
* Шелл скрипт cli/indexer.php, который по cron'у обходит весь веб контент сайта и индексирует его средствами Zend_Search
* Простейший контроллер, который производит поиск по индексу и выставляет результат своего выполнения во view
===== Настройка библиотек =====
Прежде чем начнем, стоит заметить, как правильно настроить include_path для Limb3 и %%ZendFramework%%. Где-то в setup.php своего проекта стоит поместить подобную строчку:
Это подразумевает, что и Limb3 и %%ZendFramework%% расположены в директории lib проекта. Однако ставить весь ZF смысла нет - потребуется только директория Search и все ее содержимое и файл Exception.php в корне директории. Выглядит это примерно так:
`-lib
`-limb/...
`-Zend
`-Search/..
`-Exception.php
===== Индексирующий шелл скрипт =====
С установками разобрались, перейдем сразу к шелл скрипту.
**cli/indexer.php**
useNOINDEX();
$observer = new lmbSearchIndexingObserver($indexer);
$content_type_filter = new lmbContentTypeFilter();
$content_type_filter->allowContentType('text/html');
$uri_filter = new lmbUriFilter();
$uri_filter->allowHost($uri->getHost());
$uri_filter->allowProtocol('http');
$uri_filter->allowPathRegex('~.*~');
$normalizer = new lmbUriNormalizer();
$normalizer->stripQueryItem('PHPSESSID');
$spider = new lmbWebSpider();
$spider->setContentTypeFilter($content_type_filter);
$spider->setUriFilter($uri_filter);
$spider->setUriNormalizer($normalizer);
$spider->registerObserver($observer);
$spider->crawl($uri);
?>
В приведенном выше скрипте происходит много чего интересного, однако, по-большому счету, мы просто объединяем несколько компонентов в единое целое и конфигурируем их. Более подробная информация о пакете WEB_SPIDER и SEARCH появится в соответсвующих разделах, здесь лишь приведем базовую информацию.
- lmbWebSpider обходит веб контент по ссылкам, которые он в нем находит. lmbWebSpider гарантирует, что ссылка будет посещена только однажды, т.е не произойдет зацикливания
- lmbWebSpider позволяет настроить практически все аспекты своей работы при помощи объектов-стратегий, которые используются во время обхода контента: фильтрация ссылок(lmbUriFilter), нормализация ссылок(lmbUriNormalizer), фильтрация контента(lmbContentTypeFilter)
- Каждый из компонентов, в свою, очередь также настраивается, например, для lmbUriFilter ставятся параметры привязки только к конкретному хосту(allowHost), использования только http протокола(allowProtocol) и использование любых путей(регулярное выражение ~.*~ в allowPathRegex). Методы, на мой взгляд, говорят о своем предназначении.
- lmbWebSpider поддерживает интерфейс Observerable, т.е он позволяет зарегистрировать слушателей, которым посылается сообщение при обходе каждой новой страницы
- Именно слушатель lmbSearchIndexingObserver делегирует работу по индексации контента индексатору %%ZendSearchIndexer%%
Использование из командной строки этого шелл скрипта крайне простое:
$ php indexer.php http://mysite.com
При выполнении скрипт выводит информацию о том, какие именно страницы обходятся в данный момент. Если все нормально, должно появится нечто похожее на это:
1)started indexing http://mysite.com...done
3)started indexing http://mysite.com/news...done
4)started indexing http://mysite.com/about...done
5)started indexing http://mysite.com/en...done
6)started indexing http://mysite.com/en/news...done
...
===== Класс-индексатор =====
Приведем код индексатора:
**%%src/search/ZendSearchIndexer.class.php%%**
require_once('Zend/Search/Lucene.php');
require_once('src/search/SearchTextTools.class.php');
class ZendSearchIndexer
{
protected $normalizer = null;
protected $left_bound = '';
protected $right_bound = '';
protected $use_noindex = false;
protected $index;
function __construct($normalizer)
{
$this->normalizer = $normalizer;
}
function useNOINDEX($status = true)
{
$this->use_noindex = $status;
}
function index($uri, $content)
{
//получаем содержимое ...
$title = $this->_extractTitle($content);
//вырезаем контент который необходимо необходимо индексировать,
//если стоит опция use_noindex
$content = $this->_getIndexedContent($content);
//нормализуем контент нормализатором(такое вот масло масляное):
//вырезаем теги
$content = $this->normalizer->process($content);
//индексируем контент
$doc = new Zend_Search_Lucene_Document();
$doc->addField(Zend_Search_Lucene_Field::Text('uri', $uri->toString()));
//производим транслитерацию полей из русскоязычного UTF8 в us-ascii
$field = Zend_Search_Lucene_Field::Text('title', SearchTextTools :: sanitize($title));
//увеличиваем релевантность заголовков
$field->boost = 1.5;
$doc->addField($field);
$doc->addField(Zend_Search_Lucene_Field::Text('content', SearchTextTools :: sanitize($content)));
//сохраняем оригинальный заголовок без изменений, в поиске он не участвует!
$doc->addField(Zend_Search_Lucene_Field::Binary('title_orig', $title));
$index = $this->_getIndex();
//индексер от ZendFramework выдает надоедливый notice во время использования iconv,
//избавляемся от него, возможно, стоит просто на время индексации ставить error_level
//без notice
@$index->addDocument($doc);
}
function _getIndex()
{
if(!$this->index)
$this->index = Zend_Search_Lucene::create(LIMB_VAR_DIR . '/search_index');
return $this->index;
}
function _getIndexedContent($content)
{
if(!$this->use_noindex)
return $content;
$regex = '~' .
preg_quote($this->left_bound) .
'(.*?)' .
preg_quote($this->right_bound) .
'~s';
return preg_replace($regex, ' ', $content);
}
function _extractTitle(&$content)
{
$regex = '~([^<]*)~';
if(preg_match($regex, $content, $matches))
return $matches[1];
else
return '';
}
}
?>
Центральным методом индексера является index($uri, $content), в который приходит объект lmbUri и ненормализованный контент(со всеми тегами). В комментариях написано, что именно происходит, остановимся лишь на непонятном аттрибуте $use_noindex и методе %%SearchTextTools :: sanitize(..)%%:
- Индексер позволяет вырезать из контента места, которые не следует индексировать. Например, у каждого сайта есть меню навигации, которое повторяется на каждой странице, индексировать его - совершенно лишнее. Так, если в меню навигации есть пункт "Новости", то при поиске "Новости" отобразятся все страницы! Чтобы не индексировать это меню или другой подобный повторяющийся контент, следует поместить его в дизайне HTML шаблона в пару маркеров: **...**. Таких маркеров можно иметь любое количество в шаблоне. Для активации подобной возможности необходимо вызвать метод useNOINDEX(..), что и происходит в шелл скрипте
- %%SearchTextTools :: sanitize(..)%% производит транслитерацию и нормализацию контента, на выходе получается ascii строка в нижнем регистре("ВаСилий" => "vasiliy"). Zend_Search как-то очень странно работает с utf-8 кодировкой, если ни сказать, что не работает вообще(iconv ругается и ничего не индексируется), хотя в документации и почтовой рассылке утверждается обратное. У нас не было особо времени разбираться, т.к требовалось рабочее решение для одного из проектов, поэтому мы поступили следующим образом: весь utf-8 русскоязычный контент мы транслитерируем в ascii и индексируем именно его. Причем, для того, чтобы выводить оригинальный заголовок, мы его сохраняем без изменений в бинарном неиндексируемом поле 'title_orig'. Как видно, это не лучшее решение, ограничено работой только с русскоязычным и англоязычным контентом, но его вполне хватает. Пока же будем ждать поддержки utf-8 в Zend_Search.
===== Обработка и нормализация контента =====
Транслитерация и очистка текста от не ascii символов производится классом %%SearchTextTools%%:
**%%src/search/SearchTextTools.class.php%%**
= 0x80)
continue;
$res .= $content{$i};
}
return $res;
}
}
?>
Здесь должно быть все понятно, поэтому перейдем сразу к контроллеру
===== Котроллер, осуществляющий поиск =====
**%%src/controller/SearchController.class.php%%**
_getQueryWords()));
$index = Zend_Search_Lucene :: open(LIMB_VAR_DIR . '/search_index');
try
{
$hits = $index->find($query);
}
catch(Zend_Exception $e)
{
$hits = array();
}
$result = array();
foreach($hits as $hit)
{
$result[] = array('id' => $hit->id,
'score' => $hit->score,
'title' => $hit->title_orig,
'uri' => $hit->uri);
}
$this->view->set('search_result', $result);
}
protected function _getQueryWords()
{
$query = $this->request->get('query_string');
return explode(' ', htmlspecialchars($query));
}
}
?>
В контроллере мы получаем строку запроса и обрабатываем ее, т.к индекс у нас хранится в транслите, следовательно и запрос поиска тоже необходимо делать в транслите, что и делается при помощи %%SearchTextTools :: sanitize(..)%%. После этого собственно делаем запрос.
Помните мы хранили бинарную неиндексируемую строку заголовка без изменений? Именно ее мы и возвращаем во view вместо транслителируемой.
===== Несколько финальных замечаний =====
Кроме проблем с индексацией не ascii строк, есть еще одна неприятная особенность - невозможность частичного поиска слова, хотя, возможно мы плохо смотрели документацию Zend_Search.