Извлечение постов по сходству
Идея
Под постом полезно показать «Похожие посты» — так читатель задержится на сайте. Простой и хороший критерий сходства: чем больше общих тегов, тем более похожи посты.
Алгоритм
- Взять теги текущего поста.
- Найти все опубликованные посты с любым из этих тегов.
- Исключить сам текущий пост.
- Посчитать для каждого найденного поста число общих тегов.
- Отсортировать по этому числу (при равенстве — по свежести) и взять несколько первых.
Реализация в post_detail
from django.db.models import Count
def post_detail(request, year, month, day, post):
post = get_object_or_404(Post, status=Post.Status.PUBLISHED, slug=post,
publish__year=year, publish__month=month, publish__day=day)
comments = post.comments.filter(active=True)
form = CommentForm()
# похожие посты
post_tags_ids = post.tags.values_list("id", flat=True)
similar_posts = (
Post.published.filter(tags__in=post_tags_ids)
.exclude(id=post.id)
.annotate(same_tags=Count("tags"))
.order_by("-same_tags", "-publish")[:4]
)
return render(request, "blog/post/detail.html", {
"post": post,
"comments": comments,
"form": form,
"similar_posts": similar_posts,
})
Разберём:
values_list("id", flat=True)— плоский список id тегов:[1, 3, 7]. Это QuerySet, поэтому ORM вставит его в итоговый запрос как подзапрос.filter(tags__in=...)— посты, у которых есть хотя бы один из этих тегов. Пост с тремя общими тегами появится в JOIN трижды…- …поэтому
annotate(same_tags=Count("tags"))группирует результат и считает, сколько раз пост встретился, — то есть число общих тегов. order_by("-same_tags", "-publish")— сначала самые похожие, при равенстве — самые свежие.[:4]—LIMIT 4.
Вывод в шаблоне
В detail.html перед комментариями:
<h2>Похожие посты</h2>
{% for post in similar_posts %}
<p>
<a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
<small>(общих тегов: {{ post.same_tags }})</small>
</p>
{% empty %}
<p>Похожих постов пока нет.</p>
{% endfor %}
Аннотированное поле same_tags доступно как обычный атрибут.
Как это выглядит в SQL
Упрощённо:
SELECT blog_post.*, COUNT(taggit_taggeditem.tag_id) AS same_tags
FROM blog_post
JOIN taggit_taggeditem ON ...
WHERE blog_post.status = 'PB'
AND taggit_taggeditem.tag_id IN (SELECT tag_id FROM ... WHERE object_id = 5)
AND NOT blog_post.id = 5
GROUP BY blog_post.id
ORDER BY same_tags DESC, publish DESC
LIMIT 4;
Одна строка Python — и сложный запрос с JOIN, подзапросом, группировкой и сортировкой.
Улучшения
- django-taggit умеет искать похожие объекты сам:
post.tags.similar_objects()(но без фильтра по статусу и менее гибко). - Сходство по тексту — через полнотекстовый поиск PostgreSQL (
TrigramSimilarity) — в уроке про поиск. - Кэширование блока похожих постов — в уроке про кеширование.
Итог
- Похожесть = количество общих тегов.
values_list(flat=True)+filter(tags__in=...)+exclude+annotate(Count)+order_by+ срез.- Аннотированные поля доступны в шаблоне как атрибуты.
Практика решено 0 из 5
Расставьте шаги алгоритма поиска похожих постов.
- Посчитать для каждого поста число общих тегов
- Отсортировать по числу общих тегов и дате, взять первые 4
- Исключить из результата сам текущий пост
- Найти опубликованные посты с любым из этих тегов
- Получить id всех тегов текущего поста
Получите плоский список id тегов поста post в переменную post_tags_ids.
post_tags_ids = post.tags.values_list("id", flat=True)
Найдите похожие посты: опубликованные, с тегами из post_tags_ids, кроме текущего (id=post.id),
с аннотацией same_tags=Count("tags"), сортировкой по -same_tags, -publish и первыми 4.
Результат — в similar_posts.
from django.db.models import Count
similar_posts = (
Post.published.filter(tags__in=post_tags_ids)
.exclude(id=post.id)
.annotate(same_tags=Count("tags"))
.order_by("-same_tags", "-publish")[:4]
)
Почему Count("tags") здесь считает именно общие теги, а не все теги поста?
Сколько SQL-запросов выполнит этот код для одной страницы поста (без учёта вывода в шаблоне)?
Войдите, чтобы отмечать пройденные уроки.