Haystack, Whoosh i błąd


23 listopada 2013


Wyszukiwanie pełnotekstowe w Django z wykorzystaniem Haystack oraz Whoosh a także rozwiązanie problemu, który uniemożliwia filtrowanie danych dla wybranego modelu.

Wyszukiwanie pełnotekstowe, w porównaniu do zwykłych zapytań z użyciem "LIKE", jest szybszą operacją ponieważ przeszukiwany jest "index" a nie wszystkie rekordy w tabeli bazy danych. Nie jesteśmy ograniczeni do wyszukiwania wyłącznie po zadanych parametrach umieszczonych w formularzu ale możemy przeszukiwać także opisy tekstowe. "Full text search" posiada wiele zalet ale wymaga także aby "index" był uaktualniany co w przypadku bardzo dużej liczby danych może być operacją czasochłonną. 

W niniejszym wpisie zaprezentuję w jaki sposób korzystać z Haystack oraz Whoosh. 
Whoosh to stworzona w Pythonie biblioteka służąca do indeksowania oraz wyszukiwania danych wykorzystująca wyszukiwanie pełnotekstowe. 
Haystack dostarcza mechanizm wyszukiwania w Django, który może wykorzystywać takie mechanizmy indeksowania jak: Solr, Elasticsearch, Whoosh, Xapian oraz inne. Z uwagi na to, że Whoosh został napisany w Pythonie i nie wymaga uruchomienia serwera wyszukiwania postanowiłem w jednym z moich projektów wykorzystać właśnie tę bibliotekę. 

Zaprezentuję jak zainstalować oraz zintegrować oba narzędzia z Django. 

1. Instalacja

Instalujemy django-haystack oraz Whoosh:

pip install Whoosh 
pip install django-haystack

2. Konfiguracja Django

W settings.py dodajemy informację o haystack oraz wykorzystywanym mechanizmie indeksującym (konfiguracja połączenia):

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
        'PATH': os.path.join(os.path.dirname(__file__), 'whoosh'),
    },
} 

Zmienna PATH określa katalog, w którym Whoosh stworzy indeksy. 
Do krotki INSTALLED_APPS dodajemy wpis:

INSTALLED_APPS = (
    ...
    'haystack',
    ...
)

Gdy uruchomimy polecenie "manage.py" bez żadnych parametrów zobaczymy, że zostało one wzbogacone o nowe parametry dotyczące haystack: build_solr_schema, clear_index, haystack_info, rebuild_index, update_index

3. Konfiguracja urls.py

Wpis może wyglądać następująco:

from haystack.views import basic_search
urlpatterns = patterns('',
...
url(r'search/$', basic_search, name='search'),
...
)

4. Definicja indeksów

Załóżmy, że mamy przykładowy model w aplikacji "kalendarz":

from django.db import models

