Utilisation de sphinx pour la recherche texte.
authorEric Mc Sween <eric.mcsween@gmail.com>
Mon, 22 Nov 2010 06:11:48 +0000 (01:11 -0500)
committerEric Mc Sween <eric.mcsween@gmail.com>
Mon, 22 Nov 2010 06:11:48 +0000 (01:11 -0500)
18 files changed:
auf_savoirs_en_partage/chercheurs/forms.py
auf_savoirs_en_partage/chercheurs/models.py
auf_savoirs_en_partage/savoirs/admin.py
auf_savoirs_en_partage/savoirs/forms.py
auf_savoirs_en_partage/savoirs/lib/recherche.py
auf_savoirs_en_partage/savoirs/models.py
auf_savoirs_en_partage/savoirs/templatetags/sep.py
auf_savoirs_en_partage/savoirs/views.py
auf_savoirs_en_partage/scripts/sphinx.conf.py.in [new file with mode: 0644]
auf_savoirs_en_partage/settings.py
auf_savoirs_en_partage/sitotheque/forms.py
auf_savoirs_en_partage/sitotheque/models.py
auf_savoirs_en_partage/sitotheque/views.py
auf_savoirs_en_partage/templates/savoirs/actualite_resultat.html
auf_savoirs_en_partage/templates/savoirs/evenement_resultat.html
auf_savoirs_en_partage/templates/savoirs/ressource_resultat.html
auf_savoirs_en_partage/templates/sites/resultat.html
buildout.cfg

index 7ac665d..0f5d0d2 100644 (file)
@@ -283,40 +283,39 @@ class RepertoireSearchForm (forms.Form):
             pays.queryset = pays.queryset.filter(region=region)
 
     def get_query_set(self):
-        qs = Chercheur.objects.all()
+        chercheurs = Chercheur.objects
         if self.is_valid():
+            q = self.cleaned_data["q"]
+            if q:
+                chercheurs = chercheurs.search(q)
             nom = self.cleaned_data['nom']
             if nom:
-                qs = qs.search_nom(nom)
-            domaine = self.cleaned_data["domaine"]
-            if domaine:
-                qs = qs.filter(groupes=domaine)
+                chercheurs = chercheurs.add_to_query('@(nom,prenom) ' + nom)
             groupe_recherche = self.cleaned_data['groupe_recherche']
             if groupe_recherche:
-                for word in groupe_recherche.split():
-                    qs = qs.filter(groupe_recherche__icontains=word)
-            q = self.cleaned_data["q"]
-            if q:
-                qs = qs.search(q)
-            statut = self.cleaned_data["statut"]
-            if statut:
-                if statut == "expert":
-                    qs = qs.exclude(expertises=None)
-                else:
-                    qs = qs.filter(statut=statut)
+                chercheurs = chercheurs.add_to_query('@groupe_recherche ' + groupe_recherche)
             discipline = self.cleaned_data['discipline']
             if discipline:
-                qs = qs.filter_discipline(discipline)
+                chercheurs = chercheurs.filter_discipline(discipline)
             region = self.cleaned_data['region']
             if region:
-                qs = qs.filter_region(region)
+                chercheurs = chercheurs.filter_region(region)
+            statut = self.cleaned_data["statut"]
+            if statut:
+                if statut == "expert":
+                    chercheurs = chercheurs.filter_expert()
+                else:
+                    chercheurs = chercheurs.filter_statut(statut)
+            domaine = self.cleaned_data["domaine"]
+            if domaine:
+                chercheurs = chercheurs.filter_groupe(domaine)
             pays = self.cleaned_data["pays"]
             if pays:
-                qs = qs.filter(Q(etablissement__pays=pays) | Q(etablissement_autre_pays=pays))
+                chercheurs = chercheurs.filter_pays(pays)
             nord_sud = self.cleaned_data['nord_sud']
             if nord_sud:
-                qs = qs.filter(Q(etablissement__pays__nord_sud=nord_sud) | Q(etablissement_autre_pays__nord_sud=nord_sud))
-        return qs
+                chercheurs = chercheurs.filter_nord_sud(nord_sud)
+        return chercheurs.all()
     
 class SendPasswordForm(forms.Form):
     email = forms.EmailField(required=True, label="Adresse électronique")
index 4b486db..69d414b 100644 (file)
@@ -1,11 +1,11 @@
 # -*- encoding: utf-8 -*-
 import hashlib
+from datamaster_modeles.models import *
 from django.db import models
 from django.db.models import Q
 from django.utils.encoding import smart_str
-from datamaster_modeles.models import *
-#from auf_references_modeles.models import Thematique
-from savoirs.models import Discipline, RandomQuerySetMixin
+from djangosphinx.models import SphinxSearch
+from savoirs.models import Discipline, SEPManager, SEPSphinxQuerySet, SEPQuerySet
 
 GENRE_CHOICES = (('m', 'Homme'), ('f', 'Femme'))
 class Personne(models.Model):
@@ -47,100 +47,78 @@ class Utilisateur(Personne):
     def get_new_password_code(self):
         return hashlib.md5(smart_str(u.courriel+u.encrypted_password)).hexdigest()[0:6]
 
-class ChercheurManager(models.Manager):
+class ChercheurQuerySet(SEPQuerySet):
+
+    def filter_groupe(self, groupe):
+        return self.filter(groupes=groupe)
+
+    def filter_pays(self, pays):
+        return self.filter(Q(etablissement__pays=pays) | Q(etablissement_autre_pays=pays))
+
+    def filter_region(self, region):
+        return self.filter(Q(etablissement__pays__region=region) | Q(etablissement_autre_pays__region=region))
+
+    def filter_nord_sud(self, nord_sud):
+        return self.filter(Q(etablissement__pays__nord_sud=nord_sud) | Q(etablissement_autre_pays__nord_sud=nord_sud))
+
+    def filter_statut(self, statut):
+        return self.filter(statut=statut)
+
+    def filter_expert(self):
+        return self.exclude(expertises=None)
+
+class ChercheurSphinxQuerySet(SEPSphinxQuerySet):
+
+    def __init__(self, model=None):
+        return SEPSphinxQuerySet.__init__(self, model=model, index='chercheurs',
+                                          weights=dict(nom=2, prenom=2))
+
+    def filter_region(self, region):
+        return self.filter(region_id=region.id)
+
+    def filter_groupe(self, groupe):
+        return self.filter(groupe_ids=groupe.id)
+
+    def filter_pays(self, pays):
+        return self.filter(pays_id=pays.id)
+
+    NORD_SUD_CODES = {'Nord': 1, 'Sud': 2}
+    def filter_nord_sud(self, nord_sud):
+        return self.filter(nord_sud=self.NORD_SUD_CODES[nord_sud])
+
+    STATUT_CODES = {'enseignant': 1, 'etudiant': 2, 'independant': 3}
+    def filter_statut(self, statut):
+        return self.filter(statut=self.STATUT_CODES[statut])
+
+    def filter_expert(self):
+        return self.filter(expert=1)
+
+class ChercheurManager(SEPManager):
 
     def get_query_set(self):
         return ChercheurQuerySet(self.model)
 
-    def search(self, text):
-        return self.get_query_set().search(text)
-
-    def search_nom(self, nom):
-        return self.get_query_set().search_nom(nom)
+    def get_sphinx_query_set(self):
+        return ChercheurSphinxQuerySet(self.model).order_by('-date_modification')
 
     def filter_region(self, region):
+        """Le filtrage de chercheurs par région n'est pas une recherche texte."""
         return self.get_query_set().filter_region(region)
 
