Replace Haystack by django-elasticsearch-dsl. Haystack is compatible with an outdated version of elasticsearch
authorymh <ymh.work@gmail.com>
Tue, 05 Jun 2018 11:32:49 +0200
changeset 536 7f8390504d84
parent 535 4217bf54446a
child 537 0664c4845ce7
Replace Haystack by django-elasticsearch-dsl. Haystack is compatible with an outdated version of elasticsearch
src/iconolab/apps.py
src/iconolab/conf.py
src/iconolab/documents.py
src/iconolab/models.py
src/iconolab/search_indexes/__init__.py
src/iconolab/search_indexes/forms.py
src/iconolab/search_indexes/indexes.py
src/iconolab/search_indexes/query.py
src/iconolab/search_indexes/signals.py
src/iconolab/search_indexes/urls.py
src/iconolab/search_indexes/views.py
src/iconolab/templates/partials/header_search_form.html
src/iconolab/templates/search/annotation_search.html
src/iconolab/templates/search/default_search.html
src/iconolab/templates/search/image_search.html
src/setup.py
--- a/src/iconolab/apps.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/apps.py	Tue Jun 05 11:32:49 2018 +0200
@@ -7,3 +7,4 @@
     def ready(self):
         import iconolab.signals.handlers
         import iconolab.templatetags.iconolab_tags
+        from iconolab.conf import settings
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/conf.py	Tue Jun 05 11:32:49 2018 +0200
@@ -0,0 +1,58 @@
+from django.conf import settings
+from appconf import AppConf
+from elasticsearch_dsl import analyzer, token_filter
+
+french_elision = token_filter(
+    'french_elision',
+    type='elision',
+    articles_case=True,
+    articles=["l", "m", "t", "qu", "n", "s", "j", "d", "c", "jusqu", "quoiqu", "lorsqu", "puisqu"]
+)
+french_stemmer = token_filter(
+    'french_stemmer',
+    type="stemmer",
+    language="light_french"
+)
+french_analyzer = analyzer(
+    'french',
+    tokenizer='standard',
+    filter=['lowercase', 'asciifolding', french_elision, french_stemmer]
+)
+
+class IconolabConf(AppConf):
+
+
+    INDEXES_CONFIG = {
+        'items': {
+            'name': 'iconolab_items',
+            'number_of_shards': 1,
+            'number_of_replicas': 0,
+        },
+        'annotations': {
+            'name': 'iconolab_annotations',
+            'number_of_shards': 1,
+            'number_of_replicas': 0,
+        }
+    }
+
+    INDEXES_DEFAULT_CONFIG = {
+        'number_of_shards': 1,
+        'number_of_replicas': 0
+    }
+
+    INDEXES_ANALYZER = {
+        'items': french_analyzer,
+        'annotations': french_analyzer
+    }
+
+    INDEXES_FIELD_ANALYZER = {
+        'items': french_analyzer,
+        'annotations': french_analyzer
+    }
+
+    INDEXES_QUERYSET_PAGINATION = 5000
+
+    SEARCH_PAGE_SIZE = 10
+
+    class Meta:
+        prefix = 'iconolab'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/iconolab/documents.py	Tue Jun 05 11:32:49 2018 +0200
@@ -0,0 +1,3 @@
+from .search_indexes import AnnotationDocument, ItemDocument
+
+__all__ = ['AnnotationDocument', 'ItemDocument']
--- a/src/iconolab/models.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/models.py	Tue Jun 05 11:32:49 2018 +0200
@@ -472,6 +472,8 @@
 
     @property
     def tag_labels(self):
+        if self.current_revision is None:
+            return []
         current_revision_tags = json.loads(
             self.current_revision.get_tags_json())
         return [tag_infos['tag_label'] for tag_infos in current_revision_tags if tag_infos.get('tag_label') is not None]
--- a/src/iconolab/search_indexes/__init__.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/__init__.py	Tue Jun 05 11:32:49 2018 +0200
@@ -1,2 +1,3 @@
-from .indexes import AnnotationIndex, ItemIndex 
-__all__ = ['AnnotationIndex', 'ItemIndex', 'RevisionSignalProcessor']
\ No newline at end of file
+# from .indexes import AnnotationIndex, ItemIndex
+from .indexes import ItemDocument, AnnotationDocument
+__all__ = ['AnnotationDocument', 'ItemDocument']
--- a/src/iconolab/search_indexes/forms.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/forms.py	Tue Jun 05 11:32:49 2018 +0200
@@ -1,19 +1,28 @@
+import json
+import logging
+
 from django import forms
