Реализация AI-исправления опечаток в поиске мобильного приложения
Мобильная клавиатура промахивается. Swipe-ввод, автозамена, маленькие клавиши — среднее количество опечаток при мобильном вводе в 2–3 раза выше, чем при десктопном. При этом пользователь ожидает, что поиск понял его, а не выдал «0 результатов». «0 результатов» — это выход из приложения.
Типы ошибок и подходы к исправлению
Ошибки в мобильном вводе делятся на три категории, и для каждой нужен свой инструмент:
Опечатки (transposition, deletion, substitution) — «кроссвки» вместо «кроссовки». Обрабатываются алгоритмами редакционного расстояния: Levenshtein, Damerau-Levenshtein (учитывает транспозицию соседних символов, что типично для мобиля).
Фонетические ошибки — пользователь пишет как слышит: «найк» → «nike», «адидас» → «adidas». Для русского языка: метафонный алгоритм или специализированный phonetic encoder под кириллицу.
Транслитерация — «krossovki», «кроссовки», «crossovki» должны давать одинаковый результат. Стандартные транслитерационные таблицы + нормализация перед индексацией.
Elasticsearch: spell correction из коробки и его ограничения
ES предоставляет term suggester с fuzzy matching. Работает, но:
- ищет ближайшие термины из индекса по edit distance — не учитывает контекст запроса
- слабо работает для коротких токенов (< 4 символов) из-за количества вариантов с ed=1
- нет учёта частотности: «найки» (опечатка) и «найки» (брендовый термин) получают одинаковый приоритет
# ES term suggester — базовый уровень
response = await es.search(
index="products",
body={
"suggest": {
"spell_suggest": {
"text": query,
"term": {
"field": "title",
"suggest_mode": "missing", # только если термин не найден
"max_edits": 2,
"min_word_length": 4,
"string_distance": "jaro_winkler"
}
}
}
}
)
jaro_winkler лучше подходит для коротких строк, чем levenshtein — он придаёт больший вес совпадениям в начале строки.
SymSpell: быстрее Levenshtein на порядок
Для продакшна при > 1000 запросов/сек стандартный Levenshtein не подходит из-за O(n²) сложности. SymSpell (Symmetric Delete) предвычисляет все возможные удаления до максимального edit distance и хранит в хэш-таблице. Время lookup — O(1) для большинства запросов.
from symspellpy import SymSpell, Verbosity
sym_spell = SymSpell(max_dictionary_edit_distance=2, prefix_length=7)
sym_spell.load_dictionary("ru_frequency_dict.txt", term_index=0, count_index=1)
def correct_query(query: str) -> str:
suggestions = sym_spell.lookup_compound(
query,
max_edit_distance=2,
transfer_casing=True
)
if suggestions and suggestions[0].distance > 0:
return suggestions[0].term
return query
Частотный словарь для русского языка строим из поисковых логов приложения — это важно: «кожаный ремень» будет в топе частотности именно в контексте вашего домена, а не «кожаная куртка» из универсального словаря Яндекса.
Контекстное исправление через N-gram Language Model
SymSpell исправляет каждое слово независимо. «кросовки адидас» исправит оба слова правильно. Но «белие кроссовки» — SymSpell может предложить «белые» или «белее», не зная, какой вариант грамматически корректен в контексте.
N-gram language model на поисковых логах помогает выбрать правильный вариант: P("белые кроссовки") >> P("белее кроссовки").
Мобильная интеграция: UX исправления
// Android: отображение исправления с возможностью отмены
@Composable
fun SearchResultsHeader(
originalQuery: String,
correctedQuery: String?,
onRevertToOriginal: () -> Unit
) {
if (correctedQuery != null && correctedQuery != originalQuery) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = buildAnnotatedString {
append("Результаты для: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(correctedQuery)
}
}
)
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = onRevertToOriginal) {
Text("Искать «$originalQuery»")
}
}
}
}
Паттерн «мы исправили — но вы можете вернуть оригинал» — стандарт индустрии. Не навязывайте исправление без escape hatch.
// iOS: аналогичный подход через SwiftUI
struct CorrectionNoticeView: View {
let original: String
let corrected: String
let onRevert: () -> Void
var body: some View {
HStack {
Text("Показываем результаты для «\(corrected)»")
.font(.subheadline)
Spacer()
Button("Искать «\(original)»", action: onRevert)
.font(.subheadline)
}
.padding(.horizontal)
.padding(.vertical, 6)
.background(Color(.systemGray6))
}
}
Процесс работы
Сбор частотного словаря из поисковых логов приложения.
Анализ типичных опечаток: какие символы путают на конкретной клавиатуре.
Настройка SymSpell + ES phrase suggester.
Интеграция исправления в search API + UX на клиентах.
Метрика качества: zero-result rate до и после внедрения.
Ориентиры по срокам
ES fuzzy + SymSpell с готовым словарём — 2–4 дня. Кастомный частотный словарь из логов + N-gram LM для контекстного исправления — 1–2 недели дополнительно.