-    def filter_discipline(self, discipline):
-        return self.get_query_set().filter_discipline(discipline)
-
-class ChercheurQuerySet(models.query.QuerySet, RandomQuerySetMixin):
-
-    def search(self, text):
-        q = None
-        for word in text.split():
-            matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
-            matching_etablissements = list(Etablissement.objects.filter(Q(nom__icontains=word) | Q(pays__in=matching_pays)).values_list('pk', flat=True))
-            matching_publications = list(Publication.objects.filter(titre__icontains=word).values_list('pk', flat=True))
-            matching_groupes = list(Groupe.objects.filter(nom__icontains=word).values_list('pk', flat=True))
-            matching_disciplines = list(Discipline.objects.filter(nom__icontains=word).values_list('pk', flat=True))
-            part = (Q(personne__nom__icontains=word) |
-                    Q(personne__prenom__icontains=word) |
-                    Q(theme_recherche__icontains=word) |
-                    Q(etablissement__in=matching_etablissements) |
-                    Q(etablissement_autre_nom__icontains=word) |
-                    Q(etablissement_autre_pays__in=matching_pays) |
-                    Q(discipline__in=matching_disciplines) |
-                    Q(groupe_recherche__icontains=word) |
-                    Q(publication1__in=matching_publications) |
-                    Q(publication2__in=matching_publications) |
-                    Q(publication3__in=matching_publications) |
-                    Q(publication4__in=matching_publications) |
-                    Q(these__in=matching_publications) |
-                    Q(groupes__in=matching_groupes) |
-                    Q(expertises__nom__icontains=word) |
-                    Q(mots_cles__icontains=word) |
-                    Q(membre_association_francophone_details__icontains=word) |
-                    Q(membre_reseau_institutionnel_details__icontains=word)
-                   )
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        return self.filter(q).distinct() if q is not None else self
-
-    def search_nom(self, nom):
-        q = None
-        for word in nom.split():
-            part = Q(personne__nom__icontains=word) | Q(personne__prenom__icontains=word)
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        return self.filter(q) if q is not None else self
-
-    def filter_discipline(self, discipline):
-        """Ne conserve que les chercheurs dans la discipline donnée.
-           
-        Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-        if discipline is None:
-            return self
-        if not isinstance(discipline, Discipline):
-            discipline = Discipline.objects.get(pk=discipline)
-        return self.filter(Q(discipline=discipline) |
-                           Q(theme_recherche__icontains=discipline.nom) |
-                           Q(groupe_recherche__icontains=discipline.nom) |
-                           Q(publication1__titre__icontains=discipline.nom) |
-                           Q(publication2__titre__icontains=discipline.nom) |
-                           Q(publication3__titre__icontains=discipline.nom) |
-                           Q(publication4__titre__icontains=discipline.nom) |
-                           Q(these__titre__icontains=discipline.nom) |
-                           Q(groupes__nom__icontains=discipline.nom) |
-                           Q(expertises__nom__icontains=discipline.nom) |
-                           Q(mots_cles__icontains=discipline.nom) |
-                           Q(membre_instance_auf_details__icontains=discipline.nom) |
-                           Q(membre_association_francophone_details__icontains=discipline.nom) |
-                           Q(expert_oif_details__icontains=discipline.nom) |
-                           Q(membre_reseau_institutionnel_details__icontains=discipline.nom)).distinct()
+    def filter_groupe(self, groupe):
+        return self.get_query_set().filter_groupe(groupe)
 
-    def filter_region(self, region):
-        """Ne conserve que les évènements dans la région donnée.
-           
-        Si ``region`` est None, ce filtre n'a aucun effet."""
-        if region is None:
-            return self
-        return self.filter(Q(etablissement__pays__region=region) |
-                           Q(etablissement_autre_pays__region=region))
+    def filter_pays(self, pays):
+        return self.get_query_set().filter_pays(pays)
+
+    def filter_nord_sud(self, nord_sud):
+        return self.get_query_set().filter_nord_sud(nord_sud)
+
+    def filter_statut(self, statut):
+        return self.get_query_set().filter_statut(statut)
+
+    def filter_expert(self):
+        return self.get_query_set().filter_expert()
 
 STATUT_CHOICES = (('enseignant', 'Enseignant-chercheur dans un établissement'), ('etudiant', 'Étudiant-chercheur doctorant'), ('independant', 'Chercheur indépendant docteur'))
 class Chercheur(models.Model):
@@ -223,6 +201,7 @@ class Chercheur(models.Model):
     
     # Manager
     objects = ChercheurManager()
+    all_objects = models.Manager()
 
     def __unicode__(self):
         return u"%s %s" % (self.personne.nom.upper(), self.personne.prenom.title())
index 345a2e6..e517083 100644 (file)
@@ -3,6 +3,7 @@ import re
 
 from django.core.urlresolvers import reverse as url
 from django.db import models
+from django.db.models.query import QuerySet
 from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User
@@ -11,9 +12,8 @@ from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext as _
 from django.utils.encoding import smart_unicode, iri_to_uri
 from django.http import HttpResponseRedirect
-
-from models import SourceActualite, Actualite, Discipline, Evenement, Record, ListSet, HarvestLog, Profile
 from savoirs.globals import META
+from savoirs.models import SourceActualite, Actualite, Discipline, Evenement, Record, ListSet, HarvestLog, Profile
 
 admin.site.register(SourceActualite)
 
@@ -82,6 +82,41 @@ class ReadOnlyAdminFields(object):
                     form.base_fields[field_name].required = False
         return form
 