-from haystack.forms import SearchForm
-from iconolab.models import Item, Annotation, Collection
+from django.utils.translation import ugettext_lazy as _
 
+from iconolab.conf import settings
+from iconolab.models import Annotation, Collection, Item
 
+from .indexes import AnnotationDocument, ItemDocument
+from .query import EmptyQueryResults, QueryResults
 
+logger = logging.getLogger(__name__)
 
-class IconolabSearchForm(SearchForm):
-
+class IconolabSearchForm(forms.Form):
+    """
+    Inspired by Haystack.forms.SearchForm
+    """
+    q = forms.CharField(required=False, label=_('Search'),
+                        widget=forms.TextInput(attrs={'type': 'search'}))
     model_type = forms.ChoiceField(required=False, choices=(("images","Images"), ("annotations","Annotations")) )
     tags = forms.BooleanField(required=False, initial=False)
 
     def __init__(self, *args, **kwargs):
         self.collection_name = kwargs.pop("collection_name")
-        if self.collection_name and Collection.objects.filter(name=self.collection_name).exists():
-            self.collection = Collection.objects.get(name=self.collection_name)
         selected_model_type =  kwargs.pop("model_type", None)
 
         if selected_model_type is not None:
@@ -23,37 +32,40 @@
                 data["model_type"] = selected_model_type
                 kwargs['data'] = data
 
