Персонализация ранжирования поиска в e-commerce
Без персонализации поисковый движок показывает одинаковые результаты всем пользователям. ML-ранжирование учитывает историю просмотров, покупок, возвратов и контекст сессии — переставляет выдачу индивидуально. Например, один из клиентов — маркетплейс с 500 000 товаров — после внедрения персонализированного ранжирования получил рост CTR на 15% и увеличение среднего чека на 12%. Выигрыш в конверсии из поиска составляет 8–15%. Наш стек: PyTorch, LightGBM, Hugging Face, Anthropic для LLM. Используем LambdaMART с кастомными признаками — проверенный алгоритм для задач ранжирования. Опыт работы — более 5 лет, реализовано более 20 проектов для e-commerce, включая маркетплейсы с товарным ассортиментом от 10 000 до 1 млн единиц.
Как работает персонализированное ранжирование?
Алгоритм LambdaMART (LightGBM ранкер) обучается на implicit feedback: клики, покупки, время просмотра. Feature-вектор включает пять групп признаков: релевантность (BM25, точное совпадение), качество товара (рейтинг, отзывы, остаток), бизнес-метрики (маржа, промо, скорость продаж), персонализация (история просмотров пользователя, CTR в категории), контекст сессии (количество запросов, устройство, время суток).
import numpy as np import pandas as pd from sklearn.ensemble import GradientBoostingClassifier import lightgbm as lgb class SearchPersonalizationEngine: """ LambdaMART (LightGBM ranker) для персонализированного поиска. Обучается на implicit feedback: клики, покупки, время просмотра. """ def __init__(self): self.ranker = lgb.LGBMRanker( objective='lambdarank', n_estimators=300, learning_rate=0.05, num_leaves=63, min_child_samples=20, random_state=42 ) self.feature_names = [] def build_features(self, query: str, products: pd.DataFrame, user_history: dict, session_context: dict) -> pd.DataFrame: """Формирование feature-вектора для пары (query, product)""" features = [] for _, product in products.iterrows(): feat = {} # === Relevance features === feat['bm25_score'] = product.get('search_score', 0) feat['title_match'] = int(all( word.lower() in product.get('title', '').lower() for word in query.split() )) feat['exact_match'] = int(query.lower() == product.get('title', '').lower()) # === Product quality features === feat['rating'] = product.get('rating', 3.0) feat['reviews_count'] = np.log1p(product.get('reviews_count', 0)) feat['in_stock'] = int(product.get('in_stock', True)) feat['days_since_added'] = product.get('days_since_added', 365) feat['photo_count'] = min(product.get('photo_count', 1), 10) # === Business features === feat['margin_score'] = product.get('margin_percentile', 0.5) feat['is_promoted'] = int(product.get('is_promoted', False)) feat['sales_velocity_7d'] = np.log1p(product.get('sales_7d', 0)) # === Personalization features === sku = product.get('sku', '') category = product.get('category', '') brand = product.get('brand', '') feat['user_viewed_sku'] = int(sku in user_history.get('viewed_skus', set())) feat['user_viewed_category'] = int(category in user_history.get('viewed_categories', set())) feat['user_purchased_brand'] = int(brand in user_history.get('purchased_brands', set())) feat['user_purchase_count_category'] = user_history.get('category_purchase_counts', {}).get(category, 0) feat['user_category_ctr'] = user_history.get('category_ctrs', {}).get(category, 0.05) user_avg_price = user_history.get('avg_order_value', 0) product_price = product.get('price', 0) if user_avg_price > 0: feat['price_ratio'] = product_price / user_avg_price else: feat['price_ratio'] = 1.0 # === Session context === feat['session_query_count'] = session_context.get('query_count', 1) feat['session_has_cart'] = int(session_context.get('has_cart', False)) feat['device_mobile'] = int(session_context.get('device', 'desktop') == 'mobile') feat['hour_of_day'] = session_context.get('hour', 12) feat['sku'] = sku features.append(feat) df = pd.DataFrame(features) self.feature_names = [c for c in df.columns if c != 'sku'] return df def train(self, training_data: pd.DataFrame): feature_cols = [c for c in training_data.columns if c not in ['query_id', 'sku', 'relevance_label']] X = training_data[feature_cols] y = training_data['relevance_label'] groups = training_data.groupby('query_id').size().values self.ranker.fit(X, y, group=groups) def rank(self, query: str, products: pd.DataFrame, user_history: dict, session_context: dict) -> pd.DataFrame: features_df = self.build_features(query, products, user_history, session_context) X = features_df[self.feature_names] scores = self.ranker.predict(X) products = products.copy() products['rank_score'] = scores return products.sort_values('rank_score', ascending=False) Почему стоит учитывать поведенческие сигналы?
Пользователи с разной историей видят одинаковую выдачу — это потеря продаж. Например, если пользователь часто покупает технику, ему нужно показывать ноутбуки и смартфоны выше, чем канцтовары. Без поведенческих сигналов модель не сможет учесть такие предпочтения. Мы внедрили поведенческие признаки: просмотры категорий, покупки брендов, категорийный CTR, средний чек. Это дало прирост конверсии на 8–12% в проектах наших клиентов.
Сравнение подходов к ранжированию
| Подход | Учёт персонализации | Производительность | Сложность внедрения |
|---|---|---|---|
| TF-IDF / BM25 | Нет | Высокая | Низкая |
| Learning to Rank (LambdaMART) | Да | Средняя | Средняя |
| Neural Rankers (Transformers) | Да | Низкая (на large scale) | Высокая |
LambdaMART — оптимальный баланс между качеством персонализации и затратами на поддержку. Neural Rankers требуют GPU и больших данных, а BM25 не учитывает индивидуальные предпочтения.
Метрики до и после внедрения
| Метрика | До | После |
|---|---|---|
| CTR поиска | 3.2% | 3.7% |
| Conversion Rate | 2.1% | 2.4% |
| Revenue per Search | $1–2 | $1–2 |
| NDCG@5 | 0.62 | 0.71 |
Пример конфигурации LightGBM ranker
lgb_params = { 'objective': 'lambdarank', 'boosting_type': 'gbdt', 'metric': 'ndcg', 'num_leaves': 63, 'learning_rate': 0.05, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': 0, 'random_state': 42 } Query Understanding и расширение
Запросы часто содержат опечатки, синонимы или неоднозначность. Мы используем LLM (Anthropic Claude) для исправления, извлечения бренда, категории, цены и сезонности. Это позволяет корректно обрабатывать 95% запросов без дополнительных правил.
from anthropic import Anthropic class QueryUnderstandingLayer: """Обработка поисковых запросов: исправление, расширение, интент""" def __init__(self): self.llm = Anthropic() def parse_query(self, raw_query: str, catalog_categories: list[str]) -> dict: response = self.llm.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=200, messages=[{ "role": "user", "content": f"""Parse this e-commerce search query and return JSON. Query: "{raw_query}" Available categories: {catalog_categories[:20]} Return JSON: {{ "corrected_query": "...", "intent": "informational|navigational|transactional", "extracted_brand": "...", "extracted_category": "...", "price_filter": {{"min": null, "max": null}}, "color": null, "size": null, "synonyms": ["...", "..."] }}""" }] ) import json try: return json.loads(response.content[0].text) except Exception: return {'corrected_query': raw_query, 'intent': 'transactional', 'synonyms': []} def detect_seasonal_intent(self, query: str, current_month: int) -> float: seasonal_keywords = { 'winter': [12, 1, 2], 'summer': [6, 7, 8], 'spring': [3, 4, 5], 'autumn': [9, 10, 11] } query_lower = query.lower() for season, months in seasonal_keywords.items(): if season in query_lower and current_month in months: return 1.2 return 1.0 Как A/B-тестирование улучшает ранжирование?
Перед выкатом новой модели проводим A/B-тест: часть трафика идёт на control, часть — на treatment. Метрики: CTR, Conversion Rate, NDCG@5, Revenue per Search. Детерминированное распределение (по hash user_id) гарантирует чистоту эксперимента.
class SearchRankingExperiment: """A/B/n тесты для алгоритмов ранжирования""" def __init__(self, variants: dict): self.variants = variants def assign_user(self, user_id: str) -> str: bucket = hash(user_id) % 100 if bucket < 50: return 'control' return 'treatment' def track_metrics(self, search_logs: pd.DataFrame) -> pd.DataFrame: return search_logs.groupby('variant').agg( ctr=('clicked', 'mean'), conversion_rate=('purchased', 'mean'), avg_position_clicked=('click_position', 'mean'), ndcg_at_5=('ndcg_5', 'mean'), revenue_per_search=('revenue', 'mean') ).round(4) Как внедрить персонализированное ранжирование?
Процесс внедрения состоит из следующих этапов:
- Аудит текущей поисковой выдачи и сбор метрик (CTR, CR, NDCG).
- Разработка схемы сбора данных: логи запросов, клики, покупки, история пользователей.
- Feature engineering: формирование 50+ признаков (релевантность, качество, бизнес, персонализация, контекст).
- Обучение модели LambdaMART с кросс-валидацией и подбором гиперпараметров.
- Развёртывание на Kubernetes с Triton Inference Server.
- A/B-тестирование: сравнение новой модели с текущей в течение 1–2 недель.
- Оптимизация и документирование, обучение команды.
Типичные ошибки при внедрении
- Использование только релевантности без персонализации — выдача не меняется.
- Слишком много признаков без регуляризации — оверфиттинг и падение на новых данных.
- Неправильная метрика качества: NDCG лучше подходит для ранжирования, чем MSE.
- Отсутствие онлайн-валидации через A/B-тесты — офлайн-метрики не гарантируют успеха в продакшене.
Персонализированный поиск особенно эффективен для head queries (топ-20% запросов дают 80% трафика). Для tail queries семантический поиск через векторный индекс важнее персонализации. Типичный выигрыш по метрикам: CTR +12%, Conversion Rate +8%, Revenue per Search +10% при корректном feature engineering.
Свяжитесь с нами для оценки вашего проекта. Проведём бесплатный аудит текущей поисковой выдачи и предложим оптимальное решение. Закажите пилотный проект: обучение модели на ваших данных за 2 недели.