+class RecordAdminQuerySet(QuerySet):
+
+    def filter(self, *args, **kwargs):
+        """Gère des filtres supplémentaires pour l'admin.
+           
+        C'est la seule façon que j'ai trouvée de contourner les mécanismes
+        de recherche de l'admin."""
+        search = kwargs.pop('admin_search', None)
+        search_titre = kwargs.pop('admin_search_titre', None)
+        search_sujet = kwargs.pop('admin_search_sujet', None)
+        search_description = kwargs.pop('admin_search_description', None)
+        search_auteur = kwargs.pop('admin_search_auteur', None)
+
+        if search:
+            qs = self
+            search_all = not (search_titre or search_description or search_sujet or search_auteur)
+            fields = []
+            if search_titre or search_all:
+                fields += ['title', 'alt_title']
+            if search_description or search_all:
+                fields += ['description', 'abstract']
+            if search_sujet or search_all:
+                fields += ['subject']
+            if search_auteur or search_all:
+                fields += ['creator', 'contributor']
+
+            for bit in search.split():
+                or_queries = [Q(**{field + '__icontains': bit}) for field in fields]
+                qs = qs.filter(reduce(operator.or_, or_queries))
+
+            if args or kwargs:
+                qs = super(RecordAdminQuerySet, qs).filter(*args, **kwargs)
+            return qs
+        else:
+            return super(RecordAdminQuerySet, self).filter(*args, **kwargs)
 
 class RecordAdmin(ReadOnlyAdminFields, admin.ModelAdmin):
     fields = ['server', 'title', 'creator', 'description', 'modified',
@@ -108,7 +143,8 @@ class RecordAdmin(ReadOnlyAdminFields, admin.ModelAdmin):
         self.readonly_fields.append('listsets')
         super(RecordAdmin, self).__init__(*args, **kwargs) 
 
-    # Recherche par mots-clés
+    def queryset(self):
+        return RecordAdminQuerySet(Record)
 
     # Présentation de l'information
     
@@ -152,7 +188,6 @@ class RecordAdmin(ReadOnlyAdminFields, admin.ModelAdmin):
         selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
         return HttpResponseRedirect(url('assigner_disciplines', kwargs=dict(app_name='savoirs', model_name='record')) + '?ids=' + ','.join(selected))
     assigner_disciplines.short_description = u'Assigner des disciplines'
-
 admin.site.register(Record, RecordAdmin)
 
 class ListSetAdmin(ReadOnlyAdminFields, admin.ModelAdmin):
index 6620a1d..8d445d4 100644 (file)
@@ -42,37 +42,30 @@ class RecordSearchForm(forms.Form):
     def get_query_set(self):
         """Retourne l'ensemble des ressources qui correspondent aux valeurs
            entrées dans le formulaire."""
-        records = Record.objects.validated()
+        records = Record.objects
         if self.is_valid():
-            query = self.cleaned_data['q']
-            if query:
-                records = records.search(query)
+            q = self.cleaned_data['q']
+            if q:
+                records = records.search(q)
             auteur = self.cleaned_data['auteur']
             if auteur:
-                records = records.search_auteur(auteur)
+                records = records.add_to_query('@(creator,contributor) ' + auteur)
             titre = self.cleaned_data['titre']
             if titre:
-                records = records.search_titre(titre)
+                records = records.add_to_query('@title ' + titre)
             sujet = self.cleaned_data['sujet']
             if sujet:
-                records = records.search_sujet(sujet)
+                records = records.add_to_query('@subject ' + sujet)
             publisher = self.cleaned_data['publisher']
             if publisher:
-                for word in publisher.split():
-                    records = records.filter(publisher__icontains=word)
+                records = records.add_to_query('@publisher ' + publisher)
             discipline = self.cleaned_data['discipline']
             if discipline:
                 records = records.filter_discipline(discipline)
             region = self.cleaned_data['region']
             if region:
                 records = records.filter_region(region)
-        return records
-
-    def get_search_regexp(self):
-        """Retourne une expression régulière compilée qui peut servir à
-           chercher les mot-clés recherchés dans un texte."""
-        if self.is_valid():
-            return build_search_regexp(self.cleaned_data['q'])
+        return records.all()
 
 class ActualiteSearchForm(forms.Form):
     """Formulaire de recherche pour les actualités."""
@@ -87,31 +80,25 @@ class ActualiteSearchForm(forms.Form):
     def get_query_set(self):
         """Retourne l'ensemble des actualités qui correspondent aux valeurs
            entrées dans le formulaire."""
-        actualites = Actualite.objects.filter(visible=True)
+        actualites = Actualite.objects
         if self.is_valid():
-            query = self.cleaned_data['q']
-            if query:
-                actualites = actualites.search(query)
-            date_min = self.cleaned_data['date_min']
-            if date_min:
-                actualites = actualites.filter(date__gte=date_min)
-            date_max = self.cleaned_data['date_max']
-            if date_max:
-                actualites = actualites.filter(date__lte=date_max)
+            q = self.cleaned_data['q']
+            if q:
+                actualites = actualites.search(q)
             discipline = self.cleaned_data['discipline']
             if discipline:
                 actualites = actualites.filter_discipline(discipline)
             region = self.cleaned_data['region']
             if region:
                 actualites = actualites.filter_region(region)
-        return actualites
+            date_min = self.cleaned_data['date_min']
+            if date_min:
+                actualites = actualites.filter_date(min=date_min)
+            date_max = self.cleaned_data['date_max']
+            if date_max:
+                actualites = actualites.filter_date(max=date_max)
+        return actualites.all()
     
-    def get_search_regexp(self):
-        """Retourne une expression régulière compilée qui peut servir à
-           chercher les mot-clés recherchés dans un texte."""
-        if self.is_valid():
-            return build_search_regexp(self.cleaned_data['q'])
-
 class EvenementSearchForm(forms.Form):
     """Formulaire de recherche pour les évènements."""
 
@@ -127,36 +114,30 @@ class EvenementSearchForm(forms.Form):
     def get_query_set(self):
         """Retourne l'ensemble des évènements qui correspondent aux valeurs
            entrées dans le formulaire."""
-        evenements = Evenement.objects.filter(approuve=True)
+        evenements = Evenement.objects
         if self.is_valid():
             query = self.cleaned_data['q']
             if query:
                 evenements = evenements.search(query)
             titre = self.cleaned_data['titre']
             if titre:
-                evenements = evenements.search_titre(titre)
-            type = self.cleaned_data['type']
-            if type:
-                evenements = evenements.filter(type=type)
-            date_min = self.cleaned_data['date_min']
-            if date_min:
-                evenements = evenements.filter(debut__gte=date_min)
-            date_max = self.cleaned_data['date_max']
-            if date_max:
-                evenements = evenements.filter(debut__lte=date_max)
+                evenements = evenements.add_to_query('@titre ' + titre)
             discipline = self.cleaned_data['discipline']
             if discipline:
                 evenements = evenements.filter_discipline(discipline)
             region = self.cleaned_data['region']
             if region:
                 evenements = evenements.filter_region(region)
-        return evenements
-
-    def get_search_regexp(self):
-        """Retourne une expression régulière compilée qui peut servir à
-           chercher les mot-clés recherchés dans un texte."""
-        if self.is_valid():
-            return build_search_regexp(self.cleaned_data['q'])
+            type = self.cleaned_data['type']
+            if type:
+                evenements = evenements.filter_type(type)
+            date_min = self.cleaned_data['date_min']
+            if date_min:
+                evenements = evenements.filter_debut(min=date_min)
+            date_max = self.cleaned_data['date_max']
+            if date_max:
+                evenements = evenements.filter_debut(max=date_max)
+        return evenements.all()
 
 ###
 
index 65804a8..eda784f 100644 (file)
@@ -2,6 +2,7 @@
 import urllib, httplib, time, simplejson, pprint, math, re
 from django.core.urlresolvers import reverse
 from django.conf import settings
+from django.utils.safestring import mark_safe
 from auf_savoirs_en_partage.backend_config import RESOURCES
 from sep import SEP
 from utils import smart_str
@@ -182,7 +183,7 @@ def build_search_regexp(query):
 
         # Faire ceci après avoir traité les caractères accentués...
         part = part.replace('a', u'[aàâÀÂ]')
-        part = part.replace('e', u'[eéèëêÉÊ]')
+        part = part.replace('e', u'[eéèëêÉÊÈ]')
         part = part.replace('i', u'[iïîÎ]')
         part = part.replace('o', u'[oôÔ]')
         part = part.replace('u', u'[uûüù]')
@@ -190,3 +191,14 @@ def build_search_regexp(query):
 
         parts.append(part)
     return re.compile('|'.join(parts), re.I) 
+
+def excerpt_function(manager, words):
+    """Construit une fonction qui extrait la partie pertinente d'un texte
+       suite à une recherche textuelle."""
+    qs = manager.get_sphinx_query_set()
+    client = qs._get_sphinx_client()
+    index = qs._index
+    def excerpt(text):
+        return mark_safe(client.BuildExcerpts([text], index, words)[0])
+    return excerpt
+
index 82fc289..a073f5d 100644 (file)
@@ -1,16 +1,29 @@
 # -*- encoding: utf-8 -*-
-import simplejson, uuid, datetime, caldav, vobject, uuid, random, operator, pytz, os
+import caldav
+import datetime
+import operator
+import os
+import pytz
+import random
+import simplejson
+import time
+import uuid
+import vobject
+from backend_config import RESOURCES
 from babel.dates import get_timezone_name
+from caldav.lib import error
+from datamaster_modeles.models import Thematique, Pays, Region
 from django.contrib.auth.models import User
 from django.db import models
 from django.db.models import Q, Max
 from django.db.models.signals import pre_delete
-from auf_savoirs_en_partage.backend_config import RESOURCES
+from django.utils.encoding import smart_unicode
+from djangosphinx.models import SphinxQuerySet
 from savoirs.globals import META
+from savoirs.lib.calendrier import combine
 from settings import CALENDRIER_URL, SITE_ROOT_URL
-from datamaster_modeles.models import Thematique, Pays, Region
-from lib.calendrier import combine
-from caldav.lib import error
+
+# Fonctionnalités communes à tous les query sets
 
 class RandomQuerySetMixin(object):
     """Mixin pour les modèles.
@@ -25,6 +38,59 @@ class RandomQuerySetMixin(object):
         positions = random.sample(xrange(count), min(n, count))
         return [self[p] for p in positions]
 
+class SEPQuerySet(models.query.QuerySet, RandomQuerySetMixin):
+    pass
+
+class SEPSphinxQuerySet(SphinxQuerySet, RandomQuerySetMixin):
+    """Fonctionnalités communes aux query sets de Sphinx."""
+
+    def __init__(self, model=None, index=None, weights=None):
+        SphinxQuerySet.__init__(self, model=model, index=index,
+                                mode='SPH_MATCH_EXTENDED2',
+                                rankmode='SPH_RANK_PROXIMITY_BM25',
+                                weights=weights)
+
+    def add_to_query(self, query):
+        """Ajoute une partie à la requête texte."""
+        new_query = smart_unicode(self._query) + ' ' + query if self._query else query
+        return self.query(new_query)
+
+    def search(self, text):
+        """Recherche ``text`` dans tous les champs."""
+        return self.add_to_query('@* ' + text)
+
+    def filter_discipline(self, discipline):
+        """Par défaut, le filtre par discipline cherche le nom de la
+           discipline dans tous les champs."""
+        return self.search('"%s"' % discipline.nom)
+
+    def filter_region(self, region):
+        """Par défaut, le filtre par région cherche le nom de la région dans
+           tous les champs."""
+        return self.search('"%s"' % region.nom)
+
+class SEPManager(models.Manager):
+    """Lorsque les méthodes ``search``, ``filter_region`` et
+       ``filter_discipline`` sont appelées sur ce manager, le query set
+       Sphinx est créé, sinon, c'est le query set Django qui est créé."""
+
+    def query(self, query):
+        return self.get_sphinx_query_set().query(query)
+
+    def add_to_query(self, query):
+        return self.get_sphinx_query_set().add_to_query(query)
+
+    def search(self, text):
+        return self.get_sphinx_query_set().search(text)
+
+    def filter_region(self, region):
+        return self.get_sphinx_query_set().filter_region(region)
+
+    def filter_discipline(self, discipline):
+        return self.get_sphinx_query_set().filter_discipline(discipline)
+
+# Disciplines
+
 class Discipline(models.Model):
     id = models.IntegerField(primary_key=True, db_column='id_discipline')
     nom = models.CharField(max_length=765, db_column='nom_discipline')
@@ -36,6 +102,8 @@ class Discipline(models.Model):
         db_table = u'discipline'
         ordering = ["nom",]
 
+# Actualités
+
 class SourceActualite(models.Model):
     nom = models.CharField(max_length=255)
     url = models.CharField(max_length=255)
@@ -43,56 +111,40 @@ class SourceActualite(models.Model):
     def __unicode__(self,):
         return u"%s" % self.nom
 
-class ActualiteManager(models.Manager):
-    
-    def get_query_set(self):
-        return ActualiteQuerySet(self.model)
+class ActualiteQuerySet(SEPQuerySet):
 
-    def search(self, text):
-        return self.get_query_set().search(text)
+    def filter_date(self, min=None, max=None):
+        qs = self
+        if min:
+            qs = qs.filter(date__gte=min)
+        if max:
+            qs = qs.filter(date__lte=max)
+        return qs
 
-    def filter_region(self, region):
-        return self.get_query_set().filter_region(region)
+class ActualiteSphinxQuerySet(SEPSphinxQuerySet):
 
-    def filter_discipline(self, discipline):
-        return self.get_query_set().filter_discipline(discipline)
+    def __init__(self, model=None):
+        SEPSphinxQuerySet.__init__(self, model=model, index='actualites',
+                                   weights=dict(titre=3))
 
-class ActualiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
+    def filter_date(self, min=None, max=None):
+        qs = self
+        if min:
+            qs = qs.filter(date__gte=min.toordinal()+365)
+        if max:
+            qs = qs.filter(date__lte=max.toordinal()+365)
+        return qs
 
-    def search(self, text):
-        q = None
-        for word in text.split():
-            part = (Q(titre__icontains=word) | Q(texte__icontains=word) |
-                    Q(regions__nom__icontains=word) | Q(disciplines__nom__icontains=word))
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        return self.filter(q).distinct() if q is not None else self
+class ActualiteManager(SEPManager):
+    
+    def get_query_set(self):
+        return ActualiteQuerySet(self.model).filter(visible=True)
 
-    def filter_discipline(self, discipline):
-        """Ne conserve que les actualités dans la discipline donnée.
-           
-        Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-        if discipline is None:
-            return self
-        if not isinstance(discipline, Discipline):
-            discipline = Discipline.objects.get(pk=discipline)
-        return self.filter(Q(disciplines=discipline) |
-                           Q(titre__icontains=discipline.nom) |
-                           Q(texte__icontains=discipline.nom)).distinct()
+    def get_sphinx_query_set(self):
+        return ActualiteSphinxQuerySet(self.model).order_by('-date')
 
-    def filter_region(self, region):
-        """Ne conserve que les actualités dans la région donnée.
-           
-        Si ``region`` est None, ce filtre n'a aucun effet."""
-        if region is None:
-            return self
-        if not isinstance(region, Region):
-            region = Region.objects.get(pk=region)
-        return self.filter(Q(regions=region) |
-                           Q(titre__icontains=region.nom) |
-                           Q(texte__icontains=region.nom)).distinct()
+    def filter_date(self, min=None, max=None):
+        return self.get_query_set().filter_date(min=min, max=max)
 
 class Actualite(models.Model):
     id = models.AutoField(primary_key=True, db_column='id_actualite')
@@ -107,6 +159,7 @@ class Actualite(models.Model):
     regions = models.ManyToManyField(Region, blank=True, related_name="actualites", verbose_name='régions')
 
     objects = ActualiteManager()
+    all_objects = models.Manager()
 
     class Meta:
         db_table = u'actualite'
@@ -121,73 +174,51 @@ class Actualite(models.Model):
     def assigner_regions(self, regions):
         self.regions.add(*regions)
 
-class EvenementManager(models.Manager):
+# Agenda
 
-    def get_query_set(self):
-        return EvenementQuerySet(self.model)
+class EvenementQuerySet(SEPQuerySet):
 
-    def search(self, text):
-        return self.get_query_set().search(text)
+    def filter_type(self, type):
+        return self.filter(type=type)
 
-    def filter_region(self, region):
-        return self.get_query_set().filter_region(region)
+    def filter_debut(self, min=None, max=None):
+        qs = self
+        if min:
+            qs = qs.filter(debut__gte=min)
+        if max:
+            qs = qs.filter(debut__lt=max+datetime.timedelta(days=1))
+        return qs
 
-    def filter_discipline(self, discipline):
-        return self.get_query_set().filter_discipline(discipline)
+class EvenementSphinxQuerySet(SEPSphinxQuerySet):
 
-class EvenementQuerySet(models.query.QuerySet, RandomQuerySetMixin):
+    def __init__(self, model=None):
+        SEPSphinxQuerySet.__init__(self, model=model, index='evenements',
+                                   weights=dict(titre=3))
 
-    def search(self, text):
-        q = None
-        for word in text.split():
-            part = (Q(titre__icontains=word) | 
-                    Q(mots_cles__icontains=word) |
-                    Q(discipline__nom__icontains=word) | 
-                    Q(discipline_secondaire__nom__icontains=word) |
-                    Q(type__icontains=word) |
-                    Q(lieu__icontains=word) |
-                    Q(description__icontains=word) |
-                    Q(contact__icontains=word) |
-                    Q(regions__nom__icontains=word))
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        return self.filter(q).distinct() if q is not None else self
-
-    def search_titre(self, text):
+    def filter_type(self, type):
+        return self.add_to_query('@type "%s"' % type)
+    
+    def filter_debut(self, min=None, max=None):
         qs = self
-        for word in text.split():
-            qs = qs.filter(titre__icontains=word)
+        if min:
+            qs = qs.filter(debut__gte=min.toordinal()+365)
+        if max:
+            qs = qs.filter(debut__lte=max.toordinal()+365)
         return qs
 
-    def filter_discipline(self, discipline):
-        """Ne conserve que les évènements dans la discipline donnée.
-           
-        Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-        if discipline is None:
-            return self
-        if not isinstance(discipline, Discipline):
-            discipline = Discipline.objects.get(pk=discipline)
-        return self.filter(Q(discipline=discipline) |
-                           Q(discipline_secondaire=discipline) |
-                           Q(titre__icontains=discipline.nom) |
-                           Q(mots_cles__icontains=discipline.nom) |
-                           Q(description__icontains=discipline.nom))
+class EvenementManager(SEPManager):
 
-    def filter_region(self, region):
-        """Ne conserve que les évènements dans la région donnée.
-           
-        Si ``region`` est None, ce filtre n'a aucun effet."""
-        if region is None:
-            return self
-        if not isinstance(region, Region):
-            region = Region.objects.get(pk=region)
-        return self.filter(Q(regions=region) |
-                           Q(titre__icontains=region.nom) |
-                           Q(mots_cles__icontains=region.nom) |
-                           Q(description__icontains=region.nom) |
-                           Q(lieu__icontains=region.nom)).distinct()
+    def get_query_set(self):
+        return EvenementQuerySet(self.model).filter(approuve=True)
+
+    def get_sphinx_query_set(self):
+        return EvenementSphinxQuerySet(self.model).order_by('-debut')
+
+    def filter_type(self, type):
+        return self.get_query_set().filter_type(type)
+
+    def filter_debut(self, min=None, max=None):
+        return self.get_query_set().filter_debut(min=min, max=max)
 
 def build_time_zone_choices():
     fr_names = set()
@@ -212,7 +243,7 @@ class Evenement(models.Model):
                     (u'Conférence', u'Conférence'),
                     (u'Appel à contribution', u'Appel à contribution'),
                     (u'Journée d\'étude', u'Journée d\'étude'),
-                    (None, u'Autre'))
+                    (u'None', u'Autre'))
     TIME_ZONE_CHOICES = build_time_zone_choices()
 
     uid = models.CharField(max_length=255, default=str(uuid.uuid1()))
