Блог на Django #34: Создание представления поиска
Теперь нужно создать представление для того, чтобы пользователи могли осуществлять поиск по постам. В первую очередь нужна форма поиска. Отредактируйте файл forms.py приложения blog и добавьте следующую форму:
class SearchForm(forms.Form): query = forms.CharField()
Будем использовать поле query , чтобы пользователи могли ввести поисковые запросы. Отредактируйте файл views.py и добавьте следующий код:
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.objects.annotate( search=SearchVector('title', 'body'), ).filter(search=query) return render(request, 'blog/post/search.html', 'form': form, 'query': query, 'results': results>)
В этом представлении в первую очередь создается экземпляр формы SearchForm . Она будет приниматься с помощью метода GET , так, чтобы итоговый URL включал параметр query . Чтобы проверить, принята ли форма, проверяем параметр query в словаре request.GET . Когда форма принимается, создаем ее экземпляр с принятыми данными GET и верифицируем. Если она проходит проверку, проводим поиск с помощью SearchVector по полям title и body .
Представление поиска готово. Нужно создать шаблон для отображения формы и результатов поиска. Создайте файл в папке с шаблонами /blog/post/ , назовите его search.html и добавьте следующий код:
extends "blog/base.html" %> block title %>Searchendblock %> block content %> if query %> h1>Posts containing "query >>"h1> h3> with results.count as total_results %> Found total_results >> resulttotal_results|pluralize >> endwith %> h3> for post in results %> h4>a href token property">post.get_absolute_url >>">post.title >>a>h4> post.body|truncatewords:5 >> empty %> p>There are no results for your query.p> endfor %> p>a href token property">url "blog:post_search" %>">Search againa>p> else %> h1>Search for postsh1> form action="." method="get"> form.as_p >> input type="submit" value="Search"> form> endif %> endblock %>
Как и в представлении поиска, узнать, была ли форма проверена, можно по присутствию параметра query . Перед проверкой отображаем форму и кнопку подтверждения. После проверки, отображаем проведенный поиск и общее количество результатов, а также список постов.
Наконец, отредактируйте файл urls.py приложения blog и добавьте следующий шаблон URL:
path('search/', views.post_search, name='post_search'),
Теперь откройте https://127.0.0.1:8000/blog/search/ в браузере. Вы увидите форму. Введите запрос и нажмите на кнопку «Search».
Теперь у блога есть базовая поисковая система.
Создание представления поиска
Теперь мы создадим пользовательское представление, чтобы пользователи могли выполнять поиск по записям. Во-первых, нам нужна форма поиска. Измените файл forms.py приложения блога и добавьте следующую форму:
class SearchForm(forms.Form): query = forms.CharField()
Мы будем использовать поле query, чтобы позволить пользователям вводить условия поиска. Измените файл views.py приложения блога и добавьте в него следующий код:
from .forms import EmailPostForm, CommentForm, SearchForm from haystack.query import SearchQuerySet def post_search(request): form = SearchForm() if 'query' in request.GET: form = SearchForm(request.GET) if form.is_valid(): cd = form.cleaned_data results = SearchQuerySet().models(Post).filter(content=cd['query']).load_all() # count total results total_results = results.count() return render(request, 'blog/post/search.html', 'form': form, 'cd': cd, 'results': results, 'total_results': total_results>)
В этом представлении сначала мы создаем экземпляр SearchForm, который мы создали ранее. Мы собираемся отправить форму с помощью метода GET, чтобы результирующий URL-адрес включал параметр запроса. Чтобы узнать, была ли отправлена форма, мы рассмотрим query параметр в request.GET dictionary. Когда форма отправляется, мы создаем ее экземпляр с отправленными данными GET и проверяем допустимые данные. Если форма валидна, мы используем SearchQuerySet для выполнения поиска индексированных объектов Post, основное содержимое которых содержит заданный запрос. Метод load_all загружает все связанные объекты Post из базы данных одновременно. Этот метод позволяет заполнить результаты поиска объектами базы данных, чтобы избежать доступа к базе данных для каждого объекта при итерации результатов для доступа к данным объекта. Наконец, мы храним общее число результатов в переменной total_results и передаем локальные переменные в качестве контекста для визуализации шаблона.
Представление поиска готово. Необходимо создать шаблон для отображения формы и результатов поиска, когда пользователь выполняет поиск. Создайте новый файл в каталоге templates/blog/post/, назовите его search.html и добавьте в него следующий код:
Search
Posts containing «>»
Found > result>
>»>>
>
There are no results for your query.
«>Search again
Search for posts
>
Как и в представлении поиска, мы узнаем, если форма была отправлена на основании наличия параметра query. Перед отправкой post мы выводим форму и кнопку отправки. После отправки post мы выводите выполненный query, общее число результатов и список результатов. Каждый result — это документ, возвращенный Solr и инкапсулированный в Haystack. Нам нужно использовать result.object для доступа к фактическому объекту Post, связанному с этим результатом.
Наконец, измените файл urls.py приложения блога и добавьте следующий шаблон URL-адреса:
url(r'^search/$', views.post_search, name='post_search'),
Откройте в браузере http://127.0.0.1:8000/blog/search/ . Вы должны видеть такую форму поиска:

Теперь введите запрос и нажмите кнопку Search. Результаты поиска будут выглядеть следующим образом:
Django как сделать поиск по сайту
![]()
Django База [2023]: Реализация поиска с помощью PostgreSQL в Django #34
21 февраля 2023
Оценки статьи
Еще никто не оценил статью
Django ORM уже имеются некоторые фильтры для поиска, но они не всегда соответствуют нашим требованиям. Вместо этого мы будем использовать поисковый движок полнотекстового поиска из модуля django.contrib.postgres в PostgreSQL, который обеспечивает широкие возможности для поиска на нашем сайте.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Создание представления
Первое, что нам необходимо сделать, это создать нужное предсталвение для реализации поиска. В нашем файле views.py приложения blog добавим следующий код представления:
blog/views.py
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank from django.views.generic import ListView class ArticleSearchResultView(ListView): """ Реализация поиска статей на сайте """ model = Article context_object_name = 'articles' paginate_by = 10 allow_empty = True template_name = 'blog/articles_list.html' def get_queryset(self): query = self.request.GET.get('do') search_vector = SearchVector('full_description', weight='B') + SearchVector('title', weight='A') search_query = SearchQuery(query) return (self.model.objects.annotate(rank=SearchRank(search_vector, search_query)).filter(rank__gte=0.3).order_by('-rank')) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['title'] = f'Результаты поиска: self.request.GET.get("do")>' return context
Данный код представляет собой реализацию поиска статей на сайте. Он использует класс ListView из Django для отображения списка статей, соответствующих запросу пользователя.
В методе get_queryset() происходит поиск статей с помощью полнотекстового поискового движка из модуля django.contrib.postgres.search в PostgreSQL. Для этого формируется поисковый вектор (search_vector), содержащий поля, по которым будет производиться поиск, и задаются веса (weight) каждого поля. Затем формируется поисковый запрос (search_query) на основе запроса пользователя (query), который был передан через GET-параметр do. Поиск выполняется с помощью методов SearchRank и filter модели Article. Результаты сортируются по релевантности в обратном порядке.
Метод get_context_data() формирует контекст для отображения найденных статей в шаблоне.
Обработка представления в urls.py
Следующим действием нам нужно обработать созданное представление в urls.py файле нашего приложения blog:
blog/urls.py
from django.urls import path from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView, \ ArticleUpdateView, ArticleDeleteView, CommentCreateView, ArticleByTagListView, ArticleSearchResultView urlpatterns = [ path('', ArticleListView.as_view(), name='home'), path('articles/', articles_list, name='articles_by_page'), path('articles/create/', ArticleCreateView.as_view(), name='articles_create'), path('articles//update/', ArticleUpdateView.as_view(), name='articles_update'), path('articles//delete/', ArticleDeleteView.as_view(), name='articles_delete'), path('articles//', ArticleDetailView.as_view(), name='articles_detail'), path('articles//comments/create/', CommentCreateView.as_view(), name='comment_create_view'), path('articles/tags//', ArticleByTagListView.as_view(), name='articles_by_tags'), path('category//', ArticleByCategoryListView.as_view(), name="articles_by_category"), path('search/', ArticleSearchResultView.as_view(), name='search'), ]
Обязательно добавляем юрл в самый конец, для правильной обработки.
Изменение шаблона формы поиска
Переходим в header.html, где наша поисковая форма, и меняем разметку на следующую:
templates/header.html
form class="col-12 col-lg-auto mb-2 mb-lg-0 me-lg-auto" role="search" method="get" action=""> input type="search" class="form-control" placeholder="Search. " aria-label="Search" name='do' autocomplete="off" id="search"> form>
Проверка работы поисковой системы

