Разработка поиска по изображению в мобильном приложении
Пользователь фотографирует товар в магазине конкурента или делает скриншот из Instagram — и хочет найти то же самое в вашем каталоге. Visual search закрывает этот сценарий. Технически задача состоит из двух частей: получить векторное представление изображения (embedding) и найти по нему ближайших соседей в базе.
Два подхода к реализации
Embedded модель на устройстве. На iOS — Vision фреймворк с VNGenerateImageFeaturePrintRequest, на Android — ML Kit Image Labeling или кастомная TFLite-модель через TensorFlow Lite Task Library. Преимущество: работает offline, нет задержки сети. Ограничение: feature print от Apple работает только внутри экосистемы iOS и не совместим с серверным индексом.
Серверный embedding. Изображение отправляется на сервер, там прогоняется через модель (CLIP, EfficientNet, ResNet), возвращается вектор, который ищется по индексу. Это точнее и гибче — один и тот же индекс работает с iOS, Android, вебом.
На практике чаще выбираем серверный вариант с локальным pre-processing: изображение сжимается и нормализуется на устройстве до отправки.
Захват и подготовка изображения
На iOS для выбора из галереи — PHPickerViewController (не UIImagePickerController, он deprecated с iOS 14). Для камеры — AVCaptureSession с AVCapturePhotoOutput. Изображение перед отправкой:
func prepareForSearch(image: UIImage) -> Data? {
// Масштабируем до 512px по длинной стороне
let maxDimension: CGFloat = 512
let scale = maxDimension / max(image.size.width, image.size.height)
let newSize = CGSize(width: image.size.width * scale,
height: image.size.height * scale)
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
image.draw(in: CGRect(origin: .zero, size: newSize))
let resized = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return resized?.jpegData(compressionQuality: 0.85)
}
На Android — ActivityResultContracts.TakePicture() для камеры и PickVisualMedia() для галереи (Photo Picker API, доступен с Android 13 и через registerForActivityResult через Jetpack).
Серверный поиск: векторный индекс
Для поиска ближайших соседей по embedding используем Qdrant, Weaviate или pgvector (если уже PostgreSQL в стеке). CLIP-модель от OpenAI даёт хорошие результаты для товарного поиска — она обучена на пары изображение-текст, поэтому работает в обе стороны: по фото найти текст и наоборот.
Запрос к серверу с прогресс-индикатором:
// Android, Retrofit + OkHttp
suspend fun searchByImage(imageBytes: ByteArray): List<SearchResult> {
val requestBody = imageBytes.toRequestBody("image/jpeg".toMediaType())
val part = MultipartBody.Part.createFormData("image", "search.jpg", requestBody)
return searchApi.visualSearch(part)
}
Важно: обрабатываем случай, когда сервер не нашёл близких совпадений (cosine distance > порога). Показываем «ничего не найдено» честно, а не возвращаем нерелевантные результаты с дальними векторами.
Предобработка с CoreML / TFLite на устройстве
Если нужен offline или нужно ускорить отклик — встраиваем лёгкую модель. MobileNetV3 или EfficientNet-Lite дают разумный компромисс между точностью и размером. На iOS конвертируем в .mlmodel через coremltools, на Android — в .tflite. Локальный индекс хранится в SQLite с расширением для косинусного расстояния или используем Faiss через JNI/FFI.
Процесс работы
Аудит каталога: размер, тип товаров, требования к точности поиска.
Выбор архитектуры: серверный embedding или гибридный (локальная предобработка + серверный индекс).
Подготовка эталонных embeddings для каталога, настройка векторного индекса.
Разработка UI: выбор фото, кроп-инструмент (опционально), отображение результатов с similarity score.
Тестирование с реальными пользовательскими фотографиями — плохое освещение, углы, частичные совпадения.
Ориентиры по срокам
Интеграция с готовым серверным API поиска — 3–5 дней. Полная реализация включая серверную часть (embedding-сервис, векторный индекс, загрузку каталога) — 3–6 недель в зависимости от объёма каталога и требуемой точности.