@@ -236,6 +267,7 @@ class Evenement(models.Model):
     regions = models.ManyToManyField(Region, blank=True, related_name="evenements", verbose_name='régions')
 
     objects = EvenementManager()
+    all_objects = models.Manager()
 
     class Meta:
         ordering = ['-debut']
@@ -350,15 +382,14 @@ class Evenement(models.Model):
             self.discipline = disciplines[0]
             self.discipline_secondaire = disciplines[1]
 
-
-# Surcharge du comportement de suppression
-# La méthode de connexion par signals est préférable à surcharger la méthode delete()
-# car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
 def delete_vevent(sender, instance, *args, **kwargs):
+    # Surcharge du comportement de suppression
+    # La méthode de connexion par signals est préférable à surcharger la méthode delete()
+    # car dans le cas de la suppression par lots, cell-ci n'est pas invoquée
     instance.delete_vevent()
+pre_delete.connect(delete_vevent, sender=Evenement) 
 
-pre_delete.connect(delete_vevent, sender = Evenement) 
-
+# Ressources
 
 class ListSet(models.Model):
     spec = models.CharField(primary_key = True, max_length = 255)
@@ -369,141 +400,24 @@ class ListSet(models.Model):
     def __unicode__(self,):
         return self.name
 
-class RecordManager(models.Manager):
-    
-    def get_query_set(self):
-        return RecordQuerySet(self.model)
-
-    def search(self, text):
-        return self.get_query_set().search(text)
-
-    def validated(self):
-        return self.get_query_set().validated()
+class RecordSphinxQuerySet(SEPSphinxQuerySet):
 
-    def filter_region(self, region):
-        return self.get_query_set().filter_region(region)
-
-    def filter_discipline(self, discipline):
-        return self.get_query_set().filter_discipline(discipline)
+    def __init__(self, model=None):
+        SEPSphinxQuerySet.__init__(self, model=model, index='ressources',
+                                   weights=dict(title=3))
 
