Реализация AI-рекомендательной системы контента (Content-Based) в мобильном приложении
Content-Based Filtering не требует данных о других пользователях — он строит профиль конкретного пользователя на основе характеристик контента, с которым тот взаимодействовал. Для новых приложений без накопленной пользовательской базы это часто единственный работающий вариант рекомендаций с первого дня.
Когда Content-Based выигрывает у Collaborative Filtering
Три сценария, где CB-подход предпочтительнее:
Нишевой контент с уникальными метаданными. Статьи, рецепты, туристические маршруты — каждый айтем имеет богатый набор атрибутов (теги, категории, авторы, локации). CF работает на сигнале «пользователи похожи», но для нишевого контента похожих пользователей может быть слишком мало.
Privacy-first архитектура. CB может работать полностью на устройстве — профиль пользователя хранится локально, рекомендации строятся без отправки данных на сервер.
Длинный хвост контента. Новая статья, опубликованная час назад, не имеет истории взаимодействий для CF. CB рекомендует её сразу, как только метаданные проиндексированы.
Ядро системы: представление контента и пользователя
TF-IDF и эмбеддинги для текстового контента
Для статей, описаний, новостей — два подхода: TF-IDF для скорости, sentence embeddings для качества. На практике используем sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (278 МБ, поддерживает русский): каждый айтем превращается в вектор 384 измерений.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
def embed_article(article: Article) -> np.ndarray:
text = f"{article.title}. {article.description}. {' '.join(article.tags)}"
return model.encode(text, normalize_embeddings=True)
# Косинусное сходство через numpy
def similarity(v1: np.ndarray, v2: np.ndarray) -> float:
return float(np.dot(v1, v2)) # нормализованные векторы, dot = cosine
Профиль пользователя — скользящее среднее эмбеддингов
Профиль пользователя — это взвешенное среднее эмбеддингов контента, с которым он взаимодействовал. Недавние взаимодействия весят больше (экспоненциальное затухание):
def update_user_profile(profile: np.ndarray, new_item_embedding: np.ndarray,
interaction_weight: float, decay: float = 0.9) -> np.ndarray:
updated = decay * profile + (1 - decay) * interaction_weight * new_item_embedding
return updated / np.linalg.norm(updated) # renormalize
On-device рекомендации на iOS через CoreML
Для небольших каталогов (до 50K айтемов) весь CB-поиск можно вынести на устройство:
// iOS: локальный CB-поиск без сетевых запросов
class OnDeviceRecommender {
private let userProfileKey = "user_embedding_v2"
private var itemIndex: [(id: String, embedding: [Float])] = []
func loadItemIndex(from url: URL) {
// загружаем предвычисленные эмбеддинги при запуске
let data = try! Data(contentsOf: url)
itemIndex = try! JSONDecoder().decode([(id: String, embedding: [Float])].self, from: data)
}
func getRecommendations(count: Int) -> [String] {
guard let profileData = UserDefaults.standard.data(forKey: userProfileKey),
let profile = try? JSONDecoder().decode([Float].self, from: profileData)
else { return popularItemIds(count: count) }
return itemIndex
.map { item in (item.id, cosineSimilarity(profile, item.embedding)) }
.sorted { $0.1 > $1.1 }
.prefix(count)
.map { $0.0 }
}
private func cosineSimilarity(_ a: [Float], _ b: [Float]) -> Float {
zip(a, b).map(*).reduce(0, +) // предполагаем нормализованные векторы
}
}
Эмбеддинги индекса обновляются при старте приложения или по расписанию — загружаем JSON с предвычисленными векторами с сервера (~20 МБ для 50K айтемов × 384d float32).
Структурированные метаданные: не только текст
Для каталога товаров текстовые эмбеддинги дополняются категориальными признаками: категория, бренд, ценовой диапазон, цвет. Финальный вектор — конкатенация нормализованного текстового эмбеддинга и one-hot/ordinal признаков:
def build_item_vector(item: Product) -> np.ndarray:
text_emb = embed_text(f"{item.name} {item.description}") # 384-dim
cat_features = encode_categorical({
'category': item.category_id,
'brand': item.brand_id,
'price_range': bucket_price(item.price) # [0-500, 500-2000, 2000+]
}) # ~50-dim
return np.concatenate([text_emb * 0.7, cat_features * 0.3]) # взвешенная конкатенация
Процесс работы
Анализ структуры контента: какие метаданные доступны, их качество и полнота.
Выбор модели эмбеддингов под язык и домен.
Построение индекса и механизма обновления профиля пользователя.
Решение про on-device vs серверные рекомендации исходя из размера каталога и требований к privacy.
Ориентиры по срокам
Серверный CB с готовыми эмбеддингами и API — 1–1,5 недели. On-device вариант для iOS/Android с локальным индексом — 2–3 недели. Гибрид с частичной on-device обработкой — 3–4 недели.







