Moteur de recherche avec Zend Search Lucene

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" /> &nbsp;
</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 :

Ajouter d'une page à Zend Search Lucene

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

Recherche d'une page avec Zend Search Lucene

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.

Ressouces complémentaires

Zend Framework , Permalien.

Une réponse pour Moteur de recherche avec Zend Search Lucene

  1. hind14 says:

    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 .

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">