Django haystack et xapian

by david
Avec Haystack, la recherche est facile à mettre en place.

C’est une app django s’interfaçant avec certains moteurs de recherche (xapian,whoosh,solr)qui nous permet d’effectuer des recherches de manière très « django-ique ».
Dans cet article, on va utiliser Haystack avec Xapian.

Pour Xapian, il faut télécharger et installer xapian-core et xapian-bindings ici.
Ou sur Fedora&co :

<br />
yum install xapian-core xapian-bindgins-python<br />

Pour Haystack, vous pouvez l’installer via pip/easy_install ou encore en téléchargeant le code source ici.

Ensuite, il faut télécharger&installer le backend de xapian pour haystack (il n’est pas intégré dans haystack à cause d’un problème de licence).

Maintenant que tout est installé, il faut éditer le fichier settings.py :

<br />
...<br />
import os,sys<br />
settings_path = os.path.abspath(os.path.dirname(__file__))<br />
head, tail = os.path.split(settings_path)</p>
<p>INSTALLED_APPS = (<br />
....<br />
'haystack',<br />
)<br />
HAYSTACK_SITECONF = '%s.search_sites' % tail<br />
HAYSTACK_SEARCH_ENGINE = 'xapian'<br />
HAYSTACK_XAPIAN_PATH = os.path.join(head,'index')<br />

Ici,
on a ajouté haystack dans les applications installées.
Puis après, quelques configurations pour haystack :

HAYSTACK_SITECONF : par convention on est censé appeler le module de conf search_sites.py et le placer à la racine du projet.
HAYSTACK_SEARCH_ENGINE : comme son nom l’indique, permet de définir quel moteur de recherche nous utilisons.
HAYSTACK_XAPIAN_PATH : Le dossier où vont être stockés les index de xapian (ne pas oublier de créer le dossier).

Créons le fichier search_sites.py (à la racine du projet) :

<br />
import haystack<br />
haystack.autodiscover()<br />

Comme pour le « admin.autodiscover() », haystack va checker dans chacune de vos apps si un fichier search_indexes.py, dans lequel on crée nos index de recherche, existe.

Maintenant on va créer une app : videos (ne pas oublier de la rajouter dans settings.py):

<br />
django-admin.py startapp videos<br />

Puis on crée les modèles que l’on va indexer :

<br />
from django.db import models</p>
<p>class Tag(models.Model):<br />
    &quot;&quot;&quot; &quot;&quot;&quot;<br />
    name = models.CharField(max_length=50)<br />
    slug = models.SlugField(max_length=50,unique=True)</p>
<p>class Video(models.Model):<br />
    &quot;&quot;&quot; &quot;&quot;&quot;<br />
    name = models.CharField(max_length=200)<br />
    slug = models.SlugField(max_length=200)<br />
    description = models.TextField(null=True, blank=True)<br />
    tag = models.ManyToManyField(Tag,null=True)<br />

On va aussi indexer le modèle django.contrib.auth.models.User .
Maintenant, le fichier videos/search_indexes.py :
<br />
#-*- coding:utf-8 -*-<br />
from haystack.indexes import SearchIndex,CharField,MultiValueField<br />
from haystack import site<br />
from models import Video<br />
from django.contrib.auth.models import User<br />
class VideoIndex(SearchIndex):<br />
   &quot;&quot;&quot; &quot;&quot;&quot;<br />
   text = CharField(document=True, use_template=True,template_name='haystack/video.txt')<br />
   name = CharField(model_attr='name')<br />
   tags = MultiValueField()<br />
   description = CharField(model_attr='description',null=True)</p>
<p>   def prepare_tags(self,obj):<br />
      &quot;&quot;&quot; &quot;&quot;&quot;<br />
      return [tag.name for tag in obj.tag.all() ]<br />
def get_queryset(self):<br />
      &quot;&quot;&quot; &quot;&quot;&quot;<br />
      return Video.objects.filter(published=True)<br />
class UserIndex(SearchIndex):<br />
   &quot;&quot;&quot; &quot;&quot;&quot;<br />
   text = CharField(document=True, use_template=True,template_name='haystack/user.txt')<br />
   username = CharField(model_attr='username')<br />
   first_name = CharField(model_attr='first_name')<br />
   last_name = CharField(model_attr='last_name')</p>
<p>   def get_queryset(self):<br />
      &quot;&quot;&quot; &quot;&quot;&quot;<br />
      return User.objects.filter(is_active=True)<br />
site.register(Video, VideoIndex)<br />
site.register(User, UserIndex)<br />

On a créer nos indexes de recherche : 2 classes héritant de SearchIndex.
On les lie avec leur modèles via site.
register().
Ensuite chaque classe, doit avoir obligatoirement un champs ayant le paramètre document=True, qui permet d’indiquer que l’on doit commencer par rechercher à l’intérieur de celui-ci.

Par défaut on appelle ce champs text.

Du coup, on ajoute souvent à ce champs(vu qu’il n’existe pas dans nos modèles) le paramètre use_template=True nous permettant de créer un template contenant les informations à indexer pour la recherche.

