Django: делаем поиск по сайту одним запросом за 20 минут
Пошаговая инструкция: добавляем поиск по статьям в проект на Django 5.x без Elasticsearch и внешних сервисов — только модель, форма и один запрос к базе.

К концу этой инструкции у вас будет рабочая строка поиска на сайте: пользователь вводит слово, страница показывает список статей, где это слово встречается в заголовке или тексте. Всё держится на одном запросе к базе через встроенные средства Django — без Elasticsearch, Solr и сторонних библиотек. На это уйдёт минут двадцать, если проект уже поднят.
Инструкция написана для Django 5.x (проверялась на 5.0). Для 4.2 всё работает так же — отличий в описанных методах нет.
Что нужно иметь до начала
Проверьте это до того, как трогать код. Иначе застрянете на середине.
- Установленный Python 3.10+ и Django 5.x (
python -m django --versionпокажет версию); - готовый проект с приложением — дальше в примерах оно называется
blog. Если у вас другое имя, подставляйте своё; - модель с текстовыми полями, по которым будем искать;
- применённые миграции (
python manage.py migrateотработал без ошибок); - любая база — SQLite подойдёт для старта. О её ограничениях скажу в конце.
Если модели ещё нет, вот такая подойдёт для примера. Положите её в blog/models.py:
from django.db import models
class Article(models.Model):
title = models.CharField("Заголовок", max_length=200)
body = models.TextField("Текст")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
После добавления модели выполните python manage.py makemigrations и python manage.py migrate. В консоли должно появиться сообщение о применённой миграции с именем модели.
Шаг 1. Пишем представление с запросом
Вся суть поиска — в одном запросе с Q-объектами. Q позволяет соединить условия через «или»: искать слово и в заголовке, и в тексте одновременно.
Откройте blog/views.py и добавьте:
from django.db.models import Q
from django.shortcuts import render
from .models import Article
def article_search(request):
# Берём поисковый запрос из строки ?q=...
query = request.GET.get("q", "").strip()
results = []
if query:
# icontains — поиск подстроки без учёта регистра
results = Article.objects.filter(
Q(title__icontains=query) | Q(body__icontains=query)
).order_by("-created_at")
return render(request, "blog/search.html", {
"query": query,
"results": results,
})
Здесь title__icontains=query означает «в поле title встречается подстрока query, регистр не важен». Оператор | между двумя Q склеивает их в условие «или».
Проверить, что запрос сам по себе работает, можно в консоли Django ещё до шаблона:
python manage.py shell
from blog.models import Article
from django.db.models import Q
Article.objects.filter(Q(title__icontains="django") | Q(body__icontains="django"))
Если в базе есть подходящая статья — в ответ придёт QuerySet с ней. Пустой список означает, что совпадений нет, а не что код сломан.
Шаг 2. Подключаем URL
В blog/urls.py (создайте файл, если его нет) добавьте маршрут:
from django.urls import path
from . import views
urlpatterns = [
path("search/", views.article_search, name="article_search"),
]
Убедитесь, что этот файл подключён в корневом urls.py проекта через include:
from django.urls import include, path
urlpatterns = [
path("", include("blog.urls")),
]
Проверка: запустите python manage.py runserver и откройте http://127.0.0.1:8000/search/. Пока шаблона нет, вы увидите ошибку TemplateDoesNotExist — это нормально, значит маршрут найден и представление вызвалось.
Шаг 3. Делаем шаблон с формой и результатами
Создайте файл blog/templates/blog/search.html. Путь важен: Django ищет шаблоны внутри папки templates приложения.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Поиск по сайту</title>
</head>
<body>
<form method="get" action="{% url 'article_search' %}">
<input type="text" name="q" value="{{ query }}"
placeholder="Что ищем?">
<button type="submit">Найти</button>
</form>
{% if query %}
<p>Найдено: {{ results|length }}</p>
<ul>
{% for article in results %}
<li>{{ article.title }}</li>
{% empty %}
<li>Ничего не нашлось по запросу «{{ query }}».</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
Форма отправляет данные методом GET — поэтому запрос попадает в адресную строку как ?q=... и ссылку с результатами можно скопировать и переслать. Тег {% empty %} внутри цикла срабатывает, когда список пустой.
Проверка: обновите страницу /search/, введите слово, которое точно есть в одной из статей, нажмите «Найти». Под формой появится счётчик и список заголовков.
Для сайта с сотнями и тысячами страницicontains— рабочее и честное решение. Полнотекстовый поиск и внешние движки нужны, когда счёт идёт на десятки тысяч записей и обычныйLIKEначинает тормозить.
Если не получилось
Пять ошибок, которые встречаются чаще всего на этих трёх шагах.
- TemplateDoesNotExist: blog/search.html — шаблон лежит не там. Он должен быть по пути
blog/templates/blog/search.html, папкаblogвнутриtemplatesобязательна. Также проверьте, что приложение указано вINSTALLED_APPS. - NoReverseMatch: 'article_search' — имя в
{% url %}не совпадает сnameвurls.py. Сверьте буква в букву. - Поиск ничего не находит, хотя слово есть — проверьте, что фильтруете по правильным полям. Опечатка вроде
title__icontansне даст ошибку сразу, а просто вернёт пустой результат или уронит запрос сFieldError. - FieldError: Cannot resolve keyword — имя поля в фильтре не совпадает с полем модели. Django подскажет список доступных полей в тексте ошибки.
- Кириллица ищется криво в SQLite — стандартный
icontainsна SQLite не всегда корректно понижает регистр для кириллицы. На PostgreSQL этой проблемы нет. Если проект на SQLite и поиск по русским словам ведёт себя странно с заглавными буквами — это оно.
Что делать дальше
Базовый поиск работает. Дальше по мере роста проекта смотрите в эту сторону:
- если статей станет много и запросы начнут тормозить — переходите на полнотекстовый поиск PostgreSQL через
SearchVectorиSearchQueryизdjango.contrib.postgres.search. Это отдельная тема, здесь мы её не разворачиваем; - добавьте пагинацию результатов через
Paginator, чтобы не выводить сотни строк сразу; - подсветку найденного слова в тексте делают на уровне шаблона или простым фильтром — тоже отдельная задача.
Мерить стоит время ответа страницы /search/ на реальном объёме данных: пока оно в пределах десятков миллисекунд, icontains вас устраивает.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: Django QuerySet API reference — filter и field lookups, Complex lookups with Q objects — документация Django
Частые вопросы
А если у меня Django 4.2, а не 5.x?
Поиск не находит русские слова с заглавной буквы — что не так?
Чем icontains отличается от contains?
Не будет ли такой поиск тормозить?
Почему форма отправляет GET, а не POST?
Как искать сразу по нескольким моделям?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.