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 :

yum install xapian-core xapian-bindgins-python

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 :

...
import os,sys
settings_path = os.path.abspath(os.path.dirname(__file__))
head, tail = os.path.split(settings_path)

INSTALLED_APPS = (
....
'haystack',
)
HAYSTACK_SITECONF = '%s.search_sites' % tail
HAYSTACK_SEARCH_ENGINE = 'xapian'
HAYSTACK_XAPIAN_PATH = os.path.join(head,'index')

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) :

import haystack
haystack.autodiscover()

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):

django-admin.py startapp videos

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

from django.db import models

class Tag(models.Model):
    """ """
    name = models.CharField(max_length=50)
    slug = models.SlugField(max_length=50,unique=True)

class Video(models.Model):
    """ """
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200)
    description = models.TextField(null=True, blank=True)
    tag = models.ManyToManyField(Tag,null=True)
On va aussi indexer le modèle django.contrib.auth.models.User .
Maintenant, le fichier videos/search_indexes.py :
#-*- coding:utf-8 -*-
from haystack.indexes import SearchIndex,CharField,MultiValueField
from haystack import site
from models import Video
from django.contrib.auth.models import User
class VideoIndex(SearchIndex):
   """ """
   text = CharField(document=True, use_template=True,template_name='haystack/video.txt')
   name = CharField(model_attr='name')
   tags = MultiValueField()
   description = CharField(model_attr='description',null=True)

   def prepare_tags(self,obj):
      """ """
      return [tag.name for tag in obj.tag.all() ]
def get_queryset(self):
      """ """
      return Video.objects.filter(published=True)
class UserIndex(SearchIndex):
   """ """
   text = CharField(document=True, use_template=True,template_name='haystack/user.txt')
   username = CharField(model_attr='username')
   first_name = CharField(model_attr='first_name')
   last_name = CharField(model_attr='last_name')

   def get_queryset(self):
      """ """
      return User.objects.filter(is_active=True)
site.register(Video, VideoIndex)
site.register(User, UserIndex)

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 :
{{object.name}}
{{object.description}}
et videos/templates/haystack/user.txt :
{{object.username}}
{{object.first_name}}
{{object.last_name}}
Ne reste plus qu’à indexer :
python manage.py rebuild_index
Et pour mettre à jour :
python manage.py update_index
Ne nous reste plus qu’à créer le formulaire de recherche.
C’est assez simple :
- projet/urls.py :
urlpatterns = patterns('',
   (r'^$','videos.views.search',{},'search_url'),
),

- videos/views.py :

#-*- coding:utf-8 -*-
from haystack.query import SearchQuerySet
from videos.models import Video

from django.contrib.auth.models import User
from django.shortcuts import render_to_response
from django.template import RequestContext

def search(request):
    """ """
    results = []
    if request.method=="POST":
        type_result = request.POST.get('select','all')
        query = request.POST.get('query','')
        if type_result == 'all':
            results = SearchQuerySet().auto_query(query)
        elif type_result == 'user':
            results = SearchQuerySet().models(User).auto_query(query)
        elif type_result == 'video':
            results = SearchQuerySet().models(Video).auto_query(query)
    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 :
<form action="{% url search_url %}" method="POST" id="search_form">
    {% csrf_token %}
	<p><input type="text" id="query" name="query" /></p>
    <p>
       <select id="select_type" name="select">
        <option value="all">Tout</option>
        <option value="user">Utilisateurs</option>
        <option value="video">Vidéos</option>
       </select>
    </p>
	<p><input type="submit" value="ok" />
</form>

{% if results %}
    {% for result in results %}
        <p>{{result.text|safe}}</p>
    {% endfor %}
{% endif %}
and Voilà !

Leave a Reply