-class RecordQuerySet(models.query.QuerySet, RandomQuerySetMixin):
-
-    def search(self, text):
-        qs = self
-        words = text.split()
-
-        # Ne garder que les ressources qui contiennent tous les mots
-        # demandés.
-        q = None
-        for word in words:
-            matching_pays = list(Pays.objects.filter(Q(nom__icontains=word) | Q(region__nom__icontains=word)).values_list('pk', flat=True))
-            part = (Q(title__icontains=word) | Q(description__icontains=word) |
-                    Q(creator__icontains=word) | Q(contributor__icontains=word) |
-                    Q(subject__icontains=word) | Q(disciplines__nom__icontains=word) |
-                    Q(regions__nom__icontains=word) | Q(pays__in=matching_pays) |
-                    Q(publisher__icontains=word))
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        if q is not None:
-            qs = qs.filter(q).distinct()
-
-        # On donne un point pour chaque mot présent dans le titre.
-        if words:
-            score_expr = ' + '.join(['(title LIKE %s)'] * len(words))
-            score_params = ['%' + word + '%' for word in words]
-            qs = qs.extra(
-                select={'score': score_expr},
-                select_params=score_params
-            ).order_by('-score')
-        return qs
-
-    def search_auteur(self, text):
-        qs = self
-        for word in text.split():
-            qs = qs.filter(Q(creator__icontains=word) | Q(contributor__icontains=word))
-        return qs
+class RecordManager(SEPManager):
 
-    def search_sujet(self, text):
-        qs = self
-        for word in text.split():
-            qs = qs.filter(subject__icontains=word)
-        return qs
-
-    def search_titre(self, text):
-        qs = self
-        for word in text.split():
-            qs = qs.filter(title__icontains=word)
-        return qs
-            
-    def filter_discipline(self, discipline):
-        """Ne conserve que les ressources dans la discipline donnée.
-           
-        Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-        if discipline is None:
-            return self
-        if not isinstance(discipline, Discipline):
-            discipline = Discipline.objects.get(pk=discipline)
-        return self.filter(Q(disciplines=discipline) |
-                           Q(title__icontains=discipline.nom) |
-                           Q(description__icontains=discipline.nom) |
-                           Q(subject__icontains=discipline.nom)).distinct()
-
-    def filter_region(self, region):
-        """Ne conserve que les ressources dans la région donnée.
-           
-        Si ``region`` est None, ce filtre n'a aucun effet."""
-        if region is None:
-            return self
-        if not isinstance(region, Region):
-            region = Region.objects.get(pk=region)
-        return self.filter(Q(pays__region=region) |
-                           Q(regions=region) |
-                           Q(title__icontains=region.nom) |
-                           Q(description__icontains=region.nom) |
-                           Q(subject__icontains=region.nom)).distinct()
-
-    def validated(self):
+    def get_query_set(self):
         """Ne garder que les ressources validées et qui sont soit dans aucun
            listset ou au moins dans un listset validé."""
-        qs = self.filter(validated=True)
+        qs = SEPQuerySet(self.model)
+        qs = qs.filter(validated=True)
         qs = qs.filter(Q(listsets__isnull=True) | Q(listsets__validated=True))
         return qs.distinct()
 
-    def filter(self, *args, **kwargs):
-        """Gère des filtres supplémentaires pour l'admin.
-           
-        C'est la seule façon que j'ai trouvée de contourner les mécanismes
-        de recherche de l'admin."""
-        search = kwargs.pop('admin_search', None)
-        search_titre = kwargs.pop('admin_search_titre', None)
-        search_sujet = kwargs.pop('admin_search_sujet', None)
-        search_description = kwargs.pop('admin_search_description', None)
-        search_auteur = kwargs.pop('admin_search_auteur', None)
-
-        if search:
-            qs = self
-            search_all = not (search_titre or search_description or search_sujet or search_auteur)
-            fields = []
-            if search_titre or search_all:
-                fields += ['title', 'alt_title']
-            if search_description or search_all:
-                fields += ['description', 'abstract']
-            if search_sujet or search_all:
-                fields += ['subject']
-            if search_auteur or search_all:
-                fields += ['creator', 'contributor']
-
-            for bit in search.split():
-                or_queries = [Q(**{field + '__icontains': bit}) for field in fields]
-                qs = qs.filter(reduce(operator.or_, or_queries))
-
-            if args or kwargs:
-                qs = super(RecordQuerySet, qs).filter(*args, **kwargs)
-            return qs
-        else:
-            return super(RecordQuerySet, self).filter(*args, **kwargs)
+    def get_sphinx_query_set(self):
+        return RecordSphinxQuerySet(self.model)
 
 class Record(models.Model):
     
@@ -545,8 +459,9 @@ class Record(models.Model):
     pays = models.ManyToManyField(Pays, blank=True)
     regions = models.ManyToManyField(Region, blank=True, verbose_name='régions')
 
-    # Manager
+    # Managers
     objects = RecordManager()
+    all_objects = models.Manager()
 
     class Meta:
         verbose_name = 'ressource'
index 90830f3..8d173ac 100644 (file)
@@ -127,3 +127,8 @@ def change_discipline(path, discipline):
     if not rest.startswith('/recherche'):
         rest = '/'
     return discipline_bit + region_bit + rest
+
+@register.filter
+def apply(value, func):
+    """Applique une fonction arbitraire à la valeur filtrée."""
+    return func(value)
index a7ea72b..9f48bd8 100644 (file)
@@ -10,7 +10,7 @@ from django.shortcuts import get_object_or_404
 from django.utils.safestring import mark_safe
 from django import forms
 from django.conf import settings
-from lib.recherche import google_search, build_search_regexp
+from lib.recherche import google_search, build_search_regexp, excerpt_function
 from lib import sep
 from lib.calendrier import evenements, evenement_info, combine
 from savoirs.globals import configuration
@@ -24,13 +24,30 @@ from sitotheque.models import Site
 
 def index(request, discipline=None, region=None):
     """Page d'accueil"""
-    delta = datetime.timedelta(days = 90)
-    oldest = datetime.date.today() - delta
-    actualites = Actualite.objects.filter(visible=True, date__gt=oldest).filter_discipline(discipline).filter_region(region)[:4]
-    evenements = Evenement.objects.filter(approuve=True).filter_discipline(discipline).filter_region(region)[:4]
-    ressources = Record.objects.validated().filter_discipline(discipline).filter_region(region).random(4)
-    chercheurs = Chercheur.objects.filter_discipline(discipline).filter_region(region).order_by('-date_modification')[:10]
-    sites = Site.objects.filter_discipline(discipline).filter_region(region).random(4)
+    actualites = Actualite.objects
+    evenements = Evenement.objects
+    ressources = Record.objects
+    chercheurs = Chercheur.objects
+    sites = Site.objects
+    if discipline:
+        discipline = Discipline.objects.get(pk=discipline)
+        actualites = actualites.filter_discipline(discipline)
+        evenements = evenements.filter_discipline(discipline)
+        ressources = ressources.filter_discipline(discipline)
+        chercheurs = chercheurs.filter_discipline(discipline)
+        sites = sites.filter_discipline(discipline)
+    if region:
+        region = Region.objects.get(pk=region)
+        actualites = actualites.filter_region(region)
+        evenements = evenements.filter_region(region)
+        ressources = ressources.filter_region(region)
+        chercheurs = chercheurs.filter_region(region)
+        sites = sites.filter_region(region)
+    actualites = actualites.all()[0:4]
+    evenements = evenements.all()[0:4]
+    ressources = ressources.all().random(4)
+    chercheurs = chercheurs.all()[0:10]
+    sites = sites.all().random(4)
     return render_to_response(
         "savoirs/index.html",
         dict(actualites=actualites, evenements=evenements,
@@ -68,39 +85,54 @@ def recherche(request, discipline=None, region=None):
             kwargs['region'] = region
         return HttpResponseRedirect(reverse('savoirs.views.index', kwargs=kwargs))
 
-    ressources = Record.objects.validated().filter_discipline(discipline).filter_region(region).search(query)
-    actualites = Actualite.objects.filter(visible=1).filter_discipline(discipline).filter_region(region).search(query)
-    evenements = Evenement.objects.filter(approuve=1).filter_discipline(discipline).filter_region(region).search(query)
-    chercheurs = Chercheur.objects.filter_discipline(discipline).filter_region(region).search(query)
-    sites = Site.objects.filter_discipline(discipline).filter_region(region).search(query)
+    actualites = Actualite.objects.search(query)
+    evenements = Evenement.objects.search(query)
+    ressources = Record.objects.search(query)
+    chercheurs = Chercheur.objects.search(query)
+    sites = Site.objects.search(query)
+    if discipline:
+        discipline = Discipline.objects.get(pk=discipline)
+        actualites = actualites.filter_discipline(discipline)
+        evenements = evenements.filter_discipline(discipline)
+        ressources = ressources.filter_discipline(discipline)
+        chercheurs = chercheurs.filter_discipline(discipline)
+        sites = sites.filter_discipline(discipline)
+    if region:
+        region = Region.objects.get(pk=region)
+        actualites = actualites.filter_region(region)
+        evenements = evenements.filter_region(region)
+        ressources = ressources.filter_region(region)
+        chercheurs = chercheurs.filter_region(region)
+        sites = sites.filter_region(region)
     try:
         sites_auf = google_search(0, query)['results']
     except:
         sites_auf = []
-    search_regexp = build_search_regexp(query)
 
     # Bâtissons une query string pour les liens vers les briques
     params = {}
     if query:
         params['q'] = query
     if discipline:
-        params['discipline'] = discipline
+        params['discipline'] = unicode(discipline.id)
     if region:
-        params['region'] = region
+        params['region'] = unicode(region.id)
     if params:
         briques_query_string = mark_safe('?' + '&'.join(k + '=' + v.replace('"', '&quot;') for (k, v) in params.iteritems()))
     else:
         briques_query_string = None
         
+    excerpt = excerpt_function(Record.objects, query)
+
     return render_to_response(
         "savoirs/recherche.html",
-        dict(q=query, search_regexp=search_regexp,
-             ressources=ressources[:5], total_ressources=ressources.count(), 
-             evenements=evenements[:5], total_evenements=evenements.count(),
-             chercheurs=chercheurs[:10], total_chercheurs=chercheurs.count(),
-             actualites=actualites[:5], total_actualites=actualites.count(),
-             sites=sites[:5], total_sites=sites.count(),
-             sites_auf=sites_auf[:5], briques_query_string=briques_query_string),
+        dict(q=query, excerpt=excerpt,
+             ressources=ressources[0:5], total_ressources=ressources.count(), 
+             evenements=evenements[0:5], total_evenements=evenements.count(),
+             chercheurs=chercheurs[0:10], total_chercheurs=chercheurs.count(),
+             actualites=actualites[0:5], total_actualites=actualites.count(),
+             sites=sites[0:5], total_sites=sites.count(),
+             sites_auf=sites_auf[0:5], briques_query_string=briques_query_string),
         context_instance = RequestContext(request)
     )
 
@@ -120,12 +152,15 @@ def ressource_index(request):
     search_form = RecordSearchForm(request.GET)
     ressources = search_form.get_query_set()
     nb_resultats = ressources.count()
-    search_regexp = search_form.get_search_regexp()
+    if search_form.is_valid():
+        excerpt = excerpt_function(Record.objects, search_form.cleaned_data['q'])
+    else:
+        excerpt = lambda x: x
     return render_to_response(
         "savoirs/ressource_index.html", 
-        {'search_form': search_form, 'ressources': ressources,
-         'nb_resultats': nb_resultats, 'search_regexp': search_regexp},
-        context_instance = RequestContext(request)
+        dict(search_form=search_form, ressources=ressources,
+             nb_resultats=nb_resultats, excerpt=excerpt),
+        context_instance=RequestContext(request)
     )
 
 def ressource_retrieve(request, id):
@@ -154,22 +189,25 @@ def informations (request):
 def actualite_index(request):
     search_form = ActualiteSearchForm(request.GET)
     actualites = search_form.get_query_set()
-    search_regexp = search_form.get_search_regexp()
+    if search_form.is_valid():
+        excerpt = excerpt_function(Actualite.objects, search_form.cleaned_data['q'])
+    else:
+        excerpt = lambda x: x
     return render_to_response(
         "savoirs/actualite_index.html",
         dict(actualites=actualites, search_form=search_form,
-             search_regexp=search_regexp, nb_resultats=actualites.count()),
+             excerpt=excerpt, nb_resultats=actualites.count()),
         context_instance = RequestContext(request))
 
 # agenda
 def evenement_index(request):
     search_form = EvenementSearchForm(request.GET)
     evenements = search_form.get_query_set()
-    search_regexp = search_form.get_search_regexp()
+    excerpt = excerpt_function(Evenement.objects, search_form.cleaned_data['q'])
     return render_to_response(
         "savoirs/evenement_index.html",
         dict(evenements=evenements, search_form=search_form,
-             search_regexp=search_regexp, nb_resultats=evenements.count()),
+             excerpt=excerpt, nb_resultats=evenements.count()),
         context_instance=RequestContext(request)
     )
                               
