AI-рекомендаційна система контенту (Content-Based) в мобільних додатках
Content-Based Filtering не потребує даних про інших користувачів — будує профіль конкретного користувача на основі характеристик контенту, з яким той взаємодіяв. Для нових додатків без накопленої користувальницької базри це часто єдиний робочий варіант рекомендацій з першого дня.
Коли Content-Based переважає Collaborative Filtering
Три сценарії, де CB-підхід переважніший:
Нішевий контент з унікальними метаданими. Статті, рецепти, туристичні маршрути — кожний айтем має багатий набір атрибутів (теги, категорії, автори, локації). CF працює на сигналі "користувачі схожі", але для нішевого контенту схожих користувачів може бути замало.
Privacy-first архітектура. CB може працювати повністю на пристрої — профіль користувача зберігається локально, рекомендації будуються без відправки даних на сервер.
Довгий хвіст контенту. Нова стаття, опублікована час назад, не має історії взаємодій для CF. CB рекомендує її одразу, як тільки метаданні проіндексовані.
Ядро системи: представлення контенту та користувача
TF-IDF та embeddings для текстового контенту
Для статей, описів, новин — два підходи: TF-IDF для швидкості, sentence embeddings для якості. На практиці використовуємо sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (278 MB, підтримує російську): кожний айтем перетворюється на 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
Профіль користувача — скользящее середнє embeddings
Профіль користувача — це взвешене середнє embeddings контенту, з яким він взаємодіяв. Недавні взаємодії важать більше (експоненціальне затухання):
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) # перенормалізуємо
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) {
// загружаємо передвичислені embeddings при запуску
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, +) // припускаємо нормалізовані вектори
}
}
Embeddings індекс оновляється при старті додатка або за розкладом — загружаємо JSON з передвичисленими векторами з сервера (~20 MB для 50K айтемів × 384d float32).
Структуровані метаданні: не тільки текст
Для каталогу товарів текстові embeddings доповнюються категоріальними ознаками: категорія, бренд, ціновий діапазон, колір. Остаточний вектор — конкатенація нормалізованого текстового embedding та 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]) # взвешена конкатенація
Процес роботи
Аналіз структури контенту: які метаданні доступні, їх якість та повнота.
Вибір моделі embeddings під мову та домен.
Побудова індексу та механізму оновлення профілю користувача.
Рішення про on-device vs серверні рекомендації в залежності від розміру каталогу та вимог до privacy.
Ориентири за часовими рамками
Серверний CB з готовими embeddings та API — 1–1,5 тижня. On-device варіант для iOS/Android з локальним індексом — 2–3 тижні. Гібрид з частковою on-device обробкою — 3–4 тижні.







