Django 5 для начинающих

Прогресс по курсу:  9/1004

6.11 Добавление полнотекстового поиска в блог
4 из 4 шагов пройдено

Разработка представления поиска

Теперь вы создадите конкретно-прикладное представление, позволяющее пользователям выполнять поиск постов. Прежде всего понадобится форма для поиска.

Отредактируйте файл forms.py приложения blog, добавив следующую ниже форму:

class SearchForm(forms.Form):
    query = forms.CharField()

Поле запроса будет использоваться для того, чтобы давать пользователям возможность вводить поисковые запросы.

Отредактируйте файл views.py приложения blog, добавив следующий ниже исходный код:

# ...
from django.contrib.postgres.search import SearchVector
from .forms import EmailPostForm, CommentForm, SearchForm

# ...

def post_search(request):
    form = SearchForm()
    query = None
    results = []

    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            results = Post.published.annotate(
                search=SearchVector('title', 'body'),
            ).filter(search=query)

    return render(request,
                  'blog/post/search.html',
                  {'form': form,
                   'query': query,
                   'results': results})

В приведенном выше представлении сначала создается экземпляр формы SearchForm.

Для проверки того, что форма была передана на обработку, в словаре request.GET отыскивается параметр query.

Форма отправляется методом GET, а не методом POST, чтобы результирующий URL-адрес содержал параметр query и им было легко делиться.

После передачи формы на обработку создается ее экземпляр, используя переданные данные GET, и проверяется валидность данных формы.

Если форма валидна, то с помощью конкретно-прикладного экземпляра SearchVector, сформированного с использованием полей title и body, выполняется поиск опубликованных постов.

Теперь представление поиска готово и необходимо создать шаблон отображения формы и результатов при выполнении пользователем поиска.

Внутри каталога templates/blog/post/ создайте новый файл, назовите его search.html и добавьте в него следующий ниже исходный код:

{% extends "blog/base.html" %} {% load blog_tags %}

{% block title %}Search{% endblock %}

{% block content %} {% if query %}
    <h1>
        Posts containing "{{ query }}"
    </h1>
    <h3>
        {% with results.count as total_results %}
            Found {{ total_results }} result{{ total_results|pluralize }} {% endwith %}
    </h3>
    {% for post in results %}
        <h4>
            <a href="{{ post.get_absolute_url }}"> {{ post.title }}
            </a>
        </h4>
        {{ post.body|markdown|truncatewords_html:12 }} {% empty %}
        <p>There are no results for your query.</p>
    {% endfor %}
    <p><a href="{% url 'blog:post_search' %}">Search again</a></p> {% else %}
    <h1>
        Search for posts
    </h1>
    <form method="get">
        {{ form.as_p }}
        <input type="submit" value="Search">
    </form>
{% endif %} {% endblock %}

Как и в представлении поиска, по наличию параметра query определяется, что форма была передана на обработку.

Перед передачей запроса мы отображаем форму и кнопку передачи формы.

После передачи формы поиска на обработку отображается выполненный запрос, общее число результатов и список постов, совпадающих с поисковым запросом.

Наконец, отредактируйте файл urls.py приложения blog, добавив следующий ниже шаблон URL-адреса:

urlpatterns = [
# представления поста
    path('', views.post_list, name='post_list'),
# path('', views.PostListView.as_view(), name='post_list'),
    path('tag/<slug:tag_slug>/',
         views.post_list, name='post_list_by_tag'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/',
         views.post_detail, name='post_detail'),
    path('<int:post_id>/share/', views.post_share, name='post_share'),
    path('<int:post_id>/comment/', views.post_comment, name='post_comment'),
    path('search/', views.post_search, name='post_search'),

]

Далее пройдите по URL-адресу http://127.0.0.1:8000/blog/search/ в своем браузере. Вы должны увидеть следующую ниже форму для поиска:

Введите запрос и кликните по кнопке SEARCH(Найти). Вы увидите результаты поискового запроса, как показано ниже:

Поздравляю! Вы создали базовый поисковый механизм для своего блога.


  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

Какое-то странное поведение:

Если ищу по одному из слов 'ok', 'run', 'blog', находит пост

А если ищу по 'here', 'we', 'this', не находит:

@Марат_Асылбаев, это стоп слова, поэтому нет результатов, в начале следующего шага про них рассказывается.

@Дмитрий_Селезнев, да, теперь понял)

results = Post.published.annotate( search=SearchVector('title', 'body'), ).filter(search=query)

тут получается каждый объект из QuerySet аннотирован текстом из 'title' и 'body'

а потом с помощью фильтрации мы оставляем только те объекты в которых встречается искомое слово, верно? 

@Виктор_Русинович, да, верно.

@Виктор_Русинович, Можно посмотреть в SQL, что происходит:

