Реализация AI-оптимизации поисковой выдачи в мобильном приложении
Поиск в мобильном приложении проигрывает тому же поиску в вебе не из-за худших алгоритмов — а из-за ограниченного пространства экрана. На мобиле пользователь видит 5–7 результатов без скролла. Если ни один не релевантен — он закрывает приложение. Оптимизация ранжирования поисковой выдачи для мобиля важна вдвойне.
Что ломается в стандартных поисковых движках
BM25 и TF-IDF хорошо работают на точных текстовых совпадениях. Но мобильный поиск — это короткие запросы («nike белые 42»), голосовые запросы с транскрипционными ошибками, визуальный поиск. BM25 с такими входными данными даёт нерелевантные результаты, потому что не понимает семантику.
Второй класс проблем — персонализация. Запрос «кроссовки» для мужчины 28 лет и для женщины 55 лет должен давать разные топ-результаты. BM25 об этом ничего не знает.
Learning to Rank: как работает AI-ранжирование
Три подхода LTR
Pointwise: обучаем модель предсказывать relevance score для пары (запрос, документ). Просто, но не учитывает относительный порядок в выдаче.
Pairwise: модель учится упорядочивать пары документов (A лучше B для запроса Q). RankNet, LambdaRank — в этой категории.
Listwise: оптимизирует метрики качества ранжирования (NDCG, MAP) напрямую по всей выдаче. Лучшее качество, сложнее в реализации.
Для большинства мобильных приложений оптимум — pairwise LightGBM с LambdaRank objective. Обучается на логах поисковых сессий: что пользователь кликнул, что проигнорировал.
Feature engineering для поискового ранжировщика
@dataclass
class SearchRankingFeatures:
# Query-Document relevance
bm25_score: float
exact_match_title: bool
semantic_similarity: float # cosine между query и doc эмбеддингами
# Document quality
click_through_rate: float # исторический CTR из поиска
avg_session_time_after_click: float # время на карточке после клика
conversion_rate: float # покупки / клики из поиска
# User personalization
category_affinity: float # сходство категории с историей юзера
brand_affinity: float
price_range_match: bool # цена в привычном диапазоне
# Context
query_length: int
is_voice_query: bool
device_screen_dpi: int # для оптимизации под экран
Elasticsearch + ML ранжировщик
Elasticsearch — стандартный первичный retrieval движок. Результаты BM25 из ES передаются в ML ранжировщик как кандидаты:
async def search(query: str, user: User, size: int = 20) -> list[SearchResult]:
# Stage 1: BM25 retrieval
es_results = await elasticsearch.search(
index="products",
body={
"query": {"multi_match": {"query": query, "fields": ["title^3", "description", "tags"]}},
"size": 100 # берём 100 кандидатов для переранжирования
}
)
candidates = [SearchResult.from_es(hit) for hit in es_results["hits"]["hits"]]
# Stage 2: ML reranking
features = extract_features(query, candidates, user)
scores = ranker.predict(features)
return sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)[:size]
Семантический поиск через эмбеддинги
Для запросов с семантическим смыслом (не точное совпадение): query и документы кодируются в вектора, ищем ближайших соседей через FAISS. Хорошо работает в паре с BM25 через Reciprocal Rank Fusion:
def reciprocal_rank_fusion(bm25_results: list, semantic_results: list, k=60) -> list:
scores = defaultdict(float)
for rank, doc_id in enumerate(bm25_results):
scores[doc_id] += 1 / (k + rank + 1)
for rank, doc_id in enumerate(semantic_results):
scores[doc_id] += 1 / (k + rank + 1)
return sorted(scores, key=scores.get, reverse=True)
Мобильная часть: UX ранжирования
// iOS: отображение поисковой выдачи с skeleton loading
struct SearchResultsView: View {
@StateObject var viewModel: SearchViewModel
var body: some View {
List {
if viewModel.isLoading {
ForEach(0..<6) { _ in
SearchResultSkeletonRow()
}
} else {
ForEach(viewModel.results) { result in
SearchResultRow(result: result)
.onAppear {
viewModel.trackImpression(result.id)
}
.onTapGesture {
viewModel.trackClick(result.id)
navigateTo(result)
}
}
}
}
}
}
Трекинг impressions и clicks прямо в UI — это данные для обучения следующей версии ранжировщика. Без этого логирования модель деградирует.
Процесс работы
Аудит текущего поискового движка и качества логирования кликов/конверсий.
Feature engineering и сбор обучающих данных из поисковых сессий.
Обучение LTR-модели и offline-оценка по NDCG@10.
Деплой переранжировщика за Elasticsearch + мобильная интеграция.
A/B тест: LTR vs baseline BM25 → CTR@5 и конверсия из поиска.
Ориентиры по срокам
BM25 + базовые персонализационные фильтры — 1 неделя. LTR-ранжировщик с feature engineering — 3–4 недели. Семантический поиск с FAISS + RRF fusion — ещё 2 недели поверх.