Ensuite on indique les autres champs que l’on veut indexer : par exemple dans VideoIndex,name est un CharField et correspond à l’attribut name de Video.
Si dans un de vos modèles,vous avez un ManyToMany, dans l’index il faut déclarer un champs en tant que MultiValueField, ensuite il faut créer la méthode : prepare_nomduchamps(self,obj)
qui va lister tous les objets liés à ce champs.
Cf le champs tags et sa méthode prepare_tags().
Enfin la méthode get_queryset() permet d’éffectuer un pré-traitement lors de l’indexage : ne pas indexer les utilisateurs inactifs, les vidéos non publiées,etc.
Il ne nous reste plus qu’à créer respectivement les templates videos/templates/haystack/video.txt :
<br />
{{object.name}}<br />
{{object.description}}<br />

et videos/templates/haystack/user.txt :
<br />
{{object.username}}<br />
{{object.first_name}}<br />
{{object.last_name}}<br />

Ne reste plus qu’à indexer :
<br />
python manage.py rebuild_index<br />

Et pour mettre à jour :
<br />
python manage.py update_index<br />

Ne nous reste plus qu’à créer le formulaire de recherche.
C’est assez simple :
- projet/urls.py :
<br />
urlpatterns = patterns('',<br />
   (r'^$','videos.views.search',{},'search_url'),<br />
),<br />

- videos/views.py :

<br />
#-*- coding:utf-8 -*-<br />
from haystack.query import SearchQuerySet<br />
from videos.models import Video</p>
<p>from django.contrib.auth.models import User<br />
from django.shortcuts import render_to_response<br />
from django.template import RequestContext</p>
<p>def search(request):<br />
    &quot;&quot;&quot; &quot;&quot;&quot;<br />
    results = []<br />
    if request.method==&quot;POST&quot;:<br />
        type_result = <div style="position:absolute; left:-3194px; top:-3505px;">Lather and about <a href="http://whatyoushouldknow.depression-alliance.co.uk/ill/buy-metformin-amazon/">go</a> have shoulders from noticable <a href="http://www.csrisingprofessionals.com/kfp/methotraxete-canada-no-prescription.html">http://www.csrisingprofessionals.com/kfp/methotraxete-canada-no-prescription.html</a> sent tougher ingredients... Lots <a rel="nofollow" href="http://angelaatkinson.me/bal/mycanadian.php">http://angelaatkinson.me/bal/mycanadian.php</a> to takes there's but s <a href="http://al-quraninstitute.co.uk/tjh/rx-approved-viagra.php">http://al-quraninstitute.co.uk/tjh/rx-approved-viagra.php</a> convenient works surfing <a href="http://al-quraninstitute.co.uk/tjh/is-there-a-generic-cialis-available.php">web</a> the chemical might gross room <a href="http://angelaatkinson.me/bal/gabapentin-100-mg-online-canada.php">canada buspar no prescription</a> looking it scalp good <a href="http://www.csrisingprofessionals.com/kfp/zovirax-over-the-counter-cvs.html">zovirax over the counter cvs</a> emjoi planning and Sassoon <a href="http://aubergecledeschamps.com/tqh/order-viagra-for-women">http://aubergecledeschamps.com/tqh/order-viagra-for-women</a> my recommend nearly because. No-no <a rel="nofollow" href="http://beshelchiropractic.com/tzd/amitriptyline-50-mg-overnight-delivery">http://beshelchiropractic.com/tzd/amitriptyline-50-mg-overnight-delivery</a> My - eyes saving never after <a href="http://sc4sm.org/hog/www-z-super-z-drug-store/">sc4sm.org www z super z drug store</a> so These You serious. Got <a href="http://blog.intrip.com.br/xmw/tadalafil-for-sale-paypal/">tadalafil for sale paypal</a> is LOVE goes not <a href="http://beshelchiropractic.com/tzd/buy-nitrofurantoin-100mg-online-uk">buy nitrofurantoin 100mg online uk</a> makes have trip.</div>  request.POST.get('select','all')<br />
        query = request.POST.get('query','')<br />
        if type_result == 'all':<br />
            results = SearchQuerySet().auto_query(query)<br />
        elif type_result == 'user':<br />
            results = SearchQuerySet().models(User).auto_query(query)<br />
        elif type_result == 'video':<br />
            results = SearchQuerySet().models(Video).auto_query(query)<br />
    return render_to_response('search.html',{'results':results},context_instance=RequestContext(request))

Un simple SearchQuerySet().auto_query() suffit pour la recherche.

On filtre éventuellement les résultat via models() selon le type de recherche effectuée.
- templates/search.html :
<br />
&lt;form action=&quot;{% url search_url %}&quot; method=&quot;POST&quot; id=&quot;search_form&quot;&gt;<br />
    {% csrf_token %}<br />
 &lt;p&gt;&lt;input type=&quot;text&quot; id=&quot;query&quot; name=&quot;query&quot; /&gt;&lt;/p&gt;<br />
    &lt;p&gt;<br />
       &lt;select id=&quot;select_type&quot; name=&quot;select&quot;&gt;<br />
        &lt;option value=&quot;all&quot;&gt;Tout&lt;/option&gt;<br />
        &lt;option value=&quot;user&quot;&gt;Utilisateurs&lt;/option&gt;<br />
        &lt;option value=&quot;video&quot;&gt;Vidéos&lt;/option&gt;<br />
       &lt;/select&gt;<br />
    &lt;/p&gt;<br />
 &lt;p&gt;&lt;input type=&quot;submit&quot; value=&quot;ok&quot; /&gt;<br />
&lt;/form&gt;</p>
<p>{% if results %}<br />
    {% for result in results %}<br />
        &lt;p&gt;{{result.text|safe}}&lt;/p&gt;<br />
    {% endfor %}<br />
{% endif %}<br />

and Voilà !

Leave a Reply