Objectif
Penchons nous ici sur le composant Zend Search Lucene, moteur de recherche de contenu distribué au sein de Zend Framework. Ce composant est implémenté en PHP 5 et est un dérivé du projet Apache Lucene.
Ce moteur indexe différents types de documents (html, word, excel, etc) sur le système de fichiers et ne requiert pas de base de données, il offre les fonctionnalités suivantes :
- Recherche par pertinence (Ranked searching) ;
- Nombreux types de requêtes : phrase, booléen, astérisque, proximité, intervalle, etc ;
- Recherche sur un champ spécifique (titre, auteur, etc).
Pour illustrer son fonctionnement, réalisons une application qui permet :
- d’indexer des pages html identifiées par leurs urls ;
- d’effectuer des recherches fulltext simples sur ces documents.
Créer la structure du projet
Pour commencer, télécharger la librairie ZendFramework (ici 1.7.8 Minimal), puis créer l’arborescence et les fichiers suivants en plaçant le contenu de l’archive téléchargé dans le répertoire library :
+--libre-a-vous-search | +--application | |-- bootstrap.php | | | +--controllers | | |--ErrorController.php | | |--IndexController.php | | | +--views | +--scripts | +--error | | |--error.phtml | | | +--index | |--add.phtml | |--index.phtml +--library | + Zend (extraire l'archive téléchargée ici) | +--public | - index.php | +--var
Contrôleur frontal
Editons le fichier /public/index.php :
<?php<?php // définir le répertoire de l'application define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application/')); // définir le path du futur index lucène define('LUCENE_INDEX', realpath(dirname(__FILE__).'/../var/').'/index'); // définir le path du fichier de log define('LOG_PATH', realpath(dirname(__FILE__).'/../var/').'/system.log'); // ajouter le framework dans l'include path set_include_path( APPLICATION_PATH . '/../library'. PATH_SEPARATOR. get_include_path() ); // configurer l'autoloading pour charger les classes sans include ou require require_once "Zend/Loader.php"; Zend_Loader::registerAutoload(); // ajouter le bootstrap pour définir la configuration de l'application require '../application/bootstrap.php'; // préparer la capture et le routage des requêtes Zend_Controller_Front::getInstance()->dispatch();
Bootstrap
Editons le fichier /application/boostrap.php :
<?php // définir le répertoire de l'application defined('APPLICATION_PATH') or define('APPLICATION_PATH', dirname(__FILE__)); // récupérer l'instance du contrôleur frontal (singleton) $frontController = Zend_Controller_Front::getInstance(); // définir le répertoire des contrôleurs $frontController->setControllerDirectory(APPLICATION_PATH . '/controllers'); // définir l'environnement de l'application defined('APPLICATION_ENVIRONMENT') or define( 'APPLICATION_ENVIRONMENT', 'development' ); $frontController->setParam('env', APPLICATION_ENVIRONMENT); // enlever les variables du bootstrap de la portée globale unset($frontController);
Gestion des erreurs
Ajoutons le contrôleur prenant en charge les erreurs dans /application/controllers/ErrorController.php :
<?php class ErrorController extends Zend_Controller_Action { /** * One ne rentre pas dans le détail ici, ce n'est pas l'objet du billet :) */ public function errorAction() { $this->_helper->viewRenderer->setViewSuffix('phtml'); $errors = $this->_getParam('error_handler'); switch ($errors->type) { case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_CONTROLLER: case Zend_Controller_Plugin_ErrorHandler::EXCEPTION_NO_ACTION: $this->getResponse()->setHttpResponseCode(404); $this->view->message = 'Page not found'; break; default: $this->getResponse()->setHttpResponseCode(500); $this->view->message = 'Application error'; break; } $this->view->env = $this->getInvokeArg('env'); $this->view->exception = $errors->exception; $this->view->request = $errors->request; } }
Définissons la vue présentant les erreurs dans /application/views/scripts/error/error.phtml :
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Moteur de recherche avec Zend Lucène (libre-a-vous.fr)</title>
</head>
<body>
<h1>Une erreur est survenue</h1>
<h2><?= $this->message ?></h2>
<? if ('development' == $this->env): ?>
<h3>Information relative à l'exception:</h3>
<p><b>Message:</b> <?= $this->exception->getMessage() ?></p>
<h3>Trace d'exécution:</h3>
<p><?= $this->exception->getTraceAsString() ?></p>
<h3>Paramètres de la requête:</h3>
<p><? var_dump($this->request->getParams()) ?></p>
<? endif ?>
</body>
</html>Contrôleur par défaut
Ajoutons notre contrôleur par défaut dans /application/controllers/IndexController.php :
<?php /** * Contrôleur par défaut de l'application */ class IndexController extends Zend_Controller_Action { /** * Action par défaut du contrôleur, nous listons ici les résultats d'une * recherche */ public function indexAction() { // récupérer la requête $query = $this->getRequest()->getParam('requete'); if ($query) { // exécuter la requête $index = $this->_getLuceneIndex(); $hits = $index->find($query); // retourner les résultats à la vue $this->view->query = $query; if (!empty($hits)) { $this->view->results = $hits; } } } /** * Action d'ajout d'une URL, nous indexons la page html ciblée */ public function addAction() { // vérifier que des données ont été postées if ($url = $this->getRequest()->getPost('url')) { $index = $this->_getLuceneIndex(); $log = $this->_getLogger(); // désindexer la page s'il l'url est connue $term = new Zend_Search_Lucene_Index_Term($url, 'url'); $exactSearchQuery = new Zend_Search_Lucene_Search_Query_Term($term); $hits = $index->find($exactSearchQuery); if (count($hits) > 0) { foreach ($hits as $hit) { $index->delete($hit->id); $log->info("La page $url a été désindexée"); } } // préparer le client HTTP $client = new Zend_Http_Client(); $client->setConfig(array('timeout' => 30)); $client->setUri($url); // récupérer la page $response = $client->request(); if ($response->isSuccessful()) { // créer le document html standard Lucène $body = $response->getBody(); $doc = Zend_Search_Lucene_Document_Html::loadHTML($body); // ajouter le champ url $doc->addField(Zend_Search_Lucene_Field::Keyword('url', $url)); // indexer le document $index->addDocument($doc); $log->info("La page $url a bien été indexée"); $this->view->message = "La page $url a bien été indexée !"; $log->info("Optimisation de l'index..."); // optimiser l'index et sauvegarder les modifications $index->optimize(); $index->commit(); $log->info( "L'index contient désormais ".$index->numDocs(). " documents" ); } } } /** * Retourne l'index Lucène, le créer si nécessaire */ private function _getLuceneIndex() { $log = $this->_getLogger(); $path = LUCENE_INDEX; try { $index = Zend_Search_Lucene::open($path); $log->info("Ouverture d'un index existant : $path"); } catch (Zend_Search_Lucene_Exception $e) { try { $index = Zend_Search_Lucene::create($path); $log->info("Création d'un nouvel index : $path"); } catch(Zend_Search_Lucene_Exception $e) { $log->error("Impossible d'ouvrir ou créer un index $path"); $log->error($e->getMessage()); echo "Impossible d'ouvrir ou créer un index:". "{$e->getMessage()}"; exit(1); } } return $index; } /** * Retourne le journaliseur */ private function _getLogger() { return new Zend_Log(new Zend_Log_Writer_Stream(LOG_PATH)); } }
Vue d’ajout d’une URL
Dans /application/views/scripts/index/add.phtml :
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Moteur de recherche avec Zend Lucène (libre-a-vous.fr)</title>
</head>
<body>
<h1>Indexer une nouvelle page</h1>
<form method="post" action="">
<input type="text" name="url"></input>
<input type="submit" value="Indexer la page"/>
</form>
<?php if (isset($this->message)): ?>
<p style="color:green"><?= $this->message ?></p>
<?php endif; ?>
</body>
</html>Vue de la recherche
Dans /application/views/scripts/index/index.phtml :
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Moteur de recherche avec Zend Lucène (libre-a-vous.fr)</title>
</head>
<body>
<h1>Effectuer une recherche</h1>
<form>
<label>Saisir vos mots-clés :</label>
<input type="text" name="requete" value="<?=isset($this->query) ?
$this->escape($this->query) : '' ?>" />
<input type="submit" value="Rechercher" />
</form>
<?php if (isset($this->results) && ! empty($this->results)): ?>
<h2>Résultats</h2>
<ul>
<?php foreach($this->results as $hit): ?>
<li>
<a href="<?= $hit->url ?>"><?=$this->escape($hit->title) ?>
<?= sprintf("%0.2f", $hit->score * 100) ?>%</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</body>
</html>Tester l’application
L’ajout de page est accessible à l’adresse : http://votre-hôte/libre-a-vous-search/index.php/index/add :

La recherche est accessible à l’adresse : http://votre-hôte/libre-a-vous-search/index.php/index/index :

Pour allez plus loin
Comme évoqué précédemment, Zend Search Lucene permet d’indexer de nombreux types de documents ou encore de décrire vos propres documents avec leur sémantique propre afin d’effectuer des requêtes plus poussées sur les champs définis.
Il est aisé de pondérer la recherche sur les différents champs, une requête du type suivant précise que l’expression trouvé dans le titre compte trois fois plus que dans le contenu de la page :
$query = "title:$requete^3 body:$requete^1";
Pour traiter les mots qui compose la recherche et non l’expression entière, Il suffit de découper la chaîne afin de construire une requête du type :
$query = "title:zend^3 title:lucene^3 body:zend^1 body:lucene^1";
Enfin, l’avantage indéniable de ce composant est d’être aisément intégrable (pas besoin d’inclure la totalité du framework Zend) au sein d’une application web ou d’un site existant pour l’enrichir d’une fonctionnalité de recherche avancée.
Le code source de cet exemple est sous licence GNU GPL et est téléchargeable ici, libre à vous de le ré-utiliser ou de l’adapter à vos besoins.
Je voudrai intégrer un moteur de recherche à mon site web , j’ai pensé à utiliser zend_search_lucene en se basant sur ce tuto j’ai réussi à faire une recherche sur les link ajoutés mais je voudrai pouvoir gérer mes liens ( supprimer des liens déjà ajoutés )
le plus important pour moi c’est pouvoir indexer les documents pdf .