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 :26 décembre 2009 à 11:46
Merci beaucoup pour ce plugin et cette doc.
En essayant de le mettre en place j’ai noté plusieurs erreurs. Voici les modifs que je propose :
…la vue qui va afficher les résultats paginés de la recherche : {app}/views/plugins/search/views/searches/index.ctp
Nous devons créer une table dans la base de données selon le schéma inclu ici : search/config/search.sql
Ceci fait, nous accèdons à une URL du style /admin/{uncontroleur}/buildsearchindex
Voila ! En espérant que ca aidera quelques un
21 février 2010 à 14:29
Superbe plugin qui fonctionne magistralement. Un vrai bonheur, merci !
Je me demandais simplement comment ajouter aux données indexées les données issue de modèles liés.
Par exemple, j’aimerais indexer mes Posts et les Tags auxquels les posts sont liés. De cette façon, une recherche porterait non seulement sur les titres et contenus des Posts, mais aussi sur les mots clés associés.
Amicalement, alan
30 avril 2010 à 15:35
Bonjour,
Ce plugin fonctionne très bien. Mais j’aurais une question :
« Comment pourrait-on étendre la recherche ? »
Je m’explique, actuellement si on tape ‘objet’, le plugin retournera le mot ‘objet’ et pas le mot ‘objets’. Pour cela il faudrait modifier la requête avec des ‘%’, mais je ne sais pas ou le faire.
Merci pour vos réponses.
30 avril 2010 à 16:32
C’est justement le but de l’option ‘withQueryExpansion’. La recherche Full Text de mysql mise en place ici ne fonctionne pas comme un LIKE ‘expression%’.
7 mai 2010 à 16:38
Salut à tous, merci pour ce super plugin. J’aurai quelques questions:
1- le ‘withQueryExpansion’ doit fonctionner avec le mode ‘natural’ ou peut fonctionner avec le mode boolean ?? En mettant withQueryExpansion à true, et en laissant le mode boolean si je cherche « info » il me renvoi tous les enregistrement ou y a « info » mais si je met « infos » alors il ne me renvoi rien !
2- Comment faire pour avoir un tri par pertinence avec le mode boolean ??
21 juin 2010 à 15:07
Merci pour ce plugin au code très propre.
J’ai pris l’initiative d’y rajouter un mode « patternMatching » pour pouvoir faire des recherches approximatives ; pour ceux que ça intéresserait, le code suivant est à rajouter dans le switch($this->mode) de la fonction index() du fichier controllers/searches_controller.php du plugin :
Personnellement je n’ai rien trouvé à mettre dans la fonction prepareForPatternMatchingMode(), mais je la garde au cas où. On pourrait aller encore plus loin en distinguant les cas « %motcle » et « mot_cle% », je vous laisse le soin d’adapter à vos besoins.
15 juillet 2010 à 22:49
Bonjour,
Ca fait moins d’une semaine que je suis sur CakePHP alors je vais peut-être dire quelque chose de trivial, mais j’ai eu des soucis d’URL après avoir intégré le plugin. Il a fallut que j’efface tous les fichiers de mon dossier temporaire « tmp ». Et tout à refonctionné !
Merci pour le partage.
1 octobre 2010 à 10:18
Bonjour, Encore une fois voilà un excellent article, pour ma part cela a marché du premier coup… ENORME. Je l’utilise pour un site qui contient beaucoup d’articles du coup beaucoup de recherches, pour le moment je n’ai pas trop poussé mes investigations mais il a l’air de bien fonctionner, bravo, un grand merci. Il ne me reste plus qu’à faire un peu de mise en page et roulez jeunesse
Continuez comme ça!!!
1 octobre 2010 à 10:33
Re si un petit soucis en fait le résultat de la recherche n’apparait pas dans mon layout principal… du coup plus de charte juste l’affichage du résultat, quelqu’un a une idée
6 octobre 2010 à 10:54
Bonjour,
Ce tutoriel est très bien fait! Une amélioration certaine serait d’ajouter dans la function buildIndex(&$model) du behaviors searchable des conditions d’indexations.
Une appli qui a une partie admin peut avoir par exemple des articles en cours de rédaction, proposés à la publication, ou publiés.
Donc indexer si ‘conditions’=>array(‘Article.status’=>’publié’)
En effet, dans la config actuelle le behavior index tout…
Qu’en pensez-vous?
7 novembre 2010 à 8:28
nice plugin. just wanted to know can this be done with related models like tags ? not sure how to integrat it into the same model
6 décembre 2010 à 8:02
Je plussoie le commentaire précédent !
Et même si quelqu’un pouvait m’expliquer comment ajouter une telle condition en hard code, je suis preneur.
Amicalement, Alan
6 décembre 2010 à 11:45
Ai trouvé comment résoudre le problème soulevé par Cakephp Ethno Urban Webdesign.
1) Supprimer la fonction afterSave() et la fonction beforeDelete() de /app/plugins/searche/models/behavios/searchable.php.
2) Dans l’action modifiant le statut de l’article en « publié », ajouter :
3) Dans l’action modifiant le statut de l’article en autre chose que « publié », ainsi que dans l’action d’effacement de l’article, ajouter :
Enjoy.
alan
8 janvier 2011 à 15:15
Bonjour, super tuto
je tourne sur cake 1.3 en ce moment et je n’arrive pas a faire fonctionner la « queryextension ». J’ai un mot qui est heures … et lorsque je tape heure dans la recherche il ne trouve rien
Un peu d’aide ?
15 janvier 2011 à 18:14
Bonjour,
Le plugin fonctionne très bien à l’exception d’un détail :
Si je me rend à l’adresse monsite.com/fr/recherche/acdc ou monsite.com/en/search/acdc cela fonctionne, en revanche quand on passe par le formulaire de recherche il affiche monsite.com/search/searches/index/acdc.
Voici le code de mon formulaire :
et la partie concernée dans mon routes.php :
J’ai cherché a faire passer la variable dans le formulaire histoire de voir si cake pouvait utiliser le reverse routing mais sans succès. Je m’en remets donc à vous =)
Cordialement
18 février 2011 à 10:59
Slt, Nice plugin il m’a fait gagner un temps fou, mais j’ai quand même un petit problème sur le mode boolean Si dans le moteur de recherche je tape télé tout fonctionne bien par contre si je tape des mots de 3 lettres (genre LCD) il ne me retourne rien… Quelqu’un à t il déjà eu le pb?
Thks
9 mars 2011 à 16:48
Bonjour je viens d’installer ce plugin, il n’indexe les insertions et mises à jour que pour un seul modèle alors que je l’ai placé sur plusieurs, quelqu’un a t-il eu ce petit problème? merci
13 juillet 2011 à 9:49
pareil que pour antoine et je n’arrve pas a faire des recherches approximatives
si je tape camion il me retourne bien mais si je tape camions rien…
27 juillet 2011 à 22:53
Bonjour,
C’est mon premier commentaire sur ce site que je consulte très souvent, j’en profite donc pour féliciter et remercier son brillant auteur !!!
Je vais probablement mettre place le plugin de recherche multi-modèle décrit ici mais j’ai une simple question : mon projet actuel propose un champ de recherche dynamique en AJAX (un auto-complete) mais qui bien entendu ne propose les résultats au fur et à mesure de la saisie que sur un seul modèle. Pensez-vous qu’en combinant les deux il soit possible de renvoyer les résultats du plugin multi-modèle dans les suggestions de l’auto-complete ? D’instinct je dirais oui, mais au cas où ?
D’avance, merci !