Персонализация ранжирования поиска в e-commerce
Поисковый движок без персонализации показывает одинаковые результаты всем пользователям. ML-ранжирование учитывает историю просмотров, покупок, возвратов и контекст сессии — и переставляет выдачу индивидуально. Выигрыш: +8-15% к конверсии из поиска.
Learning-to-Rank архитектура
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 ===
# BM25-подобный скор от поискового движка (Elasticsearch/OpenSearch)
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)
# CTR пользователя в этой категории (personal CTR)
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):
"""
training_data: query_id, sku, features..., relevance_label
relevance_label: 0=impression, 1=click, 2=add_to_cart, 3=purchase
"""
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)
Query Understanding и расширение
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 # 20% буст для сезонных запросов
return 1.0
Онлайн A/B тестирование ранжирования
class SearchRankingExperiment:
"""A/B/n тесты для алгоритмов ранжирования"""
def __init__(self, variants: dict):
"""variants: {'control': ranker_v1, 'treatment': ranker_v2}"""
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)
Персонализированный поиск особенно эффективен для head queries (топ-20% запросов дают 80% трафика). Для tail queries (редкие запросы) — semantic search через векторный индекс важнее персонализации. Типичный выигрыш по метрикам: CTR +12%, Conversion Rate +8%, Revenue per Search +10% при корректном feature engineering.