In [12]: Post.published_only.annotate(search=SearchVector('title', 'body'),).filter(search='django')
Out[12]: SELECT "blog_post"."id",
       "blog_post"."title",
       "blog_post"."slug",
       "blog_post"."author_id",
       "blog_post"."body",
       "blog_post"."published",
       "blog_post"."created",
       "blog_post"."updated",
       "blog_post"."status",
       to_tsvector(COALESCE("blog_post"."title", '') || ' ' || COALESCE("blog_post"."body", '')) AS "search"
  FROM "blog_post"
 WHERE ("blog_post"."status" = 'PB' AND to_tsvector(COALESCE("blog_post"."title", '') || ' ' || COALESCE("blog_post"."body", '')) @@ (plainto_tsquery('django')))
 ORDER BY "blog_post"."published" DESC
 LIMIT 21

Execution time: 0.002001s [Database: default]
<QuerySet [<Post: Пример поста>]>

У меня поле publish заменено на published, чтобы не было соблазна бездумно копировать код, не разбираясь. Так же и имя менеджера изменено

Изменен ilya kutaev

я так понимаю этот способ поиска не учитывает регистр правильно?

@Ilia_Boiarintsev, да, верно.

@Дмитрий_Селезнев, А в чем "неправильность"? Поиск регистронезависим, и это скорее правильно, чем нет

Post.objects.annotate(search=SearchVector('title', 'body'),).filter(search='ПоЧеМу')

...

<QuerySet [<Post: Почему неравномерен секстант?>, <Post: Почему наблюдаема комета Хейла-Боппа>]>
Изменен ilya kutaev

@ilya_kutaev, я только подтвердил, что регистр не учитывается.

@ilya_kutaev, я так понял вопрос:

я так понимаю этот способ поиска не учитывает регистр, правильно?

@Дмитрий_Селезнев, Да, как-то ожидаешь использование знаков пунктуации в вопросах, спасибо!

@ilya_kutaev, прошу у всех прощения, если внес недопонимания своей глупостью.))

@Ilia_Boiarintsev, Все вопросы сняты!

По прочтении этого блока информации накопилось много вопросов, не понимаю логику работы кода.
Допустим, мы переходим по адресу .../blog/search/, там прорисовывается наш шаблон, а потом что происходит? Что происходит когда мы нажимаем кнопку SEARCH, где это прописано?
Что значит "if 'query' in request.GET"? Откуда вообще код понимает, что то, что я написал в форме это query, если изначально оно принимает значение None, а потом мы сразу же проверяем это  "в словаре request.GET"?
И про request.GET тоже вопрос. Что это значит? Что за словарь? Как это интерпретировать? По отдельности что значит request, что значит GET?
Как в URL передаётся информация "query=..."?

Изменен Александр Ёлшин

@Александр_Ёлшин, когда мы нажимаем кнопку SEARCH, то браузер отправляет значение поля(ей) формы выбранным методом по указанному адресу, у нас:

    <form method="get">
        {{ form.as_p }}
        <input type="submit" value="Search">
    </form>

Так-как адрес не указан(не задано значение атрибута формы action), то данные будут отправлены по текущему адресу страницы, с которой произошла отправка - blog/search/. Метод отправки выбран GET. Сервер принимает этот запрос и передаёт в представление объект HttpRequest - request, который содержит все данные запроса. request.GET это объект QueryDict, можно считать словарём, который содержит все данные полученные методом GET, в данном случае он должен содержать ключ query.

if 'query' in request.GET так мы проверяем наличие ключа query в словаре запроса request.GET

form = SearchForm(request.GET), так мы передаём словарь запроса в форму поиска, это нужно что-бы выполнить валидацию формы и получить очищенное значение параметра query, так-как валидация запускает процесс очистки и заполняет словарь формы  form.cleaned_data очищенными данными.

query = form.cleaned_data['query'] получаем непосредственно значение параметра query из словаря очищенных данных формы.

@Дмитрий_Селезнев, спасибо, теперь стало понятнее
А чем отличается метод GET от метода POST и как после запроса в адресной строке появляется "blog/search/?query=Django"?

@Александр_Ёлшин, метод GET передаёт значения в URL запроса, всё что после символа ?, это и есть параметры запроса со значениями.

как после запроса в адресной строке появляется "blog/search/?query=Django"?

Это так браузер отправляет данные формы, методом GET, после нажатия на кнопку отправки.

Изменен Дмитрий Селезнев

@Александр_Ёлшин, Метод GET, не имеет тела запроса, а имеет только строку запроса, имеет много ограничений(длина, данные параметров видны в адресной строке браузера, не возможность отправить файлы и тд).

А метод POST отправляется внутри тела запроса, и не имеет ограничений.

не находит