Реалізація автоматичної генерації пов'язаних статей для блогу

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація автоматичної генерації пов'язаних статей для блогу
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Автоматична генерація блоку пов'язаних статей

Блок "Пов'язані статті" утримує користувачів на сайті та зменшує показник відмови. Ручний добір не масштабується — зі сотнями публікацій вам потрібна автоматична система на основі тегів, категорій або семантичної схожості.

Стратегії добору пов'язаного вмісту

За тегами та категоріями — швидко, не потребує ML, але поверхневе.

За TF-IDF — статистична схожість на основі частоти термінів.

За векторними вкладеннями — семантична схожість, найкраща якість, потребує pgvector.

Підхід на основі тегів

// RelatedArticleService
class RelatedArticleService
{
    public function getRelated(Article $article, int $limit = 4): Collection
    {
        if ($article->tags->isEmpty()) {
            // Fallback: статті з тієї ж категорії
            return Article::published()
                ->where('category_id', $article->category_id)
                ->where('id', '!=', $article->id)
                ->latest()
                ->limit($limit)
                ->get();
        }

        $tagIds = $article->tags->pluck('id');

        // Підраховуємо спільні теги
        return Article::published()
            ->where('id', '!=', $article->id)
            ->withCount(['tags as common_tags_count' => function ($q) use ($tagIds) {
                $q->whereIn('tags.id', $tagIds);
            }])
            ->having('common_tags_count', '>', 0)
            ->orderByDesc('common_tags_count')
            ->orderByDesc('published_at')
            ->limit($limit)
            ->get();
    }
}

Підхід на основі вкладень з pgvector

// Під час створення/оновлення статті
class ArticleObserver
{
    public function saved(Article $article): void
    {
        GenerateArticleEmbedding::dispatch($article)->onQueue('low');
    }
}

class GenerateArticleEmbedding implements ShouldQueue
{
    public function handle(): void
    {
        $text = implode("\n", [
            $this->article->title,
            $this->article->excerpt,
            strip_tags(substr($this->article->content, 0, 2000)),
        ]);

        $embedding = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $text,
        ])->embeddings[0]->embedding;

        $this->article->update(['embedding' => '[' . implode(',', $embedding) . ']']);

        // Перерахуємо кэш пов'язаних для цієї статті
        Cache::forget("related_articles_{$this->article->id}");
    }
}

// Запит пов'язаних через pgvector
public function getSemanticallyRelated(Article $article, int $limit = 4): Collection
{
    $embedding = $article->embedding;
    if (!$embedding) return collect();

    return Cache::remember("related_articles_{$article->id}", 86400, function () use ($article, $embedding, $limit) {
        return Article::published()
            ->where('id', '!=', $article->id)
            ->selectRaw('*, (embedding <=> ?) AS distance', [$embedding])
            ->whereNotNull('embedding')
            ->orderBy('distance')
            ->limit($limit)
            ->get();
    });
}

React-компонент з ледачим завантаженням

// RelatedArticles.tsx
export function RelatedArticles({ articleId }: { articleId: number }) {
  const ref = useRef<HTMLDivElement>(null);
  const [inView, setInView] = useState(false);

  // Завантажуємо тільки коли блок потрапляє у viewport
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) setInView(true); },
      { rootMargin: '200px' }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  const { data, isLoading } = useQuery({
    queryKey:  ['related', articleId],
    queryFn:   () => fetch(`/api/articles/${articleId}/related`).then(r => r.json()),
    enabled:   inView,
    staleTime: 10 * 60 * 1000,
  });

  return (
    <div ref={ref} className="mt-10">
      <h3 className="text-xl font-bold mb-5">Читайте також</h3>
      {isLoading ? (
        <div className="grid grid-cols-2 gap-4">
          {[...Array(4)].map((_, i) => (
            <div key={i} className="h-32 bg-gray-100 rounded-lg animate-pulse" />
          ))}
        </div>
      ) : (
        <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
          {data?.map((article: any) => (
            <a key={article.id} href={article.url}
              className="group flex gap-4 p-4 border rounded-xl hover:shadow-md transition-shadow">
              {article.image && (
                <img src={article.image} alt="" className="w-20 h-16 object-cover rounded-lg flex-shrink-0" />
              )}
              <div>
                <p className="text-xs text-blue-600 mb-1">{article.category}</p>
                <h4 className="text-sm font-medium group-hover:text-blue-600 transition-colors line-clamp-2">
                  {article.title}
                </h4>
                <p className="text-xs text-gray-400 mt-1">{article.reading_time} хв. читання</p>
              </div>
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

Часовий графік

Система пов'язаних статей з добором на основі тегів та вкладень, компонент ледачого завантаження: 3–4 робочих дні.