Реалізація AI-оптимізації пошукової видачі в мобільному додатку
Пошук у мобільному додатку програє тому ж пошуку в вебі не через гірші алгоритми — а через обмежений простір екрана. На мобілі користувач бачить 5–7 результатів без скролінгу. Якщо жоден не релевантний — він закриває додаток. Оптимізація рейтингу пошукової видачі для мобіля важлива вдвічі.
Що ломається в стандартних пошукових movimento
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 embedding
# 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 movement. Результати 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]
Семантичний пошук через embeddings
Для запитів із семантичним смислом (не точне збігання): 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 тижні поверх.