-        super(IconolabSearchForm, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
+
+
+    def search(self):
+
+        model_type = self.cleaned_data.get('model_type')
+
+        model_klass = None
+        document_klass = None
+        refine_qs = lambda qs: qs
 
-    def no_query_found(self):
-        # load all
-        selected_type = self.cleaned_data.get("model_type")
-        qs = self.get_model_type_queryset(self.searchqueryset, selected_type).load_all()
-        return qs
+        if(model_type == 'images'):
+            model_klass = Item
+            document_klass = ItemDocument
+            refine_qs = lambda qs: qs.select_related('collection', 'metadatas')
+        elif(model_type == 'annotations'):
+            model_klass = Annotation
+            document_klass = AnnotationDocument
+            refine_qs = lambda qs: qs.select_related('image', 'image__item', 'image__item__collection', 'stats', 'current_revision', 'author')
 
-    def get_model_type_queryset(self, qs, model_type, tags_only):
+        q = self.cleaned_data.get('q','')
+        if model_klass is None or not q:
+            return EmptyQueryResults()
 
-        if model_type == 'images':
-            qs = qs.models(Item).load_all_queryset(Item, Item.objects.select_related('collection', 'metadatas'))
-        if model_type == 'annotations':
-            qs = qs.models(Annotation).load_all_queryset(Annotation, Annotation.objects.select_related('image', 'image__item', 'image__item__collection', 'stats', 'current_revision', 'author'))
+        tags_only = self.cleaned_data.get("tags", False)
+        fields = ['text'] if not tags_only else ['tags']
+
+        el_qs = document_klass.search()
 
         if self.collection_name is not None:
-            qs = qs.filter(collection = self.collection_name)
+            el_qs = el_qs.filter('term', collection=self.collection_name)
 
-        if tags_only:
-            qs = qs.filter(tags=self.cleaned_data.get("q"))
-
-        return qs
+        el_qs = el_qs.query('simple_query_string', query=self.cleaned_data.get('q'), fields=fields)
 
-    def search(self):
-        selected_type = self.cleaned_data.get("model_type")
-        tags_only = self.cleaned_data.get("tags")
-
-        qs = super(IconolabSearchForm, self).search()
+        logger.debug("SEARCH : %s", json.dumps(el_qs.to_dict()))
 
-        if qs.count() == 0:
-            return qs
-        else:
-            qs = self.get_model_type_queryset(qs, selected_type, tags_only).load_all()
-        return qs
+        return QueryResults(el_qs, refine_qs)
--- a/src/iconolab/search_indexes/indexes.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/indexes.py	Tue Jun 05 11:32:49 2018 +0200
@@ -1,42 +1,99 @@
+from django.template.loader import render_to_string
 from django.utils import timezone
-from haystack import indexes
-from iconolab.models import Annotation, Item
+
+from django_elasticsearch_dsl import DocType, Index, fields
+from iconolab.conf import settings
+from iconolab.models import (Annotation, AnnotationRevision, Collection, Item,
+                             ItemMetadata)
+
+__all__ = ['ItemDocument', 'AnnotationDocument']
 
-class ItemIndex(indexes.SearchIndex, indexes.Indexable):
-    text = indexes.CharField(document=True, use_template=True)
+def build_index(key):
+    index_config = settings.ICONOLAB_INDEXES_CONFIG.get(key) or settings.ICONOLAB_INDEXES_DEFAULT_CONFIG
+    index_name = index_config.pop('name', 'iconolab_' + key)
+    index = Index(index_name)
+    index.settings(**index_config)
+    if settings.ICONOLAB_INDEXES_ANALYZER.get(key) is not None:
+        index.analyzer(settings.ICONOLAB_INDEXES_ANALYZER.get(key))
+    return index
 
-    collection = indexes.CharField(model_attr="collection")
-    authors = indexes.CharField(model_attr="metadatas__authors")
-    school = indexes.CharField(model_attr="metadatas__school")
-    designation = indexes.CharField(model_attr="metadatas__designation")
-    datation = indexes.CharField(model_attr="metadatas__datation")
-    technics = indexes.CharField(model_attr="metadatas__technics")
-    measurements = indexes.CharField(model_attr="metadatas__measurements")
-    create_or_usage_location = indexes.CharField(model_attr="metadatas__create_or_usage_location")
-    discovery_context = indexes.CharField(model_attr="metadatas__discovery_context")
-    conservation_location = indexes.CharField(model_attr="metadatas__conservation_location")
-    
-    #tags = indexes.MultiValueField(model_attr="tag_labels")
+def get_text_field_kwargs(key):
+    analyzer = settings.ICONOLAB_INDEXES_FIELD_ANALYZER.get(key)
+    if analyzer is not None:
+        return {
+            'analyzer': analyzer
+        }
+    else:
+        return {}
+
 
-    def get_model(self):
-        return Item
+items_index = build_index('items')
 
-    def index_queryset(self, using=None):
-        return self.get_model().objects.filter()
+@items_index.doc_type
+class ItemDocument(DocType):
+    class Meta:
+        model = Item
+
+        queryset_pagination = settings.ICONOLAB_INDEXES_QUERYSET_PAGINATION
+        related_models = [ItemMetadata]
 
 
-class AnnotationIndex(indexes.SearchIndex, indexes.Indexable):
+    text = fields.TextField(**get_text_field_kwargs('items'))
+
+    collection = fields.KeywordField(attr="collection.name")
+
+    authors = fields.TextField(attr="metadatas.authors")
+    school = fields.TextField(attr="metadatas.school")
+    designation = fields.TextField(attr="metadatas.designation")
+    datation = fields.TextField(attr="metadatas.datation")
+    technics = fields.TextField(attr="metadatas.technics")
+    measurements = fields.TextField(attr="metadatas.measurements")
+    create_or_usage_location = fields.TextField(attr="metadatas.create_or_usage_location")
+    discovery_context = fields.TextField(attr="metadatas.discovery_context")
+    conservation_location = fields.TextField(attr="metadatas.conservation_location")
+
+    def prepare_text(self, instance):
+        return render_to_string('search/indexes/iconolab/item_text.txt', { 'object': instance })
+
+    def get_queryset(self):
+        return super().get_queryset().select_related('collection', 'metadatas').order_by('id')
+
+    def get_instances_from_related(self, related_instance):
+        if isinstance(related_instance, ItemMetadata):
+            return related_instance.item
+        else:
+            return None
+
+
+
+annotations_index = build_index('annotations')
 
-    ##indexed field
-    text = indexes.CharField(document=True, use_template=True)
-    title = indexes.CharField(model_attr="current_revision__title")
-    description = indexes.CharField(model_attr="current_revision__description")
-    collection = indexes.CharField(model_attr="image__item__collection") 
-    tags = indexes.MultiValueField(model_attr="tag_labels")
-    
-    ## tags
-    def get_model(self):
-        return Annotation
+@annotations_index.doc_type
+class AnnotationDocument(DocType):
+
+    class Meta:
+        model = Annotation
+
+        queryset_pagination = settings.ICONOLAB_INDEXES_QUERYSET_PAGINATION
+        related_models = [ AnnotationRevision ]
+
+
+    text = fields.TextField(**get_text_field_kwargs('annotations'))
 
-    def index_queryset(self, using=None):
-        return self.get_model().objects.filter(created__lte=timezone.now())
\ No newline at end of file
+    title = fields.TextField(attr="current_revision.title")
+    description = fields.TextField(attr="current_revision.description")
+    collection = fields.KeywordField(attr="image.item.collection.name")
+    tags = fields.TextField(attr="tag_labels", multi=True)
+
+    def prepare_text(self, instance):
+        return render_to_string('search/indexes/iconolab/annotation_text.txt', { 'object': instance })
+
+    def get_queryset(self):
+        return super().get_queryset().filter(created__lte=timezone.now()).select_related('current_revision', 'image__item__collection').order_by('id')
+
+    def get_instances_from_related(self, related_instance):
+        if isinstance(related_instance, AnnotationRevision):
+            return related_instance.annotation
+        else:
+            return None
+
--- a/src/iconolab/search_indexes/query.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/query.py	Tue Jun 05 11:32:49 2018 +0200
@@ -1,17 +1,36 @@
-from haystack.query import RelatedSearchQuerySet
-from iconolab.models import Annotation, Item
-from pprint import pprint
+from django.utils.functional import LazyObject
+from itertools import starmap
+
+__all__ = ['QueryResults', 'EmptyQueryResults']
 
-class IconolabRelatedQuerySet(RelatedSearchQuerySet):
-    def __init__(self, using=None, query=None):
-        super(IconolabRelatedQuerySet, self).__init__(using=using, query=query)
+def add_obj(result, instance):
+    result.object = instance
+    return result
+
+class EmptyQueryResults(object):
+    def __len__(self):
+        return 0
+
+    def __getitem__(self, index):
+        return []
 
-    def in_bulk(self, ids):
-        results = {}
-        int_ids = [ int(id) for id in ids]
-        annotations = Annotation.objects.filter(pk__in = int_ids)
+class QueryResults(LazyObject):
+    def __init__(self, search_results, refine_query):
+        self._wrapped = search_results
+        self.refine_query = refine_query
+
+    def _setup(self):
+        # do nothing
+        pass
 
-        for annotation in annotations:
-            results[annotation.pk] = annotation
+    def __len__(self):
+        return self._wrapped.count()
 
-        return results
+    def __getitem__(self, index):
+        sliced_search_results = self._wrapped[index]
+        if isinstance(index, slice):
+            sliced_search_results = starmap(
+                add_obj,
+                zip(sliced_search_results, self.refine_query(sliced_search_results.to_queryset()))
+            )
+        return sliced_search_results
--- a/src/iconolab/search_indexes/signals.py	Wed May 16 00:22:05 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-from iconolab.models import Image, AnnotationRevision, Annotation
-from haystack import signals
-from django.db import models
-from iconolab.signals.handlers import revision_created, revision_accepted
-import logging
-
-logger = logging.getLogger(__name__)
-
-# update / create new index when a new revision is accepted
-# Then update images tags related to this revision
-class RevisionSignalProcessor(signals.BaseSignalProcessor):
-
-    def setup(self):
-        revision_created.connect(self.handle_revision, sender=AnnotationRevision)
-        revision_accepted.connect(self.handle_revision, sender=AnnotationRevision)
-
-    def handle_revision(self, **kwargs):
-        revision_instance = kwargs.get("instance", None)
-        if revision_instance and revision_instance.state in [AnnotationRevision.ACCEPTED]:
-            annotation = revision_instance.annotation
-            image_annotation = revision_instance.annotation.image
-            self.handle_save(Annotation, annotation)
-            self.handle_save(Image, image_annotation)##useful for tag
-
-    def teardown(self):
-        revision_accepted.disconnect(self.handle_accepted_revision, sender=AnnotationRevision)
--- a/src/iconolab/search_indexes/urls.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/urls.py	Tue Jun 05 11:32:49 2018 +0200
@@ -6,9 +6,9 @@
 app_name = "iconolab-search_indexes"
 urlpatterns = [
     url(r'collection/(?P<collection_name>[a-z0-9\-]+)/model/(?P<model_type>[a-z0-9\-]+)', views.IconolabSearchView.as_view(), name="collection_with_model_search"),
-    url(r'collection/(?P<collection_name>[a-z0-9\-]+)', views.IconolabSearchView.as_view(), name="collection_haystack_search"),
+    url(r'collection/(?P<collection_name>[a-z0-9\-]+)', views.IconolabSearchView.as_view(), name="collection_search"),
     url(r'^model/(?P<model_type>[a-z0-9\-]+)', views.IconolabSearchView.as_view(), name="model_search"),
-    url(r'^global/$', views.IconolabSearchView.as_view(), name="haystack_search"),
+    url(r'^global/$', views.IconolabSearchView.as_view(), name="global_search"),
 ]
 
 
--- a/src/iconolab/search_indexes/views.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/search_indexes/views.py	Tue Jun 05 11:32:49 2018 +0200
@@ -1,22 +1,34 @@
-from haystack.generic_views import SearchView
-from haystack.query import RelatedSearchQuerySet
-from iconolab.search_indexes.forms import IconolabSearchForm
 from django.shortcuts import HttpResponse, redirect
 from django.urls import reverse
-from django.views.generic import RedirectView
+from django.views.generic import ListView, RedirectView
+from django.views.generic.edit import FormMixin
+
+from iconolab.conf import settings
 from iconolab.models import Collection
+from iconolab.search_indexes.forms import IconolabSearchForm
 
-#override Search and Related QuerySet here
-class IconolabSearchView(SearchView):
-    form_class = IconolabSearchForm
-    queryset = RelatedSearchQuerySet()
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class IconolabSearchView(ListView):
+
     template_name = "search/default_search.html"
-    load_all = True
-
     templates_map = {
         "images": "search/image_search.html",
         "annotations": "search/annotation_search.html"
     }
+    paginate_by = settings.ICONOLAB_SEARCH_PAGE_SIZE
+
+    def get_template_names(self):
+        try:
+            model_type = self.kwargs.get('model_type', None)
+            template = IconolabSearchView.templates_map[model_type]
+        except KeyError:
+            template = IconolabSearchView.template_name
+        finally:
+            return [template]
 
     def complete_url(self, url, tags_only):
         query = self.request.GET.get("q", None)
@@ -43,8 +55,9 @@
     def get(self, request, *args, **kwargs):
 
         self.model_type = request.GET.get('model_type', None)
-        self.paginate_by = request.GET.get('perpage', 10)
-        collection_name = self.kwargs.get('collection_name', None)
+        self.paginate_by = request.GET.get(
+            'perpage', settings.ICONOLAB_SEARCH_PAGE_SIZE)
+        self.collection_name = self.kwargs.get('collection_name', None)
 
         tags_only = False
         if (self.model_type == 'tags'):
@@ -52,20 +65,31 @@
             tags_only = True
 
         if self.model_type is not None:
-            if collection_name is None:
-                #redirect to all_model_type
-                redirect_url = reverse('search_indexes:model_search', kwargs={'model_type': self.model_type})
+            if self.collection_name is None:
+                # redirect to all_model_type
+                redirect_url = reverse('search_indexes:model_search', kwargs={
+                                       'model_type': self.model_type})
                 return redirect(self.complete_url(redirect_url, tags_only))
             else:
-                redirect_url = reverse('search_indexes:collection_with_model_search', kwargs={'collection_name': collection_name, 'model_type':self.model_type})
+                redirect_url = reverse('search_indexes:collection_with_model_search', kwargs={
+                                       'collection_name': self.collection_name, 'model_type': self.model_type})
                 return redirect(self.complete_url(redirect_url, tags_only))
         else:
             has_error, redirectView = self.check_kwargs(**kwargs)
             if has_error:
                 return redirectView(request)
-            return super(IconolabSearchView, self).get(request, *args, **kwargs)
+            return super().get(request, *args, **kwargs)
+
 
     def check_kwargs(self, **kwargs):
+        self.collection = None
+        if self.collection_name:
+            self.collection = Collection.objects.filter(name=self.collection_name).first()
+        if not self.collection_name or self.collection:
+            return (False, None)
+        else:
+            return (True, RedirectView.as_view(url=reverse('404error')))
+
         result = (False, None)
         try:
             collection_name = kwargs.get('collection_name', None)
@@ -77,25 +101,31 @@
         finally:
             return result
 
-    def get_queryset(self):
-        return IconolabSearchView.queryset
+
+    def get_form(self, initial=None):
 
-    def get_form_kwargs(self):
-        kwargs = super(IconolabSearchView, self).get_form_kwargs()
-        kwargs['collection_name'] = self.kwargs.get('collection_name', None)
-        kwargs['model_type'] = self.kwargs.get('model_type', None)
-        return kwargs
+        form_kwargs = {
+            'collection_name': self.kwargs.get('collection_name', None),
+            'model_type': self.kwargs.get('model_type', None)
+        }
 
-    def get_template_names(self):
-        try :
-            model_type = self.kwargs.get('model_type', None)
-            template = IconolabSearchView.templates_map[model_type]
-        except KeyError:
-            template = IconolabSearchView.template_name
-        finally:
-            return [template]
+        if initial is not None:
+            form_kwargs['initial'] : initial
+        else:
+            form_kwargs['data'] = self.request.GET
+        return IconolabSearchForm(**form_kwargs)
+
+    def get_queryset(self):
+        form = self.get_form()
+        if form.is_valid():
+            return form.search()
+        else:
+            return []
 
     def get_context_data(self, *args, **kwargs):
-        context = super(IconolabSearchView, self).get_context_data(*args, **kwargs)
-        context['collection_name'] = self.kwargs.get('collection_name', '')
+        context = super().get_context_data(*args, **kwargs)
+        context['collection_name'] = self.collection_name
+        context['collection'] = self.collection
+        context['form'] = self.get_form(initial=self.request.GET)
+
         return context
--- a/src/iconolab/templates/partials/header_search_form.html	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/templates/partials/header_search_form.html	Tue Jun 05 11:32:49 2018 +0200
@@ -1,8 +1,8 @@
 
 {% if collection_name %}
-<form class="navbar-form navbar-left" method="GET" action="{% url 'search_indexes:collection_haystack_search' collection_name %}" role="search">
+<form class="navbar-form navbar-left" method="GET" action="{% url 'search_indexes:collection_search' collection_name %}" role="search">
   {% else %}
-<form class="navbar-form navbar-left" method="GET" action="{% url 'search_indexes:haystack_search' %}" role="search">
+<form class="navbar-form navbar-left" method="GET" action="{% url 'search_indexes:global_search' %}" role="search">
 {% endif %}
   <div class="form-group">
     <input name="q" type="text" class="form-control" placeholder="Trouver ...">
--- a/src/iconolab/templates/search/annotation_search.html	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/templates/search/annotation_search.html	Tue Jun 05 11:32:49 2018 +0200
@@ -1,13 +1,13 @@
 {% extends 'iconolab_base.html' %}
 
 {% load thumbnail %}
-{% load iconolab_tags %} 
+{% load iconolab_tags %}
 
 {% block content %}
 
     <h2>Recherche</h2>
 
-    <form method="get" class="form-inline" action="{% if collection_name %}{% url 'search_indexes:collection_haystack_search' collection_name %}{% else %}{% url 'search_indexes:haystack_search' %}{% endif %}">
+    <form method="get" class="form-inline" action="{% if collection_name %}{% url 'search_indexes:collection_search' collection_name %}{% else %}{% url 'search_indexes:global_search' %}{% endif %}">
       <div class="form-group">
         <div class="input-group">
           <div class="input-group-addon">Chercher</div>
@@ -18,8 +18,8 @@
                 <option value="{{ val }}"{% if label == 'Annotations' %} selected{% endif %}>{{ label }}</option>
             {% endfor %}
           </select>
-          {% if form.collection_name %}
-            <div class="input-group-addon">du fonds {{form.collection.verbose_name}}</div>
+          {% if collection %}
+            <div class="input-group-addon">du fonds {{collection.verbose_name}}</div>
           {% else %}
             <div class="input-group-addon">du site</div>
           {% endif %}
@@ -27,7 +27,7 @@
       </div>
       <input type="submit" class="search-submit btn btn-primary" value="Rechercher" style="margin-left: 10px;">
     </form>
-    <h3><strong>{{ page_obj.paginator.count }}</strong> annotation(s)</h3>    
+    <h3><strong>{{ page_obj.paginator.count }}</strong> annotation(s)</h3>
     <ul class="annotation-list-wrapper list-inline">
       {% if not page_obj.object_list %}
           <h3 class="text-center"><small>Aucune annotation à afficher</small></p>
@@ -68,7 +68,7 @@
                 {% endfor %}
               </td>
               <td>
-                  {% include "partials/annotation_stats_panel.html" with annotation=result.object %}  
+                  {% include "partials/annotation_stats_panel.html" with annotation=result.object %}
               </td>
             </tr>
           {% endfor %}
@@ -78,16 +78,16 @@
     <ul class="pagination pull-right items-perpage" style="margin-left: 15px;">
       <li class="active pagination-label"><a>Annotations par page : </a></li>
       <li class="{% if page_obj.paginator.per_page == 5 %}active{% endif %}">
-      <a href="?q={{ query }}&perpage=5">5</a>               
+      <a href="?q={{ query }}&perpage=5">5</a>
       </li>
       <li class="{% if page_obj.paginator.per_page == 10 %}active{% endif %}">
-      <a href="?q={{ query }}&perpage=10">10</a>               
+      <a href="?q={{ query }}&perpage=10">10</a>
       </li>
       <li class="{% if page_obj.paginator.per_page == 25 %}active{% endif %}">
-      <a href="?q={{ query }}&perpage=25">25</a>               
+      <a href="?q={{ query }}&perpage=25">25</a>
       </li>
       <li class="{% if page_obj.paginator.per_page == 100 %}active{% endif %}">
-      <a href="?q={{ query }}&perpage=100">100</a>               
+      <a href="?q={{ query }}&perpage=100">100</a>
       </li>
     </ul>
     {% if page_obj.has_previous or page_obj.has_next %}
@@ -99,13 +99,13 @@
         </a>
       </li>
       {% endif %}
-      
+
       {% for page in page_obj.paginator.page_range %}
         <li id="page-link-{{page}}" class="pagination-link {% if page == page_obj.number %}active{% endif %}">
           <a {% if page != page_obj.number %}href="?q={{query}}&page={{page}}&perpage={{page_obj.paginator.per_page}}"{% endif %}>{{page}}</a>
         </li>
       {% endfor %}
-      
+
       {% if page_obj.has_next %}
       <li>
          <a href="?page={{page_obj.next_page_number}}&perpage={{page_obj.paginator.per_page}}" aria-label="Précédent">
@@ -115,4 +115,4 @@
       {% endif %}
     </ul>
     {% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
--- a/src/iconolab/templates/search/default_search.html	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/templates/search/default_search.html	Tue Jun 05 11:32:49 2018 +0200
@@ -1,13 +1,13 @@
 {% extends 'iconolab_base.html' %}
 
 {% load thumbnail %}
-{% load iconolab_tags %} 
+{% load iconolab_tags %}
 
 {% block content %}
 
     <h2>Recherche</h2>
 
-    <form method="get" class="form-inline" action="{% url 'search_indexes:haystack_search' %}">
+    <form method="get" class="form-inline" action="{% url 'search_indexes:global_search' %}">
     <div class="form-group">
       <div class="input-group">
         <div class="input-group-addon">Chercher</div>
@@ -18,8 +18,8 @@
               <option value="{{ val }}"{% if label == 'Images' %} selected{% endif %}>{{ label }}</option>
           {% endfor %}
         </select>
-        {% if form.collection_name %}
-          <div class="input-group-addon">du fonds {{form.collection.verbose_name}}</div>
+        {% if collection %}
+          <div class="input-group-addon">du fonds {{collection.verbose_name}}</div>
         {% else %}
           <div class="input-group-addon">du site</div>
         {% endif %}
@@ -27,4 +27,4 @@
     </div>
     <input type="submit" class="search-submit btn btn-primary" value="Rechercher" style="margin-left: 10px;">
   </form>>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
--- a/src/iconolab/templates/search/image_search.html	Wed May 16 00:22:05 2018 +0200
+++ b/src/iconolab/templates/search/image_search.html	Tue Jun 05 11:32:49 2018 +0200
@@ -1,13 +1,13 @@
 {% extends 'iconolab_base.html' %}
 
 {% load thumbnail %}
-{% load iconolab_tags %} 
+{% load iconolab_tags %}
 
 {% block content %}
 
   <h2>Recherche</h2>
 
-  <form method="get" class="form-inline" action="{% if collection_name %}{% url 'search_indexes:collection_haystack_search' collection_name %}{% else %}{% url 'search_indexes:haystack_search' %}{% endif %}">
+  <form method="get" class="form-inline" action="{% if collection_name %}{% url 'search_indexes:collection_search' collection_name %}{% else %}{% url 'search_indexes:global_search' %}{% endif %}">
     <div class="form-group">
       <div class="input-group">
         <div class="input-group-addon">Chercher</div>
@@ -18,8 +18,8 @@
               <option value="{{ val }}"{% if label == 'Images' %} selected{% endif %}>{{ label }}</option>
           {% endfor %}
         </select>
-        {% if form.collection_name %}
-          <div class="input-group-addon">du fonds {{form.collection.verbose_name}}</div>
+        {% if collection %}
+          <div class="input-group-addon">du fonds {{collection.verbose_name}}</div>
         {% else %}
           <div class="input-group-addon">du site</div>
         {% endif %}
@@ -56,16 +56,16 @@
       <ul class="pagination pull-right items-perpage" style="margin-left: 15px;">
         <li class="active pagination-label"><a>Objets par page : </a></li>
         <li class="{% if page_obj.paginator.per_page == 5 %}active{% endif %}">
-        <a href="?q={{ query }}&perpage=5">5</a>               
+        <a href="?q={{ query }}&perpage=5">5</a>
         </li>
         <li class="{% if page_obj.paginator.per_page == 10 %}active{% endif %}">
-        <a href="?q={{ query }}&perpage=10">10</a>               
+        <a href="?q={{ query }}&perpage=10">10</a>
         </li>
         <li class="{% if page_obj.paginator.per_page == 25 %}active{% endif %}">
-        <a href="?q={{ query }}&perpage=25">25</a>               
+        <a href="?q={{ query }}&perpage=25">25</a>
         </li>
         <li class="{% if page_obj.paginator.per_page == 100 %}active{% endif %}">
-        <a href="?q={{ query }}&perpage=100">100</a>               
+        <a href="?q={{ query }}&perpage=100">100</a>
         </li>
       </ul>
       {% if page_obj.has_previous or page_obj.has_next %}
@@ -77,13 +77,13 @@
             </a>
           </li>
           {% endif %}
-          
+
           {% for page in page_obj.paginator.page_range %}
             <li id="page-link-{{page}}" class="pagination-link {% if page == page_obj.number %}active{% endif %}">
               <a {% if page != page_obj.number %}href="?q={{query}}&page={{page}}&perpage={{page_obj.paginator.per_page}}"{% endif %}>{{page}}</a>
             </li>
           {% endfor %}
-          
+
           {% if page_obj.has_next %}
           <li>
              <a href="?page={{page_obj.next_page_number}}&perpage={{page_obj.paginator.per_page}}" aria-label="Précédent">
@@ -95,4 +95,4 @@
       {% endif %}
     </div>
   </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
--- a/src/setup.py	Wed May 16 00:22:05 2018 +0200
+++ b/src/setup.py	Tue Jun 05 11:32:49 2018 +0200
@@ -123,9 +123,11 @@
         setup_requires=['setuptools_scm'],
         install_requires=[
             "Django >= 2.0",
+            "django-appconf",
             "django-comments-xtd",
             "django-contrib-comments",
-            "django-haystack",
+            "elasticsearch-dsl >= 6.0, < 7.0",
+            "django-elasticsearch-dsl",
             "django-notifications-hq",
             "elasticsearch",
             "jsonfield",