diff --git a/auf_savoirs_en_partage/scripts/sphinx.conf.py.in b/auf_savoirs_en_partage/scripts/sphinx.conf.py.in
new file mode 100644 (file)
index 0000000..51d6f29
--- /dev/null
@@ -0,0 +1,236 @@
+#!/usr/bin/python
+# coding: utf-8
+# 
+# Ce script compose dynamiquement une configuration pour Sphinx. Il suffit
+# que le fichier soit exécutable pour que Sphinx exécute le script plutôt
+# que de le lire comme un fichier statique.
+#
+# Le manuel de sphinx se trouve au http://sphinxsearch.com/docs/manual-0.9.9.html
+
+import glob
+import os
+import sys
+
+sys.path[0:0] = [
+  "%(directory)s",
+  "%(directory)s/auf_savoirs_en_partage",
+  ]
+os.environ['DJANGO_SETTINGS_MODULE'] = 'production'
+for d in glob.glob("%(directory)s/eggs/*"):
+    sys.path[0:0] = [d,]
+for d in glob.glob("%(directory)s/parts/*"):
+    sys.path[0:0] = [d,]
+
+from django.conf import settings
+
+SQL_HOST = settings.DATABASE_HOST
+SQL_USER = settings.DATABASE_USER
+SQL_PASS = settings.DATABASE_PASSWORD
+SQL_DB = settings.DATABASE_NAME
+DATA_DIR = '%(directory)s/auf_savoirs_en_partage/data/sphinx'
+
+def multiline(s):
+    """Place un marqueur de continuation avant chaque saut de ligne."""
+    return s.replace("\n", "\\\n")
+
+def emit_source(name, sql_query, sql_query_info=None, sql_attr_multi=None, sql_attr_uint=None):
+    print '''
+source %%(name)s
+{
+    type = mysql
+    sql_host = %%(sql_host)s
+    sql_user = %%(sql_user)s
+    sql_pass = %%(sql_pass)s
+    sql_db = %%(sql_db)s
+    sql_query_pre = SET NAMES utf8
+    sql_query_pre = SET SESSION query_cache_type=OFF
+    sql_query = %%(sql_query)s
+''' %% dict(name=name, sql_host=SQL_HOST, sql_user=SQL_USER,
+            sql_pass=SQL_PASS, sql_db=SQL_DB, sql_query=multiline(sql_query))
+
+    if sql_query_info:
+        print '    sql_query_info = ' + sql_query_info
+
+    if sql_attr_multi:
+        for attr in sql_attr_multi:
+            print '    sql_attr_multi = uint %%s from field' %% attr
+
+    if sql_attr_uint:
+        for attr in sql_attr_uint:
+            print '    sql_attr_uint = ' + attr
+    
+    print '}'
+
+def emit_index(name):
+    print '''
+index %%(name)s
+{
+    morphology = libstemmer_fr
+    charset_type = utf-8
+    charset_table = 0..9, A..Z->a..z, _, a..z, \\
+                    U+C0->a, U+C2->a, U+E0->a, U+E2->a, \\
+                    U+C7->c, U+E7->c, \\
+                    U+C8->e, U+C9->e, U+CA->e, U+E8->e, U+E9->e, U+EA->e, U+EB->e, \\
+                    U+CE->i, U+EE->i, U+EF->i, \\
+                    U+D4->o, U+F4->o, \\
+                    U+F9->u, U+FB->u, U+FC->u
+    source = %%(name)s
+    path = %%(path)s
+}''' %% dict(name=name, path=os.path.join(DATA_DIR, name))
+
+emit_source('ressources', 
+            '''SELECT r.id AS id, 
+                      r.title AS title, 
+                      r.description AS description, 
+                      r.creator AS creator, 
+                      r.contributor AS contributor, 
+                      r.subject AS subject, 
+                      r.publisher AS publisher, 
+                      GROUP_CONCAT(DISTINCT d.nom_discipline) AS disciplines, 
+                      GROUP_CONCAT(DISTINCT d.id_discipline) AS discipline_ids,
+                      GROUP_CONCAT(DISTINCT p.nom) AS pays, 
+                      GROUP_CONCAT(DISTINCT reg.nom) AS regions,
+                      GROUP_CONCAT(DISTINCT reg.id) AS region_ids
+                FROM savoirs_record r 
+                LEFT JOIN savoirs_record_disciplines rd ON rd.record_id = r.id 
+                LEFT JOIN discipline d ON d.id_discipline = rd.discipline_id 
+                LEFT JOIN savoirs_record_pays rp ON rp.record_id = r.id 
+                LEFT JOIN ref_pays p ON p.id = rp.pays_id 
+                LEFT JOIN savoirs_record_regions rr ON rr.record_id = r.id 
+                LEFT JOIN ref_region reg ON reg.id = rr.region_id OR reg.id = p.region 
+                LEFT JOIN savoirs_record_listsets rl ON rl.record_id = r.id
+                LEFT JOIN savoirs_listset l ON l.spec = rl.listset_id
+                WHERE r.validated AND (l.spec IS NULL OR l.validated)
+                GROUP BY r.id''',
+            sql_query_info='SELECT * from savoirs_record WHERE id=$id',
+            sql_attr_multi=['discipline_ids', 'region_ids']
+            )
+
+emit_source('actualites',
+            '''SELECT a.id_actualite AS id, 
+                      a.titre_actualite AS titre, 
+                      a.texte_actualite AS texte, 
+                      TO_DAYS(a.date_actualite) AS date,
+                      GROUP_CONCAT(DISTINCT r.nom) AS regions, 
+                      GROUP_CONCAT(DISTINCT d.nom_discipline) AS disciplines
+               FROM actualite a 
+               LEFT JOIN actualite_regions ar ON ar.actualite_id = a.id_actualite 
+               LEFT JOIN ref_region r ON r.id = ar.region_id 
+               LEFT JOIN actualite_disciplines ad ON ad.actualite_id = a.id_actualite 
+               LEFT JOIN discipline d ON d.id_discipline = ad.discipline_id
+               WHERE a.visible_actualite
+               GROUP BY a.id_actualite''',
+            sql_query_info='SELECT * from actualite WHERE id_actualite=$id',
+            sql_attr_uint=['date']
+           )
+
+emit_source('evenements',
+            '''SELECT e.id AS id,
+                      e.titre AS titre,
+                      e.mots_cles AS mots_cles,
+                      e.type AS type,
+                      e.lieu AS lieu,
+                      e.description AS description,
+                      e.contact AS contact,
+                      CONCAT_WS(',', d.nom_discipline, d2.nom_discipline) AS disciplines,
+                      GROUP_CONCAT(DISTINCT r.nom) AS regions,
+                      TO_DAYS(DATE(e.debut)) AS debut
+               FROM savoirs_evenement e
+               LEFT JOIN discipline d ON d.id_discipline = e.discipline_id
+               LEFT JOIN discipline d2 ON d2.id_discipline = e.discipline_secondaire_id
+               LEFT JOIN savoirs_evenement_regions er ON er.evenement_id = e.id
+               LEFT JOIN ref_region r ON r.id = er.region_id
+               WHERE e.approuve
+               GROUP BY e.id''',
+            sql_query_info='SELECT * from savoirs_evenement WHERE id=$id',
+            sql_attr_uint=['debut'])
+            
+emit_source('chercheurs',
+            '''SELECT c.id AS id,
+                      p.nom AS nom,
+                      p.prenom AS prenom,
+                      c.theme_recherche AS theme_recherche,
+                      c.groupe_recherche AS groupe_recherche,
+                      c.mots_cles AS mots_cles,
+                      c.membre_association_francophone_details AS membre_association_francophone_details,
+                      c.membre_reseau_institutionnel_details AS membre_reseau_institutionnel_details,
+                      c.expert_oif_details AS expert_oif_details,
+                      c.membre_instance_auf_details AS membre_instance_auf_details,
+                      IFNULL(et.nom, etablissement_autre_nom) AS etablissement,
+                      pays.nom AS pays,
+                      pays.id AS pays_id,
+                      r.nom AS region,
+                      r.id AS region_id,
+                      GROUP_CONCAT(DISTINCT d.nom_discipline) AS disciplines,
+                      CONCAT_WS(pub1.titre, pub2.titre, pub3.titre, pub4.titre) AS publications,
+                      t.titre AS these,
+                      GROUP_CONCAT(DISTINCT g.nom) AS groupes,
+                      GROUP_CONCAT(DISTINCT ex.nom) AS expertises,
+                      GROUP_CONCAT(DISTINCT g.id) AS groupe_ids,
+                      TO_DAYS(c.date_modification) AS date_modification,
+                      CASE pays WHEN 'Nord' THEN 1 
+                                WHEN 'Sud' THEN 2 
+                                END AS nord_sud,
+                      CASE statut WHEN 'enseignant' THEN 1
+                                  WHEN 'etudiant' THEN 2
+                                  WHEN 'independant' THEN 3
+                                  END AS statut,
+                      (ex.id IS NULL) AS expert
+               FROM chercheurs_chercheur c
+               INNER JOIN chercheurs_personne p ON c.personne = p.id
+               LEFT JOIN ref_etablissement et ON et.id = c.etablissement
+               LEFT JOIN ref_pays pays ON pays.id = IFNULL(et.pays, c.etablissement_autre_pays)
+               LEFT JOIN ref_region r ON pays.region = r.id
+               LEFT JOIN discipline d ON d.id_discipline = c.discipline
+               LEFT JOIN chercheurs_publication pub1 ON pub1.id = c.publication1
+               LEFT JOIN chercheurs_publication pub2 ON pub2.id = c.publication2
+               LEFT JOIN chercheurs_publication pub3 ON pub3.id = c.publication3
+               LEFT JOIN chercheurs_publication pub4 ON pub4.id = c.publication4
+               LEFT JOIN chercheurs_publication t ON t.id = c.these
+               LEFT JOIN chercheurs_chercheurgroupe cg ON cg.chercheur = c.id
+               LEFT JOIN chercheurs_groupe g ON g.id = cg.groupe
+               LEFT JOIN chercheurs_expertise ex ON ex.chercheur_id = c.id
+               GROUP BY c.id''',
+            sql_query_info='SELECT * from chercheurs_chercheur WHERE id=$id',
+            sql_attr_multi=['groupe_ids'],
+            sql_attr_uint=['pays_id', 'region_id', 'nord_sud', 'date_modification', 'statut', 'expert'])
+                      
+emit_source('sites',
+            '''SELECT s.id AS id,
+                      s.titre AS titre,
+                      s.description AS description,
+                      s.editeur AS editeur,
+                      s.auteur AS auteur,
+                      s.mots_cles AS mots_cles,
+                      GROUP_CONCAT(DISTINCT d.nom_discipline) AS disciplines,
+                      GROUP_CONCAT(DISTINCT p.nom) AS pays,
+                      GROUP_CONCAT(DISTINCT p.id) AS pays_ids,
+                      GROUP_CONCAT(DISTINCT r.nom) AS regions
+               FROM sitotheque_site s
+               LEFT JOIN sitotheque_site_discipline sd ON sd.site_id = s.id
+               LEFT JOIN discipline d ON d.id_discipline = sd.discipline_id
+               LEFT JOIN ref_pays p ON p.id = s.pays
+               LEFT JOIN ref_region r ON r.id = p.region
+               GROUP BY s.id''',
+            'SELECT * FROM sitotheque_site WHERE id=$id',
+            sql_attr_multi=['pays_ids'])
+
+emit_index('actualites')
+emit_index('ressources')
+emit_index('evenements')
+emit_index('chercheurs')
+emit_index('sites')
+
+print '''
+indexer
+{
+    mem_limit = 256M
+}
+
+searchd
+{
+    listen = 127.0.0.1:9312
+    pid_file = %%(pid_file)s
+    log = %%(log)s
+}
+''' %% dict(pid_file=os.path.join(DATA_DIR, 'sphinx.pid'), log=os.path.join(DATA_DIR, 'searchd.log'))
index 5334d72..fd96da0 100644 (file)
@@ -65,6 +65,7 @@ INSTALLED_APPS = (
     'savoirs',
     'chercheurs',
     'sitotheque',
+    'djangosphinx',
 )
 
 
