Извлечение постов по сходству

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

Идея

Под постом полезно показать «Похожие посты» — так читатель задержится на сайте. Простой и хороший критерий сходства: чем больше общих тегов, тем более похожи посты.

Алгоритм

  1. Взять теги текущего поста.
  2. Найти все опубликованные посты с любым из этих тегов.
  3. Исключить сам текущий пост.
  4. Посчитать для каждого найденного поста число общих тегов.
  5. Отсортировать по этому числу (при равенстве — по свежести) и взять несколько первых.

Реализация в 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

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

Расставьте шаги алгоритма поиска похожих постов.

  1. Посчитать для каждого поста число общих тегов
  2. Отсортировать по числу общих тегов и дате, взять первые 4
  3. Исключить из результата сам текущий пост
  4. Найти опубликованные посты с любым из этих тегов
  5. Получить id всех тегов текущего поста
Задание 2 Написать код

Получите плоский список id тегов поста post в переменную post_tags_ids.

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

Найдите похожие посты: опубликованные, с тегами из post_tags_ids, кроме текущего (id=post.id), с аннотацией same_tags=Count("tags"), сортировкой по -same_tags, -publish и первыми 4. Результат — в similar_posts.

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

Почему Count("tags") здесь считает именно общие теги, а не все теги поста?

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

Сколько SQL-запросов выполнит этот код для одной страницы поста (без учёта вывода в шаблоне)?

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