Реалізація AI-рекомендацій контенту на сайті

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація AI-рекомендацій контенту на сайті
Складна
~1-2 тижні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • 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

Реалізація AI-рекомендацій контенту на сайті

Рекомендації контенту утримують користувачів та збільшують глибину перегляду. Підхід залежить від наявності даних про поведінку: для нового сайту без історії — content-based фільтрація на embeddings; для сайту з тисячами користувачів та подіями — колаборативна фільтрація або гібридні системи.

Вибір підходу

Підхід Дані Складність Коли
Content-based (embeddings) Тільки контент Низька Новий сайт, мала аудиторія
Колаборативна фільтрація Історія взаємодій Середня 10K+ користувачів
Гібридна Контент + поведінка Висока Медіа, блоги, новинні сайти
LLM-based Контент + профіль Середня Персоналізовані добірки

Content-Based: Подібний контент через Embeddings

Найшвидший старт — подібні статті на основі векторної відстані:

import OpenAI from 'openai';
import { sql } from '@vercel/postgres';

const openai = new OpenAI();

// Індексація при публікації статті
async function indexArticle(article) {
  const textToEmbed = [
    article.title,
    article.excerpt,
    article.tags.join(', '),
    article.body.slice(0, 2000), // перші 2000 символів
  ].join('\n\n');

  const { data: [{ embedding }] } = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: textToEmbed,
  });

  await sql`
    UPDATE articles
    SET embedding = ${JSON.stringify(embedding)}::vector
    WHERE id = ${article.id}
  `;
}

// Отримання подібних статей
async function getSimilarArticles(articleId, limit = 6) {
  const result = await sql`
    WITH source AS (
      SELECT embedding FROM articles WHERE id = ${articleId}
    )
    SELECT
      a.id, a.title, a.slug, a.excerpt, a.published_at,
      a.category, a.read_time,
      1 - (a.embedding <=> source.embedding) AS similarity
    FROM articles a, source
    WHERE a.id != ${articleId}
      AND a.published = true
      AND a.embedding IS NOT NULL
    ORDER BY a.embedding <=> source.embedding
    LIMIT ${limit}
  `;

  return result.rows;
}

Колаборативна фільтрація: "Користувачі як ти читали"

Матрична факторизація через implicit feedback (перегляди, час на сторінці):

# Python-скрипт для періодичного навчання (cron)
import implicit
import numpy as np
from scipy.sparse import csr_matrix
import pickle

def train_collaborative_model():
    # Завантажуємо події: user_id, article_id, weight
    # weight = 1 (перегляд) + 2 (scroll 50%) + 5 (читав до кінця) + 10 (поділився)
    events = fetch_events_from_db()

    users = {u: i for i, u in enumerate(events['user_id'].unique())}
    items = {a: i for i, a in enumerate(events['article_id'].unique())}

    rows = events['user_id'].map(users)
    cols = events['article_id'].map(items)
    data = events['weight']

    matrix = csr_matrix((data, (rows, cols)))

    model = implicit.als.AlternatingLeastSquares(
        factors=128,
        regularization=0.01,
        iterations=50,
        use_gpu=False,
    )
    model.fit(matrix)

    # Зберігаємо модель та маппінги
    with open('/models/collab_model.pkl', 'wb') as f:
        pickle.dump({ 'model': model, 'users': users, 'items': items }, f)
// Node.js: отримання рекомендацій через Python-сервіс
async function getCollaborativeRecs(userId, limit = 10) {
  const response = await fetch('http://ml-service:5000/recommend', {
    method: 'POST',
    body: JSON.stringify({ user_id: userId, limit }),
  });
  return response.json();
}

Гібридна система з персоналізацією

Об'єднуємо content-based та колаборативні сигнали:

async function getPersonalizedRecommendations(userId, currentArticleId) {
  const [contentBased, collaborative, trending] = await Promise.all([
    getSimilarArticles(currentArticleId, 10),
    getCollaborativeRecs(userId, 10),
    getTrendingArticles(10), // за переглядами за останні 24 години
  ]);

  // Об'єднуємо з вагами
  const scores = new Map();

  contentBased.forEach((article, i) => {
    scores.set(article.id, (scores.get(article.id) || 0) + (10 - i) * 0.4);
  });

  collaborative.forEach((article, i) => {
    scores.set(article.id, (scores.get(article.id) || 0) + (10 - i) * 0.5);
  });

  trending.forEach((article, i) => {
    scores.set(article.id, (scores.get(article.id) || 0) + (10 - i) * 0.1);
  });

  // Сортуємо за загальним скором
  const allArticleIds = [...scores.keys()];
  const articles = await fetchArticlesByIds(allArticleIds);

  return articles
    .map(a => ({ ...a, score: scores.get(a.id) }))
    .sort((a, b) => b.score - a.score)
    .slice(0, 6);
}

LLM-рекомендації з поясненням

Для умнішого підбору та персоналізованого пояснення:

async function getLLMRecommendations(user, readHistory, availableArticles) {
  const userProfile = `
    Прочитав: ${readHistory.map(a => a.title).join(', ')}
    Категорії інтересів: ${getTopCategories(readHistory).join(', ')}
    Середній час читання: ${user.avgReadTime} хв
  `;

  const articlesList = availableArticles.slice(0, 20).map(a =>
    `ID:${a.id} | ${a.title} | ${a.category} | ${a.tags.join(',')}`
  ).join('\n');

  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    response_format: { type: 'json_object' },
    messages: [
      {
        role: 'system',
        content: 'Ти рекомендаційна система. Відповідай JSON: { recommendations: [{id, reason}] }',
      },
      {
        role: 'user',
        content: `Профіль: ${userProfile}\n\nДоступні статті:\n${articlesList}\n\nВибери 4 найбільш релевантних для цього користувача.`,
      },
    ],
    max_tokens: 400,
  });

  const { recommendations } = JSON.parse(response.choices[0].message.content);

  // Збагачуємо даними з БД
  return Promise.all(recommendations.map(async rec => ({
    ...await fetchArticle(rec.id),
    reason: rec.reason, // "Ви читали подібні матеріали про React"
  })));
}

Трекінг подій

Дані про поведінку — основа для поліпшення рекомендацій:

// Клієнтський трекер
class ReadingTracker {
  constructor(articleId) {
    this.articleId = articleId;
    this.startTime = Date.now();
    this.maxScroll = 0;
    this.trackScroll();
  }

  trackScroll() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const progress = entry.target.dataset.progress;
          if (progress > this.maxScroll) {
            this.maxScroll = progress;
            this.sendEvent('scroll', { progress });
          }
        }
      });
    });

    document.querySelectorAll('[data-progress]').forEach(el => observer.observe(el));
  }

  async sendEvent(type, data = {}) {
    navigator.sendBeacon('/api/track', JSON.stringify({
      type,
      articleId: this.articleId,
      timeOnPage: Date.now() - this.startTime,
      ...data,
    }));
  }
}

Кешування рекомендацій

Рекомендації — дорога операція, кешуємо їх:

async function getCachedRecommendations(userId, articleId) {
  const cacheKey = `recs:${userId}:${articleId}`;
  const cached = await redis.get(cacheKey);

  if (cached) return JSON.parse(cached);

  const recs = await getPersonalizedRecommendations(userId, articleId);
  await redis.setex(cacheKey, 3600, JSON.stringify(recs)); // 1 година

  return recs;
}

Терміни

  • Content-based рекомендації через pgvector — 3–4 дні
  • Трекінг подій + аналітика поведінки — плюс 2 дні
  • Колаборативна фільтрація (implicit ALS) — плюс 3–4 дні
  • Гібридна система з LLM-поясненнями — 2–3 тижні повного циклу
  • A/B тестування алгоритмів — плюс 2–3 дні