@@ -102,4 +103,7 @@ AUTH_PROFILE_MODULE = 'savoirs.Profile'
 
 CONTACT_EMAIL = 'contact-savoirsenpartage@auf.org'
 
+SPHINX_API_VERSION = 0x116
+SPHINX_PORT = 9312
+
 from auf_references_client.settings import *
index 5317049..d7aac37 100644 (file)
@@ -14,7 +14,7 @@ class SiteSearchForm(forms.Form):
     def get_query_set(self):
         """Retourne l'ensemble des sites qui correspondent aux valeurs
            entrées dans le formulaire."""
-        sites = Site.objects.order_by("titre")
+        sites = Site.objects
         if self.is_valid():
             q = self.cleaned_data["q"]
             if q:
@@ -27,11 +27,5 @@ class SiteSearchForm(forms.Form):
                 sites = sites.filter_region(region)
             pays = self.cleaned_data["pays"]
             if pays:
-                sites = sites.filter(pays=pays.pk)
-        return sites
-
-    def get_search_regexp(self):
-        """Retourne une expression régulière compilée qui peut servir à
-           chercher les mot-clés recherchés dans un texte."""
-        if self.is_valid():
-            return build_search_regexp(self.cleaned_data['q'])
+                sites = sites.filter_pays(pays=pays)
+        return sites.all()
index 7dd4b42..ac0c000 100644 (file)
@@ -1,8 +1,9 @@
 # -*- encoding: utf-8 -*-
+from datamaster_modeles.models import *
 from django.db import models
 from django.db.models import Q
