Plugin de moteur de recherche multi-modèles
Nous vous proposons un plugin pour intégrer un moteur de recherche interne multi-modèles à une application CakePHP. Ses caractéristiques sont les suivantes :
- Utilise les index Full-Text de MySQL.
- Facile à installer : il suffit de télécharger les fichiers et de créer une seule table, quel que soit le nombre de Modèles à indexer.
- Non-intrusif : inutile de créer d’index Full-Text sur les tables existantes.
- Plusieurs mode de recherche : langage naturel, langage naturel avec extension de requête, et recherche booléenne.
1. Installation
1.1. Fichiers
Le code source du plugin Search est hebergé sur Github.com :
http://github.com/kalt/search/tree/master
Deux méthodes pour installer les fichiers : soit en cliquant sur ‘download’ puis en extrayant le répertoire ’search’ vers le répertoire ‘plugins’ de l’application, soit en exécutant la commande suivante (pour les familiers de Git) :
git clone git://github.com/kalt/search.git
1.2. DB table
Nous devons créer une table dans la base de données selon le schéma inclu ici : search/config/sql/search.sql
2. Mise en place
2.1. Modèles
Nous allons lier les modèles que nous voulons indexer au comportement SearchableBehavior, en donnant la liste des champs à indexer.
class Article extends AppModel { var $actsAs = array('Search.Searchable' => array( 'fields' => array('title', 'body') )); }
C’est la seule chose à faire. Dès qu’un enregistrement sera ajouté ou mis à jour, les champs ‘title’ et ‘body’ seront inclus dans l’index de recherche.
2.2. Formulaire de recherche
Créons maintenant un petit formulaire de recherche. La plupart du temps, la meilleure place pour ce genre de fonction est dans le layout commun à toutes les pages du site : le moteur de recherche est ainsi disponible sur le site entier.
Le formulaire doit obligatoirement contenir un champ texte nommé ‘q’ ( »q » pour « query »).
echo $form->create('Search', array('url' => array( 'plugin' => 'search', 'controller' => 'searches', 'action' => 'index' ))); echo $form->input('q', array('label' => 'Mots clés :')); echo $form->end("Rechercher");
2.3. Pages de résultats
Nous allons maitenant créer la vue qui va afficher les résultats paginés de la recherche : {app}/views/plugins/search/searches/index.ctp
Les variables disponibles dans cette vue sont :
$q: les termes recherchés.$data: les enregistrements paginés correspondants à la recherche.
Exemple :
<?php $paginator->options(array('url' => $this->passedArgs)); ?> <h1>Résultats de la recherche</h1> <p>Votre recherche : <?php echo $q; ?></p> <div id="paginator-counter"> <?php echo $paginator->counter(array('format' => "Page %page% sur %pages%, %current% documents sur %count%")); ?> </div> <?php foreach($data as $row): $model_name = key($row); switch($model_name) { case 'Article': $link = $html->link($row['Article']['title'], array( 'plugin' => null, 'controller' => 'articles', 'action' => 'view', $row['Article']['id'] )); $description = $row['Article']['body']; break; case 'Video': $link = $html->link($row['Video']['title'], array( 'plugin' => null, 'controller' => 'videos', 'action' => 'play', $row['Video']['id'] )); $description = $row['Video']['description']; break; } ?> <div class="ressource"> <h2><?php echo $link; ?></h2> <p align="justify"><?php echo $description; ?></p> </div> <?php endforeach; ?> <div class="paging"> <?php echo $paginator->prev('<< '.__('Previous', true));?> | <?php echo $paginator->numbers();?> <?php echo $paginator->next(__('Next', true).' >>');?> </div>
3. Options
3.1. Modes de recherche
Deux types de recherche sont disponibles :
boolean(défaut) : MySQL effectue une recherche booléenne, c’est-à-dire que tous les termes de la recherche doivent être présents dans les résultats.natural: MySQL effectue une recherche en langage naturel et retourne les résultats par ordre décroissant de pertinence.
Plus d’informations sur la recherche en texte intégral sont disponibles sur la documentation de MySQL.
Pour changer de mode, il suffit d’ajouter la ligne suivante dans {app}/config/bootstrap.php
Configure::write('Search.mode', 'natural');
3.2. Extension de requête
Il s’agit d’une option du mode de recherche en langage naturel. Plus d’infos ici : recherche en texte intégral avec extension de requête.
Cette option est par défaut à null : la recherche avec extension de requête sera activée si l’expression recherchée ne contient qu’un seul mot.
Pour changer cette option, il suffit d’ajouter cette ligne dans {app}/config/bootstrap.php
Configure::write('Search.withQueryExpansion', true/false);
Si false, la recherche avec extension ne sera jamais utilisée, quelle que soit l’expression cherchée. Si true, la recherche avec extension sera toujours utilisée.
3.3. Caractères autorisés dans l’expression recherchée
De base, les caractères acceptés sont les lettres et les chiffres. Par défaut, nous avons ajouté l’espace, les lettres accentuées, la cédille et le « o-e collés ».
Il est possible de remplacer cette liste de caractères autorisés en ajoutant cette ligne dans {app}/config/bootstrap.php
Configure::write('Search.allowedChars', array(' '));
Ici par exemple, nous autorisons l’espace ‘ ‘. Au final donc, tous les signes qui ne sont ni des lettres, ni des chiffres, ni un espace, seront supprimés de l’expression à chercher.
4. Réécriture de l’URL
Par défaut, l’URL des résultats de recherche est : /search/searches/mots+cles
Nous pouvons changer cette URL en créant une nouvelle route dans {app}/config/routes.php
Router::connect( '/recherche/*', array('plugin' => 'search', 'controller' => 'searches', 'action' => 'index') );
L’URL deviendra alors /recherche/mots+cles.
5. Indexer des données existantes
Dans le cas où nous souhaiterions installer le plugin dans une application existante dont les tables à indexer contiennent déjà des données, il nous suffit d’ajouter les 3 actions suivantes dans {app}/app_controller.php
/** * Construit l'index des données existantes d'un modèle */ function admin_build_search_index() { $this->autoRender = false; $model =& $this->{$this->modelClass}; if(!isset($model->Behaviors->Searchable)) { echo "Erreur : le modèle {$model->alias} n'est pas lié au SearchableBehavior."; exit; } $data = $model->find('all'); foreach($data as $row) { $model->set($row); $model->Behaviors->Searchable->Search->saveIndex( $model->alias, $model->id, $model->buildIndex() ); } echo "L'index des données du modèle {$model->alias} a été créé."; } /** * Supprime l'index des données d'un modèle */ function admin_delete_search_index() { $this->autoRender = false; $model =& $this->{$this->modelClass}; if(!isset($model->Behaviors->Searchable)) { echo "Erreur : le modèle {$model->alias} n'est pas lié au SearchableBehavior."; exit; } $model->Behaviors->Searchable->Search->deleteAll(array( 'model' => $model->alias )); echo "L'index des données du modèle {$model->alias} a été supprimé."; } /** * Reconstruit l'index des données existantes d'un modèle */ function admin_rebuild_search_index() { $this->admin_delete_search_index(); $this->admin_build_search_index(); }
Ceci fait, nous accèdons à une URL du style /admin/{uncontroleur}/buildsearch_index pour construire l’index des données existantes du modèle de ce contrôleur. Attention, cela peut prendre un certain temps, en fonction du volume de données à indexer.
Toutes les suggestions d’amélioration et les critiques sont les bienvenues dans les commentaires.
Commentaires
1 mai 2009 à 1:32
excellent ! premiers tests avec les options par défaut et ça fonctionne à merveille
je vais avoir l’occasion de tester plus en détail sur un projet en-cours, je vous ferai un retour
ça pouvait pas mieux tomber
merci pour le plugin
1 mai 2009 à 9:33
Merci ! J’attends effectivement les retours avec impatience, notamment sur les modes de recherche. J’ai choisi de mettre le mode booléen par défaut parce que j’ai constaté de meilleurs résultats qu’avec le mode langage naturel, mais c’était sur une seule application…
2 mai 2009 à 11:08
j’ai rajouté dans la vue:
$paginator->options(array(’url’ => $this->passedArgs));
sans ça les liens de pagination n’ont pas les mot clés
2 mai 2009 à 11:21
Merci, j’ai mis l’exemple à jour.
3 mai 2009 à 18:04
Bravo ! heu, sans vouloir me la jouer, le choix de « recherche en langage naturel » me paraît peu heureux… la recherche en langage naturel, c’est pas super au point encore, et en plus, ça n’est pas vraiment ce qu’il y a dans MySql (qui marche moyennement d’ailleurs, on est d’accord).
4 mai 2009 à 9:53
Salut !
Tout d’abord, bravo et merci pour cette excellente initiative. C’est un besoin récurrent dans les projets, et il n’existait pas de solution facilement intégrable dans Cake à ce jour.
Deux petites suggestions : - il serait peut être intéressant d’associer un petit helper chargé de générer la liste de résultats sans avoir à intégrer la gestion des différents modèles dans la vue : autrement dit, générer (par défaut) les urls de résultats en fonction du modèle courant, en ayant la possibilité de forcer le controller (et l’action) dans l’appel du behavior. - le choix d’un shell serait peut être plus judicieux pour lancer/relancer l’indexation, plutot que des actions du app_controller.
Encore bravo en tout cas.
Sébastien
4 mai 2009 à 12:10
@Guillaume : c’est bien ce que j’ai moi-même constaté, et c’est la raison pour laquelle j’ai défini la recherche en mode booléen comme mode de recherche par défaut.
@Sébastien : je ne vois pas trop ce que pourrais apporter un helper, la gestion des différents modèles est totalement dépendante de l’existant, même la génération des liens. Pourrais-tu m’expliquer ta vision plus en détail, acommpagnée d’exemples ?
Pour ce qui est du shell pour la mise à jour des index, c’est une excellente idée, je vais creuser de ce côté.
Merci pour vos retours !
4 mai 2009 à 12:18
Idem de mon côté Je ne comprends pas les résultats avec l’option de recherche langage naturel, bon j’ai testé sur une appli en développement et les tables ne contiennent pas grand chose, mais le mode booléen me convient très bien. A voir avec des vrais données…
11 mai 2009 à 22:34
Enorme le plugin enorme. C’est juste domage que j’ai codé entierement ma fonction de recherche avant de le trouver. Du coup je dois jeter ce que j’ai fait pour prendre la tienne!!
Par contre, une question subsiste :
12 mai 2009 à 6:57
Je ne vois pas comment il serait possible de faire. La méthode renvoie les enregistrements avec un read(), avec le niveau de récursivité par défaut, tu dois donc faire avec ce qui est renvoyé et n’afficher que ce dont tu as besoin.
12 mai 2009 à 8:41
Le problème est que dans certains cas le résultat peut être enorme. J’ai par exemple des objets documents qui sont liés a des objets attachments. Si je ramène toutes les pieces jointes (jusqu’a 10megas) d’un document a chaque fois qu’un utilisateur fais des recherches, je tue mes performances.
Je vais essayer plonger dans ton code (je l’ai pas fait encore car je suis partisan du on ne touche pas quand ça marche). Je pense que un unbind model devrait me permettre d’obtenir des résultats probants.
12 mai 2009 à 14:32
Dans ce cas tu dois modifier le fichier search/controllers/searchcontroller.php, à la fin de l’action index, remplace :
Par :
14 mai 2009 à 10:02
[...] Plugin de moteur de recherche multi-modèles [...]
16 mai 2009 à 20:40
J’utilise le système multilingue. L’utilisation de ce controller pose-t-il un problème pour la recherche dans les différentes langues ? Si oui, existe-t-il une solution ?
Merci
17 mai 2009 à 11:05
Le plugin n’est pas adapté au multilingue, mais il suffit de mettre un index Full-Text sur la colonne « content » de la table i18n et d’effectuer la recherche dessus.
4 juin 2009 à 13:34
Juste pour savoir comment devrais-je nommer la table necessaire pour le moteur de recherche?
Est-ce que c’est qui choisit mon propre nom? Car je n’ai pas compris la phrase suivante:
« Nous devons créer une table dans la base de données selon le schéma inclu ici : search/config/sql/search.sql ».
Merci d’avance!
4 juin 2009 à 14:24
La table doit s’appeler ’search_index’. ’search/config/sql/search.sql’ contient la définition de la table et de ses colonnes.
4 juin 2009 à 15:47
Est ce que tu connaitrais un moyen / outil pour faire de la recherche full-text dans des documents ? Pour l’instant j’utilise ton plugin de recherche, mais j’aimerai l’etendre pour lui permettre de rechercher aussi dans les documents. A ce propos, j’ai fait le choix de stocker les dits documents dans la base afin de pouvoir rechercher dedans, mais je ne sais pas ce que ca va donner et ca risque de me couter cher en taille de la base. Je suis entrain de reconsidérer la solution du stockage filesystem, mais je pressens de plus grosses difficultés encore pour rechercher dedans.
Si tu as un avis sur ces deux questions …
5 juin 2009 à 14:04
Bonjour, Je suis dans la situation suivante: les tables de ma bases de données ne sont vides et donc contiennent des données. Mais je ne comprend pas comment indexer mes tables pour la recherche s’effectue en ciblant le contenu de mes tables.
De plus je ne comprend pas ceci: » - quel doit etre le chemin pour « {app}/app_controller.php »?
. »Ceci fait, nous accèdons à une URL du style /admin/{uncontroleur}/buildsearch_index pour construire l’index des données existantes du modèle de ce contrôleur. Attention, cela peut prendre un certain temps, en fonction du volume de données à indexer. »!
Merci d’avance!
5 juin 2009 à 15:24
Tu dois copier les 3 actions de la fin du tutoriel dans ton fichier appcontroller.php, qui doit se trouver à la racine de ton application. Ceci fait, tu n’as plus qu’à te rendre à l’URL suivante, si par exemple ton controller s’appelle « Articles » : /admin/articles/buildsearch_index.
26 juin 2009 à 20:10
Bonsoir en fait il m’affiche l’erreur suivante:
Missing Controller
Error: AdminController could not be found.
Error: Create the class AdminController below in file: app\controllers\admin_controller.php
Notice: If you want to customize this error message, create app\views\errors\missing_controller.ctp
Que dois-je faire?
27 juin 2009 à 14:17
Tu dois activer le routage admin en décommentant la ligne suivante dans {app}/config/core.php :
28 juin 2009 à 8:29
Grand merci ça marche!
ENcore bravo pour le plugin
10 août 2009 à 14:58
Bonjour, j’ai suivi scrupuleusement tout ce qui est écrit dans l’article, mais un problème subsiste :
lorsque je lance la fonction rebuildsearchindex, la page mouline bien, récupère bien les infos de la table que je veux indexer, mais n’insère rien dans la table search_index.
les messages confirmant la suppression et la création de l’index sont affichés.
Une idée ?
4 septembre 2009 à 1:35
Hi,
Thanks for making this plugin – I wish I saw it 3 hours ago!
It worked for me almost first time, but I noticed that the rebuildsearchindex action wasn’t quite working correctly when I attached the behaviour to a plugin model. Basically, it was saving ‘model’ into the search_index table instead of ‘Plugin.model’
Here’s my simple fix and good luck in next year’s world cup from an Englishman. (You’re going to struggle without Zidane and that clown Gallas in your team I think!)
17 septembre 2009 à 13:22
Ca marche ! Merci !
Par contre une petite question : j’ai besoin de rechercher mes données à partir de mots-clés (pour cela j’utilise votre plugin), mais également à partir de dates, d’id d’utilisateurs et autres données non textuelles.
Comment faire pour sélectionner d’abord mes données d’après les paramètres non textuels, et ensuite faire une recherche par mots-clés sur le résultat de la première sélection en utilisant votre plugin ?
j’espère avoir été compréhensible.
Vincent
25 septembre 2009 à 13:11
J’ai le même souci que Gabriel, à savoir la non population de la table search_index lors d’une construction ou reconstruction d’index.
Je travaille sur une application existante et des tables déjà remplies.
Merci d’avance pour votre aide.
5 octobre 2009 à 14:36
Est-il possible d’avoir un tuto pour implémenter l’auto-completion ajax dans ce plugin de recherche ?
14 octobre 2009 à 12:48
Bjr, j’ai un petit pb d’url avec le plugin due à mon inexpérience. ma page est http://www.monsite.fr/articles. A prtir de cette page je lance une recherche. Mais je me retrouve avec des Url du style http://www.monsite.fr/search/articles/ Toutes mes Url dans mon layout sont passées de monsite.fr/articles en monsite.fr/search/articles. Comment faire pour éviter cela et garder mes Url ? Merci
14 octobre 2009 à 13:38
@dorio: Tout à fait juste, j’ai oublié de le préciser. Il faut ajouter la clé de tableau
'plugin' => nulldans tous les liens du layout, car si l’on se trouve dans une des pages gérées par le plugin, le layout pense que tous les liens vont vers le plugin… Exemple :