Добавление полнотекстового поиска в блог

⏱ 30 мин · 🧩 заданий: 5

Поиск: от простого к мощному

Самый простой поиск:

Post.published.filter(title__icontains=query)

Минусы: ищет только точную подстроку («книги» не найдёт «книга»), не ранжирует результаты, медленный на больших таблицах. В PostgreSQL есть полнотекстовый поиск, и Django даёт к нему удобный доступ через django.contrib.postgres.

Подключите приложение:

INSTALLED_APPS = [
    ...
    "django.contrib.postgres",
]

Самый короткий вариант — поиск по одному полю:

Post.published.filter(title__search="django")

Поиск по нескольким полям: SearchVector

from django.contrib.postgres.search import SearchVector

Post.published.annotate(search=SearchVector("title", "body")).filter(search="django")

SearchVector превращает текст в набор нормализованных лексем, по которым и идёт поиск.

Форма и view поиска

blog/forms.py:

class SearchForm(forms.Form):
    query = forms.CharField(label="Поиск")

blog/views.py:

from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector

from .forms import 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"]
            search_vector = SearchVector("title", weight="A", config="russian") + \
                            SearchVector("body", weight="B", config="russian")
            search_query = SearchQuery(query, config="russian")
            results = (
                Post.published.annotate(rank=SearchRank(search_vector, search_query))
                .filter(rank__gte=0.3)
                .order_by("-rank")
            )

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

Маршрут: path("search/", views.post_search, name="post_search").

Обратите внимание: форма поиска отправляется методом GET — так результат поиска можно добавить в закладки и им можно поделиться.

Стемминг и русский язык

config="russian" включает словарь русского языка: слова приводятся к основе, стоп-слова («и», «в», «на») отбрасываются. Без него PostgreSQL использует английскую конфигурацию, и для русских текстов стемминг не работает.

Запрос Найдёт
книги книга, книгу, книгами
программирование программирования, программированию

Ранжирование и веса

SearchRank оценивает, насколько документ соответствует запросу (частота и близость слов). Веса A, B, C, D (по умолчанию 1.0, 0.4, 0.2, 0.1) позволяют считать совпадение в заголовке важнее, чем в тексте. Фильтр rank__gte=0.3 отсекает слабо релевантные результаты.

Шаблон

blog/post/search.html:

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

{% block title %}Поиск{% endblock %}

{% block content %}
  {% if query %}
    <h1>Посты по запросу «{{ query }}»</h1>
    <h3>Найдено: {{ results|length }}</h3>
    {% for post in results %}
      <h4><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h4>
      {{ post.body|markdown|truncatewords_html:12 }}
    {% empty %}
      <p>По вашему запросу ничего не найдено.</p>
    {% endfor %}
    <p><a href="{% url 'blog:post_search' %}">Искать снова</a></p>
  {% else %}
    <h1>Поиск по блогу</h1>
    <form method="get">
      {{ form.as_p }}
      <input type="submit" value="Найти">
    </form>
  {% endif %}
{% endblock %}

Поиск по триграммам

Триграмма — три подряд идущих символа. Сравнение наборов триграмм находит похожие слова даже с опечатками.

Включим расширение PostgreSQL пустой миграцией:

python manage.py makemigrations --name trigram_ext --empty blog
from django.contrib.postgres.operations import TrigramExtension
from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [("blog", "0005_...")]
    operations = [TrigramExtension()]
python manage.py migrate blog

Поиск:

from django.contrib.postgres.search import TrigramSimilarity

results = (
    Post.published.annotate(similarity=TrigramSimilarity("title", query))
    .filter(similarity__gt=0.1)
    .order_by("-similarity")
)

Производительность

На больших таблицах вычислять SearchVector на каждый запрос дорого. Решения:

  • SearchVectorField — хранить вектор в отдельном поле и обновлять его триггером или сигналом;
  • индекс GinIndex на этом поле;
  • для очень больших объёмов — отдельный поисковый движок (Elasticsearch, Meilisearch).

Итог

  • django.contrib.postgres даёт полнотекстовый поиск PostgreSQL.
  • SearchVector + SearchQuery + SearchRank — поиск с ранжированием.
  • config="russian" включает стемминг для русского языка, веса поднимают совпадения в заголовке.
  • TrigramSimilarity находит результаты с опечатками.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Почему title__icontains="книги" не найдёт пост «Лучшая книга года», а полнотекстовый поиск найдёт?

Задание 2 Написать код

Найдите опубликованные посты, в которых запрос query встречается в заголовке или тексте, с помощью SearchVector("title", "body") и аннотации search. Результат — в results.

Задание 3 Написать код

Добавьте ранжирование: создайте search_vector = SearchVector("title", "body", config="russian"), search_query = SearchQuery(query, config="russian"), аннотируйте search и rank=SearchRank(...), отфильтруйте и отсортируйте по -rank. Результат — в results.

Задание 4 Один вариант ответа

Что даст SearchVector("title", weight="A") + SearchVector("body", weight="B")?

Задание 5 Несколько вариантов

Что нужно для поиска по триграммам (TrigramSimilarity)?

Войдите, чтобы отмечать пройденные уроки.