.. _searchfield:
.. index::
single: Suche
single: Formular
single: Suchfeld
single: Template-Tag
single: Filter
single: Query-String
single: Elasticsearch
single: Haystack
single: Pagination
single: load
Eine Suche implementieren
**************************
Ein weiteres Feature für unseren Event-Manager soll ein Suchfeld sein, dass uns
ermöglicht, nach Events zu suchen. Dafür benötigen wir ein Formular mit einem
Suchfeld und eine Ausgabe-View.
Die Suchbox
---------------
In unserem ``base.html``-Layout haben wir oben im Navigationsbereich schon ein Suchfeld implementiert, welchem wir allerdings bisher noch kein Leben eingehaucht haben.
Gucken wir uns das Layout unter ``event_manager/event_manager/templates/base.html`` nochmal an und suchen die Stelle mit dem Suchformular heraus. Das ist etwa in der Zeile 39 der ``base.html``.
.. code-block:: html+django
Wir sehen ein Formular mit folgenden Eigenschaften:
* die HTTP-Methode ist GET
* das Formular hat keinen Submit-Button
* das Input-Feld ist vom Typ ``search`` und kann folglich via abgesendet werden
* Das Input-Feld hat das ``name``-Attribut mit dem Wert ``q``.
* das ``action``-Attribut ist momentan noch leer, es gibt also noch keinen Endpunkt, an den wir das Formular senden könnten.
Das ist also unser Suchformular. Um dem Formular Leben einzuhauchen, müssen wir eine View schreiben, eine URL definieren und ein Template bereitstellen.
Wenn das Formular per abgesendet wird, wird eine GET-URL in der Form ``http://www.example.com?q=suchwort`` gesendet werden. Der String nach dem Fragezeichen wird als ``Querystring`` bezeichnet, was nichts anderes als eine Key-Value-Liste ist. Diese Liste können wir in der View auswerten und darauf reagieren. Wir wollen den Key ``q`` auswerten, das steht für Question.
Mehr zum Thema Querystring auf Wikipedia:
``_
Die View für das Suchergebnis
--------------------------------
Beginnen wir mit der View für das Such-Ergebnis. Wir haben uns für eine generische ``List-View`` entschieden. Die Such-Ergebnisse allerdings wollen wir in ein anderes, eigenes Template rendern. Das müssen wir später unter ``events/event_search_results.html`` anlegen.
Außerdem soll das Such-Ergebnis paginierbar sein. Bauen wir die View also unter ``event_manager/events/views.py`` ein:
.. code-block:: python
from django.views.generic.list import ListView
from django.core.exceptions import BadRequest
class EventSearchView(ListView):
model = Event
template_name = 'events/event_search_results.html'
paginate_by = 10
queryset = Event.objects.prefetch_related("author", "category").all()
def get_queryset(self):
q = self.request.GET.get("q")
if not q:
return Event.objects.none()
result = (
queryset.filter(name__icontains=q)
| queryset.filter(sub_title__icontains=q)
)
return result
Um zu Erreichen, dass der GET-Parameter des Querystrings ``q`` ausgewertet
werden kann, überschreiben wir die ``get_queryset``-Methode. Zuerst holen wir
uns den Wert von ``q`` und geben eine leeres Queryset zurück, wenn
q keinen validen Wert hat. Dies erreichen wir mit der ``none-`` - Methode des
Model-Managers. Eine Anfrage a la ``http://example.com/search?=`` würde also in einer leeren Menge resultieren.
.. admonition:: Userfreundliche APIs und Eingabefelder
Wir hätten statt der leeren Menge, d.h. des leeren Querysets natürlich auch
einen Fehler werfen können und dem User einen ``BadRequest`` um die Ohren hauen
können. Ein ``BadRequest`` ist eine Exception mit dem HTTP-Statuscode ``ref``
Falls wir unter ``event_manager/templates`` eine eigene ``ref.html`` angelegt haben, würde
diese nun angezeigt. Allerdings nur, wenn wir im Produktivbetrieb sind und
``DEBUG`` auf False gesetzt haben, im Idealfall über unsere ``.env``-Datei.
Unsere Variante ist natürlich userfreundlicher und man sollte sich gut
überlegen, wann Eingaben in Fehler resultieren.
Hat ``q`` einen Wert, filtern wir diese Suchzeichenkette via ``icontains``, dh.
unter Nichtberücksichtigung der Groß-und-Kleinschreibung, die Namen und
Sub-Title-Felder der Events. Mit dem ``|``-Symbol erreichen wir eine
``ODER``-Operation. Die Suchzeichenkette muss also entweder im Namen des Events
oder in seinem Sub-Titel vorkommen.
Das Ergebnis der Suche wird zurückgegeben und in der
``event_manager/events/templates/events/event_search_results.html`` ausgegeben
werden.
.. admonition:: professionelle Volltextsuchen
Unsere kleine, selbstgebastelte Suche wird später ganz gut funktionieren und
wird sich auch im Einsatz einer kleinen Web-Applikation ganz gut machen. Für
professionelle Ansprüche mit vielen Hundertausenden Anfragen und Suchen, die
auch die auch komplexere Fragemuster, boolsche Operatoren oder Negationen
berücksichtigen, wird sie allerdings nicht ausreichen.
Hier sei auf ``Elasticsearch`` verwiesen. ``Elasticsearch`` ist eine Open-Source-, REST-konforme, verteilte ``Such- und Analyse-Engine``, die auf ``Apache Lucene`` basiert und in ``Java`` geschrieben wurde.
Seit ihrer Veröffentlichung 2010 hat sich Elasticsearch schnell zur
beliebtesten Suchmaschine entwickelt. Sie wird häufig für Log-Analysen,
Volltextsuche, Geschäftsanalysen und Betriebsinformationen verwendet.
Mit ``Django Elasticsearch DSL`` und ``Django Elasticsearch DSL DRF``
stehen neben ``Haystack`` gute Python-Packages zur Verfügung, um Django und
Elasticsearch komfortabel zu betreiben.
URLs für die Event App
------------------------
Wir erweitern die URLS der Event-App unter ``event_manager/events/urls.py`` und
tragen folgenden Path für in die ``urlpatterns`` ein:
.. code-block:: python
app_name = "events"
urlpatterns = [
path("search", views.EventSearchView.as_view(), name="event_search"),
...
]
HTML für die Ausgabe der Suchergebnisse
--------------------------------------------
für die Ausgabe benötigen wir ein Template. Das legen wir unter
``event_manager/events/templates/events/event_search_results.html`` an.
.. code-block:: html+django
{% extends 'base.html' %}
{% load i18n %}
{% block head%}
{% trans 'Search Results' %}
{% endblock %}
{% block content %}
{% include "../snippets/paginator.html" %}
{%endblock%}
Wenn wir jetzt einen Such-String in die Suchbox eingeben und mit
bestätigen, sollten uns die Suchergebnisse jetzt sauber ausgegeben werden.
Allerdings gibt es noch einen kleinen Schönheitsfehler: die Pagination klappt
nicht richtig. Bei einer Suche nach
``http://127.0.0.1:8000/events/search?q=as`` gehen unsere ``GET-Parameter``
verloren, wenn wir mit dem NEXT-Button eine Seite weiterklicken wollen. Das
resultiert knallhart in einem ``BadRequest ref``-Fehler, weil der
``q``-Parameter nicht vorhanden ist.
Um dieses Problem zu umgehen, müssen wir das ``paginator.html`` umbauen und uns einen eigenen ``Template-Tag`` schreiben.
Schauen wir uns das Paginator-Snippet unter ``event_manager/templates/snippets/paginator.html`` nochmal genauer an:
.. code-block:: html+django
{% if is_paginated %}
{% endif %}
Im Grunde prüfen wir, ob die Ausgabe überhaupt paginiert ist (``{% if
is_paginated %}``). Falls das zutrifft, prüfen wir mit ``if
page_obj.has_next``, ob es eine weitere Seite mit Suchergebnissen gibt. In
diesem Fall stellen wir einen NEXT-Button da, um auf die nächste Seite zu
gelangen. Dieses Steuern der Anzeigeseiten geschieht über den
Query-String-Parameter ``page``.
Die URL ``http://127.0.0.1:8000/events/search?page=2`` würde uns also zur zweiten Seite mit Suchergebnissen führen.
.. admonition:: Wie funktioniert Pagination?
**aus der Doku:** Django provides high-level and low-level ways to help you
manage paginated data – that is, data that’s split across several pages,
with “Previous/Next” links.
Nehmen wir an, wir haben 100 Suchergebnisse erhalten, die wir dem User auf
einer Ergebnis-Seite darstellen wollen. Oftmals ist es so, dass wir dem User
nicht die gesamte Menge auf einmal präsentieren wollen, sondern eher
häppchenweise. Google macht das zum Beispiel bei den Suchergebnissen so.
Dieses häppchenweise Darstellen von Datenmengen nennt sich Pagination. Wir
erstellen quasi Unterseiten mit einer festen Anzahl an Ergebnissen auf
mehrere Seiten verteilt.
Wenn wir die Anzahl auf 10 Datensätze pro Seite festlegen und wir 100
Datensätze haben, benötigen wir 10 Darstellungsseiten. Das sind die
``pages``. ``page=1`` wären die ersten 10 Treffer, ``page=2`` die zweiten 10
Treffer und so weiter.
Mit der Angabe des Parameters ``?page=1`` in der URL erstellt Django anhand
der ``LIMIT``-Klauses aus SQL eine Anfrage an die Datenbank und selektiert
die ersten 10 Einträge. Festgelegt hatten wir die Anzahl der Einträge pro
Seite in der ``EventSearchView`` mit der Eigenschaft ``paginate_by=10``.
Weiterhin wird mit ``page_obj.has_previous`` geprüft, ob es eine Vorgänger
Seite gibt, um einen PREV-Button anzuzeigen.
Wie man sehen kann, wird mit ``?page={{ page_obj.next_page_number }}`` die
page-Eigenschaft dynamisch gesetzt. Allerdings gehen an dieser Stelle alle
anderen Querystring-Parameter verloren.
Richtig müsste unser Querystring in etwa so aussehen: ``?q=as&page={{
page_obj.next_page_number }}``. Wir würden also den Wert von ``q`` mit jeder
Anfrage mitschleifen und würden die Suchanfrage nicht verlieren.
Puh, das war kompliziert. Im Grunde ist es aber relativ einfach. Um das Problem
zu lösen, gibt es viele Wege. Wir gehen den Weg über einen eigenen
``Template-Tag``.
das Verzeichnis für unseren Template-Tag
--------------------------------------------
Legen wir unter ``event_manager/pages/`` ein neues Verzeichnis namens
``templatetags`` an und erstellen in diesem neuen Verzeichnis eine Datei namens
``pagestags.py``. Um daraus ein Package zu erstellen kommt auch eine
``__init__.py`` hinein.
So in etwa sollte unser Verzeichnis ``pages`` nun aussehen (nur die wichtigsten
Dateien und Verzeichnisse):
.. code-block:: bash
pages
├── admin.py
├── models.py
├── templates
├── templatetags
│ ├── pagestags.py
│ ├── __init__.py
├── tests
├── urls.py
└── ..
Die Datei ``pagestags.py`` wird der Ort, wo wir ``Template-Filter`` und
``Template-Tags`` für die Pages-App anlegen. Dieser Dateiname ist auch der
Name, den wir in dem ``load``-Tag nutzen werden, um alle Tags, die hier
definiert sind, später im Template nutzen zu können.
.. admonition:: Reminder: was sind nochmal diese Template-Tags?
Template-Tags sind Anweisungen, um den Kontrollfluss im Template zu
kontrollieren, zum Beispiel das Iterieren über eine Collection oder das
Verzweigen mit einer if-Abfrage.
Tags sind komplexer als Variablen: Einige erstellen Text in der Ausgabe,
andere steuern den Fluss durch Ausführen von Schleifen oder Logik und einige
laden externe Informationen in die Vorlage, die von Variablen verwendet
werden können. Tags bieten also Logik im ansonsten statischen
Rendering-Prozess.
Wir haben im Buch bisher einige Tags kennengelernt, zum Beispiel den
``url``-Tag. ``{% url 'events:events' %}`` generiert uns den Hyperlink zur
der Ausgabe aller Events.
Um eigene Tags bzw. Tags aus Drittanbieter-Packages nutzen zu können, müssen
sie im jeweiligen Template mit der ``load``-Direktive geladen werden. Unsere
Pages-Tags werden mit ``{% load pagestags %}`` im Template nutzbar gemacht.
Einen eigenen Template-Tag erstellen
---------------------------------------
Wir wollen jetzt einen Template-Tag erstellen, der uns in der Paginierungslogik
generell eigene ``GET-Parameter`` berücksichtigt, nicht nur von der Event-App.
Deshalb erstellen wir den Template-Tag auch in der ``pages`` App.
Mehr zum Erstellen von eigenen Template-Tags und Filtern findet sich in der
Django-Doku:
``_
In die Datei ``event_manager/pages/templatetags/pagestags.py`` schreiben wir folgenden Inhalt:
.. code-block:: python
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def safe_query_string(context, **kwargs) -> str:
"""create url-encoded string from GET-parameters.
get querydict from GET:
next_page:
return q=a&page=3
previous_page:
return q=a&page=1
"""
query = context['request'].GET.copy()
for k, v in kwargs.items():
query[k] = v
return query.urlencode()
Wir importieren aus Django das Template-Package und erstellen die
``register``-Variable, die eine Instanz ``template.Library`` ist. Dort sind
alle Templatetags registriert. Unsere Pages-Tags aus der ``pagestags.py``
können in den Templates via ``{% load pagestags %}`` geladen und benutzt
werden.
Der Tag selbst heisst ``safe_query_string`` und kann im Template genutzt in der
Form ``{% safe_query_string %}`` werden. Die Funktion erstellt uns also in
Abhängigkeit der aktuellen Page einen QueryString mit dem ``q``-Wert und der
``page``.
Überarbeiten wir das Paginator-Snippet unter
``event_manager/templates/snippets/paginator.html``. Zuerst laden wir mit
``load`` die Template-Tags und ersetzen das Erzeugen der Links durch diese
Neuerungen:
.. code-block:: html+django
{% load pagestags %}
{% if is_paginated %}