Добавление полнотекстового поиска в блог
Поиск: от простого к мощному
Самый простой поиск:
Post.published.filter(title__icontains=query)
Минусы: ищет только точную подстроку («книги» не найдёт «книга»), не ранжирует результаты, медленный на больших таблицах. В PostgreSQL есть полнотекстовый поиск, и Django даёт к нему удобный доступ через django.contrib.postgres.
Подключите приложение:
INSTALLED_APPS = [
...
"django.contrib.postgres",
]
Lookup search
Самый короткий вариант — поиск по одному полю:
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
Почему title__icontains="книги" не найдёт пост «Лучшая книга года», а полнотекстовый поиск найдёт?
Найдите опубликованные посты, в которых запрос query встречается в заголовке или тексте,
с помощью SearchVector("title", "body") и аннотации search. Результат — в results.
from django.contrib.postgres.search import SearchVector
results = Post.published.annotate(search=SearchVector("title", "body")).filter(search=query)
Добавьте ранжирование: создайте search_vector = SearchVector("title", "body", config="russian"),
search_query = SearchQuery(query, config="russian"), аннотируйте search и rank=SearchRank(...),
отфильтруйте и отсортируйте по -rank. Результат — в results.
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
search_vector = SearchVector("title", "body", config="russian")
search_query = SearchQuery(query, config="russian")
results = (
Post.published.annotate(search=search_vector, rank=SearchRank(search_vector, search_query))
.filter(search=search_query)
.order_by("-rank")
)
Что даст SearchVector("title", weight="A") + SearchVector("body", weight="B")?
Что нужно для поиска по триграммам (TrigramSimilarity)?
Войдите, чтобы отмечать пройденные уроки.