class Dzien(models.Model):
    nazwa = models.CharField(max_length=100)
    skrot = models.CharField(max_length=10)
    usuniety = models.BooleanField(default=0, editable=Fals

W katalogu aplikacji "kalendarz" musimy umieścić plik: "search_indexes.py", w którym umieścimy definicję indeksu:

from haystack import indexes
from .models import Dzien

class DzienIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    nazwa = indexes.CharField(model_attr='nazwa')
    skrot = indexes.CharField(model_attr='skrot')
    rendered = indexes.CharField(use_template=True, indexed=False)

    def get_model(self):
        return Dzien

    def index_queryset(self, using=None):
        return self.get_model().objects.filter(usuniety=0)

W klasie wpis zmienna "rendered" określa, że podczas generowania wyników wyszukiwania dla tego indeksu będzie użyty szablon, dzięki czemu później w szablonie wyników wyszukiwania nie będzie trzeba sprawdzać instancji danego obiektu i "includować szablonu". Haystack zrobi to za nas. 

W kolejnym kroku tworzymy w katalogu z szablonami katalog "search" oraz podkatalogi "indexes/kalendarz/". W katalogu "indexes" tworzymy katalogi odpowiadające nazwie aplikacji, dla której tworzymy indeksy. Następnie tworzymy dwa pliki: dzien_text.txt (używany podczas tworzenia indeksu) oraz dzien_rendered.txt (wykorzystywany podczas generowania wyników wyszukiwania) 

Zawartość pliku: dzien_text.txt:

{{ object.nazwa }} 
{{ object.skrot }} 

Zawartość pliku: dzien_rendered.txt

<h2>{{ object.nazwa }}</h2>
<h2>{{ object.skrot }}</h2>

5. Formularz wyszukiwania

Haystack tworzy formularz wyszukiwania, który musimy umieścić w szablonie: "search.html" w katalogu "search":

{% block content %}

    <form method="get" action=".">
        <table>
            {{ form.as_table }}
            <tr>
                <td> </td>
                <td>
                    <input type="submit" value="Submit">
                </td>
            </tr>
        </table>

        {% if query %}
            <h3>Results</h3>

            {% for result in page.object_list %}
                <p>
                    <div class="search_result">
        			{{ result.rendered|safe }}
    				</div>
                </p>
            {% empty %}
                <p>No results found.</p>
            {% endfor %}

            {% if page.has_previous or page.has_next %}
            <div>
            {% if page.has_previous %}
	      <a href="?q={{ query }}&page={{ page.previous_page_number }}">
            {% endif %}
            « Previous{% if page.has_previous %}</a>{% endif %}
            |
            {% if page.has_next %}<a href="?q={{ query }}
	      &page={{ page.next_page_number }}">
	    {% endif %}
            Next »{% if page.has_next %}</a>{% endif %}
                </div>
            {% endif %}
        {% else %}
            {# Show some example queries to run, maybe 
               query syntax, something else? #}
        {% endif %}
    </form>
{% endblock %}

6. Tworzenie indeksów

Jeżeli w naszej bazie danych istnieją już rekordy dla modelu "Dzien" możemy przystąpić do tworzenia indeksów. Aby stworzyć indeksy należy z poziomu konsoli wydać polecenie:

manage.py rebuild_index

Haystack automatycznie wykona indeksowanie z wykorzystaniem Whoosh dla wszystkich modeli zdefiniowanych w pliku "search_indexes.py". Jeżeli mamy więcej aplikacji to w dla każdej aplikacji tworzymy plik "search_indexes.py". 
Po wykonaniu indeksowania powinniśmy otrzymać w konsoli (jeżeli posiadaliśmy 2 rekordy spełniające warunki indeksowania - indeksuje tylko nieusunięte ):

WARNING: This will irreparably remove EVERYTHING from your search index in 
connection 'default'.
Your choices after this are to restore from backups or rebuild via 
the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 2 dziens

Jeżeli uruchomimy aplikację i przejdziemy pod adres "search" otrzymamy wyszukiwarkę pełnotekstową z możliwością wyboru modeli, w których będziemy wyszukiwali (w następnym punkcie rozwiązanie błędu w przypadku gdy mamy stworzone indeksy dla kilku modeli). 

7. Błąd z wyszukiwaniem danych w wielu modelach

Jeżeli stworzyliśmy kilka indeksów dla różnych modeli (np. tworzymy drugi indeks dla modelu "Miesiąc") to po wykonaniu wszystkich czynności zawsze będzie nam wyszukiwało we wszystkich modelach (bez względu na zaznaczone modele w formularzu wyszukiwania).

Problem występuje wyłącznie w pakietach "django-haystack" w wersji (2.0 oraz 2.1). Wersja 2.1 jest obecnie instalowana przez "pip" domyślnie. Aby rozwiązać problem należy zainstalować wersję co najmniej 2.1.1 (deweloperska).

pip install -e 
git+https://github.com/toastdriven/django-haystack.git@master#egg=django-haystack

Gdy zainstalujemy nowszą wersję (bez błędu rozwiązanego w commicie: tutaj) wyszukiwarka będzie działała poprawnie.

Podsumowanie

Wyszukiwanie pełnotekstowe w Django wykorzystujące Haystack sprawia, że nie jesteśmy ograniczeni do jednego mechanizmu indeksującego. Oprócz Whoosh możemy użyć  Sorl, Xapian itd. wyłącznie zmieniając konfigurację aplikacji.
Więcej informacji o Haystack dostępnych jest w dokumentacji dostępnej pod adresem: tutaj.

Co zawiera blog?

Na blogu umieszczam wpisy dotyczące mojej pracy, zainteresowań. Głowna tematyka to programowanie oraz recenzje płyt oraz książek.

Underworld - dubnobasswithmyheadman


Underworld to historia muzyki techno, house i trance

Boards Of Canada - Music Has The Right ...


Jedna z najważniejszych płyt IMD, ambient, downtempo

Schematy finansowe


"Bogaty albo biedny. Po prostu różni mentalnie". Wrażenia.

The Prodigy - The Day Is My Enemy Remixed


Co sądzę o płycie z remixami i bonusowymi utworami?

The Avalanches - Since I Left You


Od zawsze fascynowała mnie muzyka tworzona przy użyciu "czarnych krążków" ...

Wieczne Blue Lines


Artwork: Michael Nash, 3D-Del Naja
Ćwierćwiecze jednej z najważniejszych płyt ...