Разработка системы рекомендаций товаров для интернет-магазина
Система рекомендаций — это алгоритмический движок, который увеличивает средний чек и глубину просмотра каталога. «Amazon рекомендует» — не маркетинговый слоган, а сложная инфраструктура с коллаборативной фильтрацией, поведенческими моделями и ML-пайплайнами. Разработка рекомендательной системы занимает от 10 до 20+ рабочих дней в зависимости от подхода.
Уровни сложности рекомендаций
Рекомендательные алгоритмы делятся по уровню сложности:
| Уровень | Подход | Применимость |
|---|---|---|
| Базовый | Rule-based (популярные, новинки, скидки) | Старт любого магазина |
| Средний | Item-based collaborative filtering | 10k+ заказов в БД |
| Продвинутый | Matrix factorization (ALS, SVD) | 100k+ событий |
| Enterprise | Deep learning (two-tower, BERT4Rec) | ML-инфраструктура |
Для большинства интернет-магазинов оптимален средний уровень: коллаборативная фильтрация по заказам, дополненная контентными сигналами.
Rule-based рекомендации (базовый уровень)
Запускается быстро, не требует накопленных данных:
class PopularityRecommender
{
public function getPopular(int $categoryId, int $limit = 8): Collection
{
return Product::where('category_id', $categoryId)
->where('is_active', true)
->withCount(['orderItems as sales_count' => fn($q) =>
$q->whereHas('order', fn($o) =>
$o->where('status', 'completed')
->where('created_at', '>=', now()->subDays(30))
)
])
->orderByDesc('sales_count')
->limit($limit)
->get();
}
}
Для новых пользователей — популярные товары. Для пользователей с историей — персональные.
Коллаборативная фильтрация по заказам
«Пользователи, купившие этот товар, также покупали...» — классический item-to-item CF:
-- Матрица совместных покупок
CREATE MATERIALIZED VIEW product_cooccurrences AS
SELECT
oi1.product_id AS product_a,
oi2.product_id AS product_b,
COUNT(DISTINCT oi1.order_id) AS cooccurrence_count
FROM order_items oi1
JOIN order_items oi2
ON oi1.order_id = oi2.order_id
AND oi1.product_id != oi2.product_id
JOIN orders o ON oi1.order_id = o.id
WHERE o.status = 'completed'
GROUP BY oi1.product_id, oi2.product_id
HAVING COUNT(DISTINCT oi1.order_id) >= 3;
CREATE INDEX idx_cooc_product_a ON product_cooccurrences(product_a, cooccurrence_count DESC);
Обновление materialized view — ночной cron:
$schedule->command('db:refresh-cooccurrences')->dailyAt('03:00');
Запрос рекомендаций по просматриваемому товару:
class CollaborativeRecommender
{
public function getSimilar(int $productId, int $limit = 8): Collection
{
$recommendedIds = DB::table('product_cooccurrences')
->where('product_a', $productId)
->orderByDesc('cooccurrence_count')
->limit($limit)
->pluck('product_b');
return Product::whereIn('id', $recommendedIds)
->where('is_active', true)
->orderByRaw("array_position(ARRAY[" . $recommendedIds->implode(',') . "]::bigint[], id)")
->get();
}
}
Персонализация по истории пользователя
Для авторизованных пользователей — рекомендации на основе их заказов и просмотров:
class PersonalizedRecommender
{
public function getForUser(User $user, int $limit = 12): Collection
{
// Категории из последних 5 заказов
$recentCategories = OrderItem::whereHas('order', fn($q) =>
$q->where('user_id', $user->id)->latest()->limit(5)
)->with('product:id,category_id')
->get()
->pluck('product.category_id')
->unique();
// Товары из этих категорий, которые пользователь ещё не покупал
$purchasedIds = $user->orders()
->with('items:product_id')
->get()
->flatMap(fn($o) => $o->items->pluck('product_id'))
->unique();
return Product::whereIn('category_id', $recentCategories)
->whereNotIn('id', $purchasedIds)
->where('is_active', true)
->inRandomOrder() // или по рейтингу
->limit($limit)
->get();
}
}
ML-рекомендации через Python-сервис
Для продвинутого уровня: Python-микросервис на FastAPI с моделью ALS (Alternating Least Squares) из библиотеки implicit:
# recommendations_service/main.py
import implicit
import numpy as np
from scipy.sparse import csr_matrix
from fastapi import FastAPI
app = FastAPI()
model = implicit.als.AlternatingLeastSquares(factors=64, iterations=20)
@app.get("/recommendations/user/{user_id}")
async def user_recommendations(user_id: int, n: int = 12):
user_idx = user_id_to_idx.get(user_id)
if user_idx is None:
return {"items": get_popular_fallback(n)}
ids, scores = model.recommend(user_idx, user_item_matrix[user_idx], N=n)
product_ids = [idx_to_product_id[i] for i in ids]
return {"items": product_ids, "scores": scores.tolist()}
PHP-бэкенд запрашивает сервис и кеширует результат:
$recommendations = Cache::remember(
"recs:user:{$userId}",
1800,
fn() => Http::timeout(2)->get("{$this->mlServiceUrl}/recommendations/user/{$userId}", ['n' => 12])
->throw()
->json('items')
);
Fallback при недоступности ML-сервиса — rule-based рекомендации.
Трекинг событий для обучения модели
Качество рекомендаций зависит от данных. Трекинг событий:
// Просмотр товара
api.post('/events', {
type: 'product_view',
product_id: product.id,
session_id: getSessionId(),
timestamp: new Date().toISOString(),
});
// Добавление в корзину
api.post('/events', { type: 'add_to_cart', product_id, quantity });
// Покупка (через backend после создания заказа)
CREATE TABLE recommendation_events (
id BIGSERIAL PRIMARY KEY,
event_type VARCHAR(30) NOT NULL,
user_id BIGINT,
session_id VARCHAR(64),
product_id BIGINT,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_rec_events_user ON recommendation_events(user_id, created_at DESC);
A/B тестирование алгоритмов
Разные алгоритмы показываются разным сегментам пользователей:
$algorithm = $user->id % 2 === 0 ? 'collaborative' : 'ml';
$recommendations = $this->recommenderFactory->make($algorithm)->get($user, $product);
// Логируем для анализа CTR и конверсии
event(new RecommendationShown($user, $recommendations, $algorithm, $context));
После 2–4 недель сравниваем CTR и конверсию по алгоритмам и выбираем победителя.
Размещение блоков рекомендаций
- Страница товара: «С этим товаром покупают», «Похожие товары»
- Корзина: «Вы забыли добавить», «Дополните комплект»
- Главная страница: «Персональные рекомендации», «Популярное»
- Email после покупки: «Вам может понравиться»
- Пустая страница поиска: «Попробуйте эти товары»