-from datamaster_modeles.models import *
-from savoirs.models import Discipline, RandomQuerySetMixin
+from djangosphinx.models import SphinxSearch
+from savoirs.models import Discipline, SEPManager, SEPSphinxQuerySet, SEPQuerySet
 
 TYPE_SITE_CHOICES = (
     ('RV', 'Revue en ligne'), 
@@ -17,66 +18,29 @@ TYPE_SITE_CHOICES = (
     ('AU', 'Autre type de site'),
     )
 
-class SiteManager(models.Manager):
+class SiteQuerySet(SEPQuerySet):
 
-    def get_query_set(self):
-        return SiteQuerySet(self.model)
+    def filter_pays(self, pays):
+        return self.filter(pays=pays)
 
-    def search(self, text):
-        return self.get_query_set().search(text)
+class SiteSphinxQuerySet(SEPSphinxQuerySet):
 
-    def filter_region(self, region):
-        return self.get_query_set().filter_region(region)
+    def __init__(self, model=None):
+        SEPSphinxQuerySet.__init__(self, model=model, index='sites', weights=dict(titre=3))
 
-    def filter_discipline(self, discipline):
-        return self.get_query_set().filter_discipline(discipline)
+    def filter_pays(self, pays):
+        return self.filter(pays_ids=pays.id)
 
-class SiteQuerySet(models.query.QuerySet, RandomQuerySetMixin):
+class SiteManager(SEPManager):
 
-    def search(self, text):
-        qs = self
-        q = None
-        for word in text.split():
-            part = (Q(titre__icontains=word) |
-                    Q(description__icontains=word) |
-                    Q(editeur__icontains=word) |
-                    Q(auteur__icontains=word) |
-                    Q(mots_cles__icontains=word) |
-                    Q(discipline__nom__icontains=word) |
-                    Q(pays__nom__icontains=word))
-            if q is None:
-                q = part
-            else:
-                q = q & part
-        if q is not None:
-            qs = qs.filter(q).distinct()
-        return qs
+    def get_query_set(self):
+        return SiteQuerySet(self.model)
 
-    def filter_discipline(self, discipline):
-        """Ne conserve que les sites dans la discipline donnée.
-           
-        Si ``disicipline`` est None, ce filtre n'a aucun effet."""
-        if discipline is None:
-            return self
-        if not isinstance(discipline, Discipline):
-            discipline = Discipline.objects.get(pk=discipline)
-        return self.filter(Q(discipline=discipline) |
-                           Q(titre__icontains=discipline.nom) |
-                           Q(description__icontains=discipline.nom) |
-                           Q(mots_cles__icontains=discipline.nom))
+    def get_sphinx_query_set(self):
+        return SiteSphinxQuerySet(self.model)
 
-    def filter_region(self, region):
-        """Ne conserve que les sites dans la région donnée.
-           
-        Si ``region`` est None, ce filtre n'a aucun effet."""
-        if region is None:
-            return self
-        if not isinstance(region, Region):
-            region = Region.objects.get(pk=region)
-        return self.filter(Q(pays__region=region) |
-                           Q(titre__icontains=region.nom) |
-                           Q(description__icontains=region.nom) |
-                           Q(mots_cles__icontains=region.nom)).distinct()
+    def filter_pays(self, pays):
+        return self.get_query_set().filter_pays(pays)
 
 class Site(models.Model):
     """Fiche d'info d'un site web"""
@@ -106,6 +70,7 @@ class Site(models.Model):
 
     # Manager
     objects = SiteManager()
+    all_objects = models.Manager()
     
     def __unicode__(self):
         return "%s" % (self.titre)
index d861e63..e45e9c9 100644 (file)
@@ -2,18 +2,18 @@
 from django.shortcuts import render_to_response
 from django.template import Context, RequestContext
 from django.db.models import Q
-
-from models import Site
 from forms import SiteSearchForm
+from models import Site
+from savoirs.lib.recherche import excerpt_function
 
 def index(request):
     search_form = SiteSearchForm(request.GET)
     sites = search_form.get_query_set()
-    search_regexp = search_form.get_search_regexp()
     nb_sites = sites.count()
+    excerpt = excerpt_function(Site.objects, search_form.cleaned_data['q'])
     return render_to_response("sites/index.html",
-                              dict(sites=sites, search_form=search_form, 
-                                   search_regexp=search_regexp, nb_sites=nb_sites), 
+                              dict(sites=sites, search_form=search_form,
+                                   excerpt=excerpt, nb_sites=nb_sites), 
                               context_instance = RequestContext(request))
             
 def retrieve(request, id):
index 103094f..322a71d 100644 (file)
@@ -1,9 +1,9 @@
-{% load search %}
+{% load sep %}
 
 <div class="resultatRecherche">
   <div class="la-date">{{ actualite.date|date:"d F Y" }}</div>
-  <a class="le-titre" href="{{ actualite.url }}">{{ actualite.titre|highlight:search_regexp }}</a>
-  <div class="resultatResume">{{ actualite.texte|highlight:search_regexp }}</div>
+  <a class="le-titre" href="{{ actualite.url }}">{{ actualite.titre|apply:excerpt }}</a>
+  <div class="resultatResume">{{ actualite.texte|apply:excerpt }}</div>
   {% if actualite.source %}
   <div><span class="lbl">Source:</span> {{ actualite.source.nom }}</div>
   {% endif %}
index 428d087..846f810 100644 (file)
@@ -1,7 +1,7 @@
-{% load search %}
+{% load sep %}
 
 <div class="resultatRecherche">
   <div class="la-date">{{ evenement.debut|date:"d/m/Y H\hi" }}</div>
-  <div><a href="{% url savoirs.views.evenement evenement.pk %}" class="le-titre">{{ evenement.titre|highlight:search_regexp }}</a></div>
-  <div class="le-resume">{{ evenement.description|excerpt:search_regexp|highlight:search_regexp }}</div>
+  <div><a href="{% url savoirs.views.evenement evenement.pk %}" class="le-titre">{{ evenement.titre|apply:excerpt }}</a></div>
+  <div class="le-resume">{{ evenement.description|apply:excerpt }}</div>
 </div>
index 8992f9a..3f0724b 100644 (file)
@@ -1,20 +1,14 @@
-{% load search %}
+{% load sep %}
 
 <div class="resultatRecherche">
-    <a class="le-titre" href="{% url savoirs.views.ressource_retrieve ressource.id %}">{{ ressource.title|highlight:search_regexp }}</a>
+    <a class="le-titre" href="{% url savoirs.views.ressource_retrieve ressource.id %}">{{ ressource.title|apply:excerpt }}</a>
     {% if ressource.creator %}
-    <div><span class="lbl">Auteur:</span> {{ ressource.creator|highlight:search_regexp }}</div>
+    <div><span class="lbl">Auteur:</span> {{ ressource.creator|apply:excerpt }}</div>
     {% endif %}
     {% if ressource.description %}
-    <div class="resultatResume"><span class="lbl">Description:</span> {{ ressource.description|excerpt:search_regexp|highlight:search_regexp }}</div>
+    <div class="resultatResume"><span class="lbl">Description:</span> {{ ressource.description|apply:excerpt }}</div>
     {% endif %}
     <div class="fiche"><span>Fiche: </span><a href="{% url savoirs.views.ressource_retrieve ressource.id %}">{% url savoirs.views.ressource_retrieve ressource.id %}</a></div>
     <div class="original"><span>Contenu original: </span><a target="_blank" href="{{ ressource.uri }}">{{ ressource.uri }}</a></div>
     <div class="provenance"><span>Provenance: </span><a target="_blank" href="{{ ressource.getServeurURL }}">{{ ressource.getServeurURL }}</a></div>
 </div>
-
-    {% comment %}
-    {% if user.is_authenticated %}
-    <a href="{{ r.admin_url}}" target="_blank">Modifier</a>
-    {% endif %}
-    {% endcomment %}
index 25f734c..403d842 100644 (file)
@@ -1,7 +1,7 @@
-{% load search %}
+{% load sep %}
 
 <div class="resultatRecherche">
-  <div><a class="le-titre" href="{% url sitotheque.views.retrieve site.id %}">{{ site|highlight:search_regexp }}</a></div>
-  <div class="resultatResume">{{ site.description|excerpt:search_regexp|highlight:search_regexp }}</div>
+  <div><a class="le-titre" href="{% url sitotheque.views.retrieve site.id %}">{{ site.titre|apply:excerpt }}</a></div>
+  <div class="resultatResume">{{ site.description|apply:excerpt }}</div>
   <div><span class="lbl">URL:</span> <a href="{{ site.url }}">{{ site.url }}</a></div>
 </div>
index 6f5c9d1..b470269 100644 (file)
@@ -1,6 +1,6 @@
 [buildout]
 newest = false
-parts = django articles harvest
+parts = django articles harvest sphinx_conf
 find-links = http://pypi.auf.org/caldav/
     http://pypi.auf.org/auf_references_client/
     http://pypi.auf.org/auf_references_modeles/
@@ -27,6 +27,7 @@ eggs = auf_references_client
     datamaster_modeles
     django-roa
     django-admin-tools
+    django-sphinx
 
 #develop = src/caldav
 
@@ -44,3 +45,8 @@ template = harvest.in
 recipe = buildout_script
 template_dir = ${buildout:directory}/auf_savoirs_en_partage/scripts/
 template = import_chercheurs.in
+
+[sphinx_conf]
+recipe = buildout_script
+template_dir = ${buildout:directory}/auf_savoirs_en_partage/scripts/
+template = sphinx.conf.py.in