changeset 753:ad53d929281a

For issue #62, upgrade Haystack from 1.2.7 to 2.1.0.
author Brian Neal <bgneal@gmail.com>
date Fri, 03 Jan 2014 19:01:18 -0600
parents 95f4e7f352fd
children a5a83971574b
files bio/search_indexes.py custom_search/forms.py custom_search/indexes.py custom_search/signals.py downloads/search_indexes.py forums/search_indexes.py news/search_indexes.py podcast/search_indexes.py requirements_dev.txt sg101/search_sites.py sg101/settings/base.py sg101/templates/search/search.html weblinks/search_indexes.py ygroup/search_indexes.py
diffstat 14 files changed, 321 insertions(+), 222 deletions(-) [+]
line wrap: on
line diff
--- a/bio/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/bio/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,30 +1,18 @@
 """Haystack search index for the bio application."""
-from haystack.indexes import *
-from haystack import site
-from custom_search.indexes import CondQueuedSearchIndex
+from haystack import indexes
 
 from bio.models import UserProfile
-from bio.signals import profile_content_update
 
 
-class UserProfileIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='user')
+class UserProfileIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='user')
 
-    def index_queryset(self):
+    def get_model(self):
+        return UserProfile
+
+    def index_queryset(self, using=None):
         return UserProfile.objects.filter(user__is_active=True)
 
     def get_updated_field(self):
         return 'update_date'
-
-    def _setup_save(self, model):
-        profile_content_update.connect(self.enqueue_save)
-
-    def _teardown_save(self, model):
-        profile_content_update.disconnect(self.enqueue_save)
-
-    def enqueue_save(self, sender, **kwargs):
-        return self.enqueue('update', sender)
-
-
-site.register(UserProfile, UserProfileIndex)
--- a/custom_search/forms.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/custom_search/forms.py	Fri Jan 03 19:01:18 2014 -0600
@@ -3,7 +3,10 @@
 our needs.
 
 """
+import logging
+
 from django import forms
+from django.conf import settings
 from haystack.forms import ModelSearchForm
 
 
@@ -18,20 +21,79 @@
     ('ygroup.post', 'Yahoo Group Archives'),
 )
 
+logger = logging.getLogger(__name__)
+
 
 class CustomModelSearchForm(ModelSearchForm):
     """
     This customized ModelSearchForm allows us to explictly label and order
     the model choices.
 
+    We also provide "all words", "exact phrase", and "exclude" text input boxes.
+    Haystack 2.1.0's auto_query() function did not seem to work right so we just
+    rolled our own.
+
     """
-    q = forms.CharField(required=False, label='',
-            widget=forms.TextInput(attrs={'type': 'search',
-                                          'class': 'search',
-                                          'size': 48,
-                                          }))
+    q = forms.CharField(required=False, label='All these words',
+            widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
+                'size': 48}))
+    exact = forms.CharField(required=False, label='This exact word or phrase',
+            widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
+                'size': 48}))
+    exclude = forms.CharField(required=False, label='None of these words',
+            widget=forms.TextInput(attrs={'type': 'search', 'class': 'search',
+                'size': 48}))
 
     def __init__(self, *args, **kwargs):
         super(CustomModelSearchForm, self).__init__(*args, **kwargs)
         self.fields['models'] = forms.MultipleChoiceField(choices=MODEL_CHOICES,
-                label='', widget=forms.CheckboxSelectMultiple)
+                label='Search in', widget=forms.CheckboxSelectMultiple)
+
+    def clean(self):
+        if not settings.SEARCH_QUEUE_ENABLED:
+            raise forms.ValidationError("Our search function is offline for "
+                    "maintenance. Please try again later. "
+                    "We apologize for any inconvenience.")
+
+        if not (self.cleaned_data['q'] or self.cleaned_data['exact'] or
+                self.cleaned_data['exclude']):
+            raise forms.ValidationError('Please supply some search terms')
+
+        return self.cleaned_data
+
+    def search(self):
+        if not self.is_valid():
+            return self.no_query_found()
+
+        logger.info('Search executed: /%s/%s/%s/ in %s',
+            self.cleaned_data['q'],
+            self.cleaned_data['exact'],
+            self.cleaned_data['exclude'],
+            self.cleaned_data['models'])
+
+        sqs = self.searchqueryset
+
+        # Note that in Haystack 2.x content is untrusted and is automatically
+        # auto-escaped for us.
+        #
+        # Filter on the q terms; these should be and'ed together:
+        terms = self.cleaned_data['q'].split()
+        for term in terms:
+            sqs = sqs.filter(content=term)
+
+        # Exact words or phrases:
+        if self.cleaned_data['exact']:
+            sqs = sqs.filter(content__exact=self.cleaned_data['exact'])
+
+        # Exclude terms:
+        terms = self.cleaned_data['exclude'].split()
+        for term in terms:
+            sqs = sqs.exclude(content=term)
+
+        if self.load_all:
+            sqs = sqs.load_all()
+
+        # Apply model filtering
+        sqs = sqs.models(*self.get_models())
+
+        return sqs
--- a/custom_search/indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-"""
-This module contains custom search indexes to tailor the Haystack search
-application to our needs.
-
-"""
-from queued_search.indexes import QueuedSearchIndex
-
-
-class CondQueuedSearchIndex(QueuedSearchIndex):
-    """
-    This customized version of QueuedSearchIndex conditionally enqueues items
-    to be indexed by calling the can_index() method.
-
-    """
-    def can_index(self, instance):
-        """
-        The default is to index all instances. Override this method to
-        customize the behavior. This will be called on all update operations.
-
-        """
-        return True
-
-    def enqueue(self, action, instance):
-        """
-        This method enqueues the instance only if the can_index() method
-        returns True.
-
-        """
-        if (action == 'update' and self.can_index(instance) or
-                action == 'delete'):
-            super(CondQueuedSearchIndex, self).enqueue(action, instance)
-
-
-class PublicQueuedSearchIndex(QueuedSearchIndex):
-    """QueuedSearchIndex for models with is_public attributes."""
-
-    def enqueue(self, action, instance):
-        """Conditionally enqueue actions as follows.
-
-        For update actions: if is_public is True, enqueue the update. If
-        is_public is False, enqueue a delete action.
-
-        Delete actions are always enqueued.
-
-        """
-        if action == 'update' and instance.is_public:
-            super(PublicQueuedSearchIndex, self).enqueue(action, instance)
-        elif (action == 'update' and not instance.is_public) or action == 'delete':
-            super(PublicQueuedSearchIndex, self).enqueue('delete', instance)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/custom_search/signals.py	Fri Jan 03 19:01:18 2014 -0600
@@ -0,0 +1,118 @@
+"""This module contains a custom Haystack signal processing class to update the
+search index in realtime. We update our search index by enqueuing edits and
+deletes into a queue for batch processing. Our class ensures we only enqueue
+content that should be in the search index.
+
+"""
+from django.db.models import signals
+import queued_search.signals
+
+from bio.signals import profile_content_update
+from forums.signals import topic_content_update, post_content_update
+
+import ygroup.models
+from weblinks.models import Link
+from podcast.models import Item
+from news.models import Story
+from downloads.models import Download
+from forums.models import Forum, Topic, Post
+from bio.models import UserProfile
+
+
+UID = 'custom_search.signals'
+
+
+class QueuedSignalProcessor(queued_search.signals.QueuedSignalProcessor):
+    """
+    This customized version of queued_search's QueuedSignalProcessor
+    conditionally enqueues items to be indexed.
+
+    """
+    def __init__(self, *args, **kwargs):
+
+        # We assume that it is okay to attempt to delete a model from the search
+        # index even if the model object is not in the index. In other words,
+        # attempting to delete an object from the index will not cause any
+        # errors if it is not in the index. Thus if we see an object that has an
+        # 'is_public' attribute, and it is false, we can safely enqueue a delete
+        # in case the 'is_public' attribute just went from True to False. We
+        # have no way of knowing that, it could have been False all along, but we
+        # just try the delete in case to be safe.
+
+        # To make the code easier to read, use a table to drive our signal
+        # connecting and disconnecting:
+        self.signal_chain = [
+            # Yahoo Group posts are always updated:
+            (signals.post_save, ygroup.models.Post, self.enqueue_save),
+            (signals.post_delete, ygroup.models.Post, self.enqueue_delete),
+
+            # Weblink Links are updated if they are public:
+            (signals.post_save, Link, self.enqueue_public_save),
+            (signals.post_delete, Link, self.enqueue_delete),
+
+            # Podcast Items are always updated:
+            (signals.post_save, Item, self.enqueue_save),
+            (signals.post_delete, Item, self.enqueue_delete),
+
+            # News Stories are always updated:
+            (signals.post_save, Story, self.enqueue_save),
+            (signals.post_delete, Story, self.enqueue_delete),
+
+            # Downloads are updated if they are public:
+            (signals.post_save, Download, self.enqueue_public_save),
+            (signals.post_delete, Download, self.enqueue_delete),
+
+            # Forum Topics are updated if they belong to a public forum:
+            (topic_content_update, None, self.enqueue_topic_save),
+            (signals.post_delete, Topic, self.enqueue_delete),
+
+            # Forum Posts are updated if they belong to a public forum:
+            (post_content_update, None, self.enqueue_post_save),
+            (signals.post_delete, Post, self.enqueue_delete),
+
+            # UserProfiles are updated when we receive a special signal:
+            (profile_content_update, None, self.enqueue_profile),
+            (signals.post_delete, UserProfile, self.enqueue_delete),
+        ]
+
+        super(QueuedSignalProcessor, self).__init__(*args, **kwargs)
+
+    def setup(self):
+        """We override setup() so we can attach signal handlers to only the
+        models we search on. In some cases we have custom signals to tell us
+        when to update the search index.
+
+        """
+        for signal, sender, receiver in self.signal_chain:
+            signal.connect(receiver, sender=sender, dispatch_uid=UID)
+
+    def teardown(self):
+        """Disconnect all signals we previously connected."""
+        for signal, sender, receiver in self.signal_chain:
+            signal.disconnect(receiver, sender=sender, dispatch_uid=UID)
+
+    def enqueue_public_save(self, sender, instance, **kwargs):
+        """Index only if the instance is_public.
+
+        If not, enqueue a delete just in case the is_public flag got flipped
+        from True to False.
+
+        """
+        if instance.is_public:
+            self.enqueue_save(sender, instance, **kwargs)
+        else:
+            self.enqueue_delete(sender, instance, **kwargs)
+
+    def enqueue_topic_save(self, sender, **kwargs):
+        """Enqueue only if the topic instance belongs to a public forum."""
+        if sender.forum.id in Forum.objects.public_forum_ids():
+            self.enqueue_save(Topic, sender, **kwargs)
+
+    def enqueue_post_save(self, sender, **kwargs):
+        """Enqueue only if the post instance belongs to a public forum."""
+        if sender.topic.forum.id in Forum.objects.public_forum_ids():
+            self.enqueue_save(Post, sender, **kwargs)
+
+    def enqueue_profile(self, sender, **kwargs):
+        """Forward the user profile instance on unconditionally."""
+        self.enqueue_save(UserProfile, sender, **kwargs)
--- a/downloads/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/downloads/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,20 +1,19 @@
 """Haystack search index for the downloads application."""
-from haystack.indexes import CharField, DateTimeField
-from haystack import site
-from custom_search.indexes import PublicQueuedSearchIndex
+from haystack import indexes
 
 from downloads.models import Download
 
 
-class DownloadIndex(PublicQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='user')
-    pub_date = DateTimeField(model_attr='date_added')
+class DownloadIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='user')
+    pub_date = indexes.DateTimeField(model_attr='date_added')
 
-    def index_queryset(self):
+    def get_model(self):
+        return Download
+
+    def index_queryset(self, using=None):
         return Download.public_objects.all()
 
     def get_updated_field(self):
         return 'update_date'
-
-site.register(Download, DownloadIndex)
--- a/forums/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/forums/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,60 +1,35 @@
 """Haystack search index for the weblinks application."""
-from haystack.indexes import *
-from haystack import site
-from custom_search.indexes import CondQueuedSearchIndex
+from haystack import indexes
 
 from forums.models import Forum, Topic, Post
-from forums.signals import topic_content_update, post_content_update
 
 
-class TopicIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='user')
-    pub_date = DateTimeField(model_attr='creation_date')
+class TopicIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='user')
+    pub_date = indexes.DateTimeField(model_attr='creation_date')
 
-    def index_queryset(self):
+    def get_model(self):
+        return Topic
+
+    def index_queryset(self, using=None):
         return Topic.objects.filter(forum__in=Forum.objects.public_forum_ids())
 
     def get_updated_field(self):
         return 'update_date'
 
-    def _setup_save(self, model):
-        topic_content_update.connect(self.enqueue_save)
 
-    def _teardown_save(self, model):
-        topic_content_update.disconnect(self.enqueue_save)
+class PostIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='user')
+    pub_date = indexes.DateTimeField(model_attr='creation_date')
 
-    def enqueue_save(self, sender, **kwargs):
-        return self.enqueue('update', sender)
+    def get_model(self):
+        return Post
 
-    def can_index(self, instance):
-        return instance.forum.id in Forum.objects.public_forum_ids()
-
-
-class PostIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='user')
-    pub_date = DateTimeField(model_attr='creation_date')
-
-    def index_queryset(self):
+    def index_queryset(self, using=None):
         return Post.objects.filter(
                 topic__forum__in=Forum.objects.public_forum_ids())
 
     def get_updated_field(self):
         return 'update_date'
-
-    def _setup_save(self, model):
-        post_content_update.connect(self.enqueue_save)
-
-    def _teardown_save(self, model):
-        post_content_update.disconnect(self.enqueue_save)
-
-    def enqueue_save(self, sender, **kwargs):
-        return self.enqueue('update', sender)
-
-    def can_index(self, instance):
-        return instance.topic.forum.id in Forum.objects.public_forum_ids()
-
-
-site.register(Topic, TopicIndex)
-site.register(Post, PostIndex)
--- a/news/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/news/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,18 +1,16 @@
 """Haystack search index for the news application."""
-from haystack.indexes import *
-from haystack import site
-from custom_search.indexes import CondQueuedSearchIndex
+from haystack import indexes
 
 from news.models import Story
 
 
-class StoryIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='submitter')
-    pub_date = DateTimeField(model_attr='date_submitted')
+class StoryIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='submitter')
+    pub_date = indexes.DateTimeField(model_attr='date_submitted')
+
+    def get_model(self):
+        return Story
 
     def get_updated_field(self):
         return 'update_date'
-
-
-site.register(Story, StoryIndex)
--- a/podcast/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/podcast/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,18 +1,16 @@
 """Haystack search index for the news application."""
-from haystack.indexes import *
-from haystack import site
-from custom_search.indexes import CondQueuedSearchIndex
+from haystack import indexes
 
 from podcast.models import Item
 
 
-class ItemIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='author')
-    pub_date = DateTimeField(model_attr='pubdate')
+class ItemIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='author')
+    pub_date = indexes.DateTimeField(model_attr='pubdate')
+
+    def get_model(self):
+        return Item
 
     def get_updated_field(self):
         return 'update_date'
-
-
-site.register(Item, ItemIndex)
--- a/requirements_dev.txt	Wed Jan 01 19:52:07 2014 -0600
+++ b/requirements_dev.txt	Fri Jan 03 19:01:18 2014 -0600
@@ -3,16 +3,16 @@
 MySQL-python==1.2.4
 django-debug-toolbar==1.0
 -e git+https://github.com/gremmie/django-elsewhere.git@1203bd331aba4c5d4e702cc4e64d807310f2b591#egg=django_elsewhere-dev
-django-haystack==1.2.7
+django-haystack==2.1.0
 django-tagging==0.3.1
 gdata==2.0.15
 html5lib==0.90
 pytz==2013b
-queued-search==1.0.4
-queues==0.6.1
+queued-search==2.1.0
+queues==0.6.3
 redis==2.7.2
 repoze.timeago==0.5
-xapian-haystack==1.1.5beta
+-e git+https://github.com/notanumber/xapian-haystack.git@37add92bc43fe50bf165e91f370269c26272f1eb#egg=xapian_haystack-dev
 anyjson==0.3.3
 celery==3.1.7
 django-picklefield==0.3.1
--- a/sg101/search_sites.py	Wed Jan 01 19:52:07 2014 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-import haystack
-haystack.autodiscover()
--- a/sg101/settings/base.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/sg101/settings/base.py	Fri Jan 03 19:01:18 2014 -0600
@@ -192,10 +192,14 @@
 #######################################################################
 # Haystack Search Settings
 #######################################################################
-HAYSTACK_SITECONF = 'sg101.search_sites'
-HAYSTACK_SEARCH_ENGINE = 'xapian'
-HAYSTACK_XAPIAN_PATH = os.path.join(PROJECT_PATH, 'xapian_index')
+HAYSTACK_SIGNAL_PROCESSOR = 'custom_search.signals.QueuedSignalProcessor'
 
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'xapian_backend.XapianEngine',
+        'PATH': os.path.join(PROJECT_PATH, 'xapian_index'),
+    },
+}
 
 #######################################################################
 # Redis integration & settings
@@ -297,7 +301,7 @@
 
 # If this flag is False, the queued_search queue will not be processed. This is
 # useful when we are rebuilding the search index.
-SEARCH_QUEUE_ENABLED = True
+SEARCH_QUEUE_ENABLED = False
 
 #######################################################################
 # Asynchronous settings (queues, queued_search, redis, celery, etc)
--- a/sg101/templates/search/search.html	Wed Jan 01 19:52:07 2014 -0600
+++ b/sg101/templates/search/search.html	Fri Jan 03 19:01:18 2014 -0600
@@ -28,60 +28,69 @@
 {% block content %}
 <h2>Search <img src="{{ STATIC_URL }}icons/magnifier.png" alt="Search" /></h2>
 <form id="search-form" method="get" action=".">
-{{ form.q }} <input type="submit" value="Search" />
 <fieldset>
-<legend>Search in:</legend>
-{{ form.models }}
-<p><a href="#" id="chk_all">Check all</a> | <a href="#" id="chk_none">Check none</a></p>
+<legend>Find content with:</legend>
+   <table>
+   {{ form.as_table }}
+   <tr>
+      <th></th>
+      <td><a href="#" id="chk_all">Check all</a> | <a href="#" id="chk_none">Check none</a></td>
+   </tr>
+   <tr>
+      <th></th>
+      <td><input type="submit" value="Search" /></td>
+   </tr>
+   </table>
 </fieldset>
 </form>
 
-  {% if query %}
-  <h3>Results for &quot;{{ query }}&quot; page {{ page.number }} of {{ page.paginator.num_pages }}</h3>
+{% if form.is_valid %}
+<h3>Search results; page {{ page.number }} of {{ page.paginator.num_pages }}</h3>
 
-     {% if page.paginator.count %}
-     <p>
-     <strong>{{ page.paginator.count }} hit{{ page.paginator.count|pluralize }}</strong>
-     </p>
-     {% endif %}
+   {% if page.paginator.count %}
+   <p>
+   <strong>{{ page.paginator.count }} hit{{ page.paginator.count|pluralize }}</strong>
+   </p>
+   {% endif %}
 
-      {% if page.object_list %}
-      <dl>
-      {% for result in page.object_list %}
-         <dt>
-         {{ result.verbose_name }}: <a href="{{ result.object.get_absolute_url }}">{{ result.object.search_title }}</a> ({{ result.score }})
-         </dt>
-         <dd>
-            {% highlight result.object.search_summary with query css_class "highlight" max_length 200 %}
-         </dd>
-      {% endfor %}
-      </dl>
-      {% else %}
-      <p>No results found for <em>{{ query }}</em>.</p>
-      {% endif %}
+   {% if page.object_list %}
+   <dl>
+   {% for result in page.object_list %}
+      <dt>
+      {{ result.verbose_name }}: <a href="{{ result.object.get_absolute_url }}">{{ result.object.search_title }}</a> ({{ result.score }})
+      </dt>
+      <dd>
+         {% highlight result.object.search_summary with query css_class "highlight" max_length 200 %}
+      </dd>
+   {% endfor %}
+   </dl>
+   {% else %}
+   <p>No search results found.</p>
+   {% endif %}
 
-      {% if page.has_previous or page.has_next %}
-          <div>
-              {% if page.has_previous %}<a href="?{% encode_params request.GET 'q' 'models' %}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; Previous{% if page.has_previous %}</a>{% endif %}
-              |
-              {% if page.has_next %}<a href="?{% encode_params request.GET 'q' 'models' %}&amp;page={{ page.next_page_number }}">{% endif %}Next &raquo;{% if page.has_next %}</a>{% endif %}
-          </div>
-      {% endif %}
-  {% else %}
-  <div>
-     <p>Thank you for using the SG101 search engine! Here are some searching tips that may help you find what you are looking for.</p>
-     <ul>
-        <li>To perform a search, type your search query in the text box and check which areas of the site you wish to search.</li>
-        <li>You can search multiple areas of the site at the same time by checking more than one box.</li>
-        <li>Your search terms are automatically <em>and-ed</em> together.</li>
-        <li>To search forum thread titles, check <em>Forum Topics</em>.</li>
-        <li>To search forum thread post bodies, check <em>Forum Posts</em>.</li>
-        <li>Our search engine removes punctuation when building its index. Thus, for example, a search query of
-         <em>FRV-1</em> will not produce any results. Instead, it is better to search for <em>FRV 1</em> or even
-         just <em>FRV</em>.</li>
-         <li>Most content is searchable by author. For example, to find all forum threads by the user <em>SurferDood101</em>
-         where the title contains <em>Gnarly</em>, your search term would be <em>SurferDood101 Gnarly</em>.</li>
-     </ul>
-  </div>
-  {% endif %}
+   {% if page.has_previous or page.has_next %}
+    <div>
+      {% if page.has_previous %}<a href="?{% encode_params request.GET 'q' 'exact' 'exclude' 'models' %}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; Previous{% if page.has_previous %}</a>{% endif %}
+      |
+      {% if page.has_next %}<a href="?{% encode_params request.GET 'q' 'exact' 'exclude' 'models' %}&amp;page={{ page.next_page_number }}">{% endif %}Next &raquo;{% if page.has_next %}</a>{% endif %}
+    </div>
+   {% endif %}
+{% else %}
+<div>
+<p>Thank you for using the SG101 search engine! Here are some searching tips that may help you find what you are looking for.</p>
+<ul>
+  <li>To perform a search, type your search query in the text boxes and check which areas of the site you wish to search.</li>
+  <li>The three text boxes will be AND-ed together. You can omit two of the text boxes.</li>
+  <li>You can search multiple areas of the site at the same time by checking more than one box.</li>
+  <li>To search forum thread titles, check <em>Forum Topics</em>.</li>
+  <li>To search forum thread post bodies, check <em>Forum Posts</em>.</li>
+  <li>Our search engine removes punctuation when building its index. Thus, for example, a search query of
+   <em>FRV-1</em> will not produce any results. Instead, it is better to search for <em>FRV 1</em> or even
+   just <em>FRV</em>.</li>
+   <li>Most content is searchable by author. For example, to find all forum threads by the user <em>SurferDood101</em>
+   where the title contains <em>Gnarly</em>, your search term (to be placed in the first text box) would be <em>SurferDood101 Gnarly</em>.</li>
+   <li>Search results are displayed newest first.</li>
+</ul>
+</div>
+{% endif %}
 {% endblock %}
--- a/weblinks/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/weblinks/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -1,20 +1,20 @@
 """Haystack search index for the weblinks application."""
-from haystack.indexes import CharField, DateTimeField
-from haystack import site
-from custom_search.indexes import PublicQueuedSearchIndex
+from haystack import indexes
 
 from weblinks.models import Link
 
 
-class LinkIndex(PublicQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    author = CharField(model_attr='user')
-    pub_date = DateTimeField(model_attr='date_added')
+class LinkIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    author = indexes.CharField(model_attr='user')
+    pub_date = indexes.DateTimeField(model_attr='date_added')
 
-    def index_queryset(self):
+    def get_model(self):
+        return Link
+
+    def index_queryset(self, using=None):
         return Link.public_objects.all()
 
     def get_updated_field(self):
         return 'update_date'
 
-site.register(Link, LinkIndex)
--- a/ygroup/search_indexes.py	Wed Jan 01 19:52:07 2014 -0600
+++ b/ygroup/search_indexes.py	Fri Jan 03 19:01:18 2014 -0600
@@ -2,19 +2,18 @@
 Haystack search index for the Yahoo Group archives application.
 
 """
-from haystack.indexes import *
-from haystack import site
-from custom_search.indexes import CondQueuedSearchIndex
+from haystack import indexes
 
 from ygroup.models import Post
 
 
-class PostIndex(CondQueuedSearchIndex):
-    text = CharField(document=True, use_template=True)
-    pub_date = DateTimeField(model_attr='creation_date')
+class PostIndex(indexes.SearchIndex, indexes.Indexable):
+    text = indexes.CharField(document=True, use_template=True)
+    pub_date = indexes.DateTimeField(model_attr='creation_date')
+
+    def get_model(self):
+        return Post
 
     def get_updated_field(self):
         return 'creation_date'
 
-
-site.register(Post, PostIndex)