Отлично. Наш поиск работает так, как нам необходимо.
Меню категорий
-
Загрузка категорий.
Добро пожаловать в Блог Разработчика Владислава Александровича.
Ведется медленная, но уверенная разработка функционала сайта.
Django Core: 0.3.4 / Next.js 1.0 / UPD: 05.06.2023
Django — Урок 063. Полнотекстовый поиск на сайте для нескольких моделей с поддержкой мультиязычности

Наконец-то мне удалось сделать достаточно быстрый полнотекстовый поиск по нескольким моделям с учётом оптимизации запросов на сайте, который бы меня устраивал и отвечал моим требованиям к качеству проекта. Если вы сейчас воспользуетесь поиском по сайту, то обнаружите, что поиск сработает достаточно быстро, а также выдаст несколько групп поиска: Статьи, Комментари, Темы форума, Ответы на форуме, Тесты.
Из всех групп поиска будут выбраны по три результата, а также будет у каждой группы есть счётчик общего количества найденных резульлатов и предложено посмотреть остальные результаты в отдельных вкладках. В итоге я сделал такое разделение по той причине, что создавать единый список со всеми результатми довольно накладно по ресурсам и недостаточо эффективно. Поэтому итоговый результат выгляди следующим образом: Впоследствии подобный подход позволит мне более гибко и легко модифицировать систему поиска, что даст возможность добавлять в поиск любой контент независимо от других частей сайта. А теперь я расскажу, как именно я это сделал.
Полнотекстовый поиск в PostgreSQL
База данных PostgreSQL поддерживает полнотекстовый поиск, а Django позволяет средствами ORM его реализовать. Самый простой способ, как запустить полнотекстовый поиск, это воспользоваться методом search по полю модели, что и описано в документации к Django. Например так
Entry.objects.filter(text__search='Cheese')В данном случае модель Entry имеет поле text, по которому вызывается полнотекстовый поиск с помощью встроенного функционала search. Более развитым способом выполнения полнотекстового поиска является использовния поисковых векторов SearchVector по нескольким полям. То есть так
from django.contrib.postgres.search import SearchVector Entry.objects.annotate(search=SearchVector('text', 'tagline')).filter(search='Cheese')К сожалению, использования метода annotate является малоэффективным, поскольку этот метод порой требует достаточно много времени.
Для улучшения производительности средства Django предлагают использовать специальное поле SearchVectorField , которое индексируется базой данных PostgreSQL. Это позволяет значительно ускорить поиск на сайте.Добавление SearchVectorField и индекса в модель
Я покажу добавление SearchVectorField и индексирования на примере модели Article.
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField class Article(models.Model): title = models.CharField('Title', max_length=200) content = models.TextField(verbose_name='Content', blank=True) # SearchVectorField for full-text search search_vector = SearchVectorField(null=True) class Meta: indexes = [GinIndex(fields=["search_vector",]),]Как видите, в представленном коде присутствует поле search_vector, которое индексируется с помощью указания GinIndex в Meta классе модели Article. После того, как вы добавили SearchVectorField и индексирование данного поля, создайте новые миграции
python manage.py makemigrationsВ общем виде новая миграция будет похожу на эту
# Generated by Django 3.2 on 2023-03-27 21:03 import django.contrib.postgres.indexes import django.contrib.postgres.search from django.db import migrations class Migration(migrations.Migration): dependencies = [ # depends on ] operations = [ migrations.AddField( model_name='article', name='search_vector', field=django.contrib.postgres.search.SearchVectorField(null=True), ), migrations.AddIndex( model_name='article', index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='knowledge_a_search__682520_gin'), ), ]Но этого будет недостаточно, поскольку нужно ещё заполнить поле SearchVectorField, что также можно сделать во время миграции. Поэтому модифичируем миграцию следующим образом
# Generated by Django 3.2 on 2023-03-27 21:03 import django.contrib.postgres.indexes import django.contrib.postgres.search from django.db import migrations def compute_search_vector(apps, schema_editor): Article = apps.get_model("knowledge", "Article") Article.objects.update(search_vector=django.contrib.postgres.search.SearchVector("title", "content")) class Migration(migrations.Migration): dependencies = [ # depends on ] operations = [ migrations.AddField( model_name='article', name='search_vector', field=django.contrib.postgres.search.SearchVectorField(null=True), ), migrations.AddIndex( model_name='article', index=django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='knowledge_a_search__682520_gin'), ), migrations.RunPython( compute_search_vector, reverse_code=migrations.RunPython.noop ), ]В данном коде добавлен дополнительный шаг, который добавляет выполнение python кода, чтобы сразу заполнить SearchVectorField , а именно последний шаг migrations.RunPython , который запускает функцию compute_search_vector .
SearchVectorField имеет важную особенность, в него нельзя напрямую добавить SearchVector , но его можно заполнить используя метод update у менеджера модели. Поэтому-то данный код и выглядит таким образомArticle.objects.update(search_vector=django.contrib.postgres.search.SearchVector("title", "content"))После чего выполните миграцию
python manage.py migrateТеперь все статьи имеют проиндексированное поисковое поле. Но на самом деле этого недостаточно для полноценной работы поисковой системы, поскольку поисковое поле нужно заполнять как в случае создания нового объекта, так и в случае редактирования старого. Для этого документация Django предлагает обратиться в документации PostgreSQL и создать триггеры. Это хорошо и правильно, но что если вы не хотите по какой-то причине лишний раз лезать в PostgreSQL и вручную создавать все необходимые триггеры. Как тогда быть? В этом случае нас спасёт сигнал/слотовая система Django. Для этого добавим в модель Article следующий код
def update_search_vector(self): qs = Article.objects.filter(pk=self.pk) qs.update(search_vector=SearchVector("title", "content"))А потом подключимся к сигналу post_save
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=Article) def post_save_artcile(sender, instance, created, update_fields, **kwargs): instance.update_search_vector()Таким образом при каждом сохранении объекта статьи будет происходить обновление поискового поля.
Класс SearchView для поиска по всем видам контента
А теперь напишем SearchView , который бы позволил выполнить поиск по нескольким видам контента и выдал бы необходимый результат. В нашем случае скажем, что у нас на сайте есть статьи ("Article") и комментарии ("Comment")
class SearchView(View): template_name = 'search/index.html' def get(self, request, *args, **kwargs): query = request.GET.get('search', None) article_results = Article.objects.filter(search_vector=query) comment_results = Comment.objects.filter(search_vector=query) return render( request=request, template_name=self.template_name, context= < 'search': query or '', 'article_results': article_results[:3], 'article_results_count': article_results.count(), 'comment_results': comment_results[:3], 'comment_results_count': comment_results.count(), >)Обратите внимание, что в строке сайта передаётся get параметер search , который отвечает за поисковую фразу. Также имеет пара ухищрений для оптимизации запросов к базе данных. Поскольку Django использует ленивые запросы к базе данных, то и реквесты выполняются ровно тогда, когда это требуется. Таким образом строка
article_results = Article.objects.filter(search_vector=query)лишь задаёт общий запрос к базе данных, но не выполняет его, поскольку для первоначальной страницы поиска нам нужно общее количество найденных материалов и только три первых объекта из всех найденных материалов.
Поэтому в качестве контекста для рендеринга передаются только два QuerySet.'article_results': article_results[:3], 'article_results_count': article_results.count(),article_results[:3] выполняет операцию limit к найденным записям и возвращает только три объекта
article_results.count() выполняет подсчёт всех найденных записей. На самом деле подобный дополнительный код позволяет значительно сократить время выполнения запросов, что значительно ускоряет поиск на сайте.Рендеринг страницы
- append_query_to_url - тег для добавления query параметра в url страницы поиска по определённому контенту.
- search_field - тег для рендеринга поля поиска
- found_objects - тег для рендеринга вывода результатов по контенту
Каталог "templatetags"
В некоторых статьях, например в Как написать блочный шаблонный тег tabbar наподобие тега blocktranslate , я уже описывал структуру этого каталога и что там должно содержаться для регистрации шаблонных тегов, поэтому не буду останавливаться на этом лишний раз.
Итак, посмотрим на содержимое файла templatetags/search.py
# -*- coding: utf-8 -*- from django import template from django.template.defaultfilters import urlencode register = template.Library() @register.inclusion_tag('search/search_field.html', takes_context=True) def search_field(context, value, **kwargs): context.update() return context @register.simple_tag(takes_context=True) def append_query_to_url(context, url): return '<>?search=<>'.format(url, urlencode(context.get('search', ''))) @register.inclusion_tag('search/found_objects.html', takes_context=True) def found_objects(context, results, results_count, objects_title, all_search_message, search_url): context.update(< 'results': results, 'results_count': results_count, 'objects_title': objects_title, 'all_search_message': all_search_message, 'search_url': search_url >) return contextКаталог "шаблоны/поиск"
Далее рассмотрим Что из себя представляют шаблоны inclusion тегов
Файл "search/found_objects.html"
В общих чертах рендеринг шаблона для нескольких ваших объектов из поиска будет выглядеть так, как показано ниже.
Как видите, здесь используется разметка из bootstrap, а также нет примера рендеринга самого объекта. Я уверен, что если вы читаете эту статью, то вполне можете самостоятельно написать то, что требуется вам, поскольку эта статья является описание моего опыта, а не прямым руководством к бездумному копированию.Файл "search/search_field.html"
Файл "urls.py"
Далее добавим SearchView в маршрутизатор Django .
# -*- coding: utf-8 -*- from django.urls import path from search import views app_name = 'search' urlpatterns = [ path('', views.SearchView.as_view(), name='index'), ]Вот так и будет выглядеть настройка для страницы полнотекстового поиска на Django, во всяком случае так будет выглядеть главная страница поиска. Но я также добавил страницы поиска по отдельным частям контента, что также важно. На главной странице поиска пользователь может посмотреть, можно ли найти хоть что-то на сайте по его запросу, а на детальных страницах пользователь сможет посмореть все найденные на сайте записи.
Класс "SearchViewByContent"
А теперь напишем более обощённый класс для поиска по разным видам контента. Для этого можно воспользоваться Generic классом ListView . Поиск будет осуществляться по полю поискового вектора. Также для правильной пагинации нам понадобится pagination url. Это всё присутствует в данном коде.
class SearchViewByContent(ListView): template_name = 'search/search_objects.html' paginate_by = 10 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(< 'search': self.request.GET.get('search', None) or '', 'last_question': self.get_pagination_url() >) return context def get_pagination_url(self): return self.request.get_full_path().replace(self.request.path, '') def get_queryset(self): qs = super().get_queryset() query = self.request.GET.get('search', None) return qs.filter(search_vector=query)Рендеринг страницы
Воспользуемся кодом уже существующего шаблона и расширим его поведение. Как видите, здесь используется шаблонный тег из батарейки django bootstrap 4 . В одной из предыдущих статей я уже описывал, как подключить эту батарейку более старой версии. С тех времён ничего не изменилось и статья также актульна и для последней версии django bootstrap 5.
Добавим маршруты в файл "urls.py"
# -*- coding: utf-8 -*- from django.urls import path from search import views from articles.models import Article, Comment app_name = 'earch' urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('articles/', views.SearchView.as_view(queryset=Article.objects.all()), name='articles'), path('comments/', views.SearchView.as_view(queryset=Comment.objects.all()), name='comments'), ]Таким образом поиск будет реализован уже по отдельным видам контента на сайте.
Поддержка мельтиязычности с использованием батарейки modeltranslation
Django modeltranslation является замечательным пакетом, который добавляет мультиязычность в ваши модели, но к сожалению он не совместим с полем SearchVectorField . Для этого нужно вручную добавлять новые поля SearchVectorField для каждого языка, который поддерживается на сайте. Но на самом деле это не так много работы. А выглядеть это может так
Модель
class Article(models.Model): # Another code # Search vectors search_vector = SearchVectorField(null=True) search_vector_ru = SearchVectorField(null=True) search_vector_en = SearchVectorField(null=True) objects = ArticleManager() class Meta: ordering = ['-pub_date'] verbose_name = _('Article') verbose_name_plural = _('Articles') indexes = [ GinIndex(fields=[ "search_vector", "search_vector_ru", "search_vector_en", ]), ]Соответственно новую миграцию нужно будет исправлять для каждого нового поля SearchVectorField
Просмотр поиска
А поисковый SearchView можно исправить следующим образом
class IndexView(View): template_name = 'evileg_search/index.html' def get(self, request, *args, **kwargs): query = request.GET.get('search', None) current_language = get_language() article_results = Article.objects.filter( Q(**'.format(current_language): query>) | Q(search_vector=query) ) comment_results = Comment.objects.filter(search_vector=query) return render( request=request, template_name=self.template_name, context= < 'search': query or '', 'article_results': article_results[:3], 'article_results_count': article_results.count(), 'comment_results': comment_results[:3], 'comment_results_count': comment_results.count(), >)Тоже самое касается и отдельного "View" для статей. Попробуйте сами написать его отдельно и модифицировать запрос подобным образом, как и в этом View.
Также здесь есть важный момент. Я делаю поиск как по вектору с указанием языка, так и без указания языка. Это связано с тем, что я могу не иметь всех переводов для всех языков у какой-то конкретной статьи, а выдача результатов поиска, по моему мнению, всегда должна производиться.ю даже если перевод отсутствует.
Заключение
Таким образом можно сделать довольно простой поиск по разным видам контента, который ко всему будет достаточно модифицируемым и поддерживаемым для будущего расширения поиска другими видами контента на сайте.
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.
Рекомендуемые статьи по этой тематике
- Django - Урок 062. Как написать блочный шаблонный тег tabbar наподобие тега blocktranslate
- Анализ ошибок при разработке и продвижении сайта на Django - Мультиязычные формы
- Django - Урок 057. Поддержка мультиязычности в MarkdownField с функционалом auto populate
- Django - Урок 049. Оптимизация производительности Django на примере боевого проекта
- Django - Урок 034. Как сделать поиск по нескольким моделям данных
По статье задано0 вопрос(ов)
Подписка на обсуждение 4
Подписка на раздел 176
Вам это нравится? Поделитесь в социальных сетях!
