Blog

Thomas Wanschik on August 11, 2010

nonrel-search updates: auto-completion and separate indexing

It was planned already very long to add some remaining features from gae-search to nonrel-search and since we stopped developing gae-search we decided to make some of the premium features open-source. So let's see what changed.

New Features

We basically changed two things in nonrel-search: first it's possible to index a model via a separate definition i.e. without having to modify the model's source itself and second you can use our auto-completion feature from the good old gae-search days. :)

Separate indexing

So let's say you want to index some of your models. With the old version of nonrel-search you had to add a SearchManager to each model you want to search for. With the latest version of nonrel-search you have to define these indexes separately from your model like this:

# post.models
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=500)
    content = models.TextField()
    author = models.CharField(max_length=500)
    category = models.CharField(max_length=500)
# post.search_indexes
import search
from search.core import porter_stemmer
from post.models import Post

# index used to retrieve posts using the title, content or the
# category.
search.register(Post, ('title', 'content','category', ),
    indexer=porter_stemmer)

As you can see we use the new register function to make posts searchable by title, content and category leaving the author aside. The first parameter defines the model you want to index. The remaining parameters are just the same as for the old SearchManager. The register function automatically adds an index called 'search_index'. Of course it's possible to add multiple such search indexes, just register more of them and pass in a name for the index:

# post.search_indexes
...
search.register(Post, ('category', 'title' ),
    indexer=porter_stemmer,
    search_index='second_search_index')

Here we define a new index called 'second_search_index'.

In addition to defining the indexes we have to make sure that the register function will be executed. Nonrel-search provides a function called autodiscover which automatically searches the INSTALLED_APPS for "search_indexes.py" modules and registers all search indexes.

# search for "search_indexes.py" in all installed apps
import search
search.autodiscover()

You should call autodiscover in your settings.AUTOLOAD_SITECONF module to make sure that your indexes get loaded. See django-autoload for more information on auto-loading modules (so don't forget to install django-autoload). Now it's possible to search for models using the newly added search function:

from search.core import search
posts = search(Post, 'Hello world')

search takes two arguments: the first specifiing the model to search for and the second argument specifies the query used for searching. search automatically uses the index 'search_index'. If you want to use a different index just pass in the name of the desired index:

from search.core import search
# use the auto-completion index explained in the next section
posts = search(Post, 'Hello world', search_index='second_search_index')

Defining indexes separately from the model definition is especially useful for already existing Django apps so you can make them searchable without having to modify their source code. For example, it's possible to make users searchable via their first name and last name just by adding a search index in a separate module.

Auto-completion or "suggest-as-you-type"

Auto-completion is the first premium feature we make open-source. Let's say you want to add auto-completion for category names while creating posts. Nonrel-search makes this an easy job. In order to do so you first have to register a search index which uses the startswith indexer:

# post.search_indexes
...
# auto-completion index used to suggest categories
search.register(Post, ('category', ), indexer=startswith,
    search_index='autocomplete_index')

Then you can use the LiveSearchField which can be integrated into forms to define an auto-completion form:

# post.forms
from django import forms
from post.models import Post
from search.forms import LiveSearchField

class CreatePostForm(forms.ModelForm):
    category = LiveSearchField('/post/live_search/')

    class Meta:
        model = Post

Just pass LiveSearchField the URL to the auto-complete view which retrieves your posts. You can also configure the auto-completion behavior with additional parameters. See the documentation for more information. You can then use this form in your templates to display an auto-completed input field. So, the only thing left is a view returning the data required for auto-completion and to include the necessary Javascript / CSS files into your html:

# post.views
from post.models import Post
from search.views import live_search_results

def live_search(request):
 return live_search_results(request, Post, search_index='autocomplete_index',
     result_item_formatting=
         lambda post: {'value': u'<div>%s</div>' % (post.category),
         'result': post.category, })

Here, we use the function live_search_results and pass in the model class to search on, the name of the index to use for searching (default is 'search_index'), and a formatting function which specifies how your auto-completed posts will be displayed. Note that this function returns a dictionary having two items: 'value' specifies how to display your posts and 'result' specifies what to put into the input field when selecting a post from the auto-completed results list.

If you don't specify any result_item_formatting function, 'value' will be the escaped value of the first indexed property and 'result' will be the unescaped value of the same property.

Note that request has to include a GET parameter 'query'.

Here is the remaining code you have to add so that all necessary Javascript / CSS files will be included using the django-mediagenerator:

# settings
# list your css and js data here
MEDIA_BUNDLES = (
    ('main.css',
        ...,
        'search/jquery.autocomplete.css',
        'search/search.css',
    ),
    ('main.js',
        ...,
        'jquery.js',
        'jquery.livequery.js',
        'search/jquery.autocomplete.js',
        'search/autocomplete_activator.js',
    ),
)

and in your templates add this:

...
{% block css %}
  {% include_media 'main.css' %}
{% endblock %}

{% block js %}
  {% include_media 'main.js' %}
{% endblock %}

You can download the nonrel-search-testapp to get started and play around with nonrel-search. If you create some nice app using nonrel-search please let us know.