Семантический поиск с помощью Embeddings API

В этом руководстве рассказывается, как реализовать на устройстве семантический поиск – метод поиска, который находит релевантный контент на основе значения запроса, а не только по ключевым словам. Семантический поиск избавляет от необходимости вручную создавать и поддерживать таблицы синонимов или таксономии тегов. Чтобы реализовать семантический поиск на устройстве, используйте следующие библиотеки:

  • ML Kit SDK, в частности Embeddings API, для создания векторных эмбеддингов.
  • AndroidX AppSearch для хранения векторных эмбеддингов и поиска структурированных данных с использованием метрик косинусного расстояния, чтобы находить контент по его семантике.

Рабочий пример можно найти в примере приложения ML Kit GenAI на GitHub.

Требования

Чтобы использовать семантический поиск на устройстве, должны выполняться следующие требования к устройству и аккаунту:

  • Ваше устройство поддерживает Android AICore. Большинство устройств с поддержкой AICore используют модели с ускорением NPU, а остальные поддерживаемые устройства возвращаются к модели ЦП (с ускорением XNNPACK).
  • Аккаунт Google на вашем устройстве участвует в программе предварительной версии для разработчиков AICore.
  • На устройстве установлена последняя версия APK-файла предварительной версии AICore для разработчиков. Чтобы установить ее, откройте страницу Android AICore в Google Play и нажмите Обновить. Вы также можете открыть Настройки устройства, найти Системные службы или Сервисы Google, нажать Android AICore (бета) и выбрать Обновить, если доступно обновление.
  • На устройстве установлена последняя версия приложения сервисы конфиденциальных вычислений. Чтобы установить ее, откройте страницу сервисы конфиденциальных вычислений в Google Play и нажмите Обновить. Вы также можете перейти в Google Play > Управление приложениями и устройством > Управление > сервисы конфиденциальных вычислений и нажать Обновить, если доступно обновление.

Поддерживаемые модели и конфигурация

ML Kit Embeddings API использует семейство моделей Google EmbeddingGemma, управляемое Android AICore. В частности, Embeddings API использует EmbeddingGemma 2 (embedding_gemma_v2) для генерации встраиваний для текстовых и графических входных данных.

Ниже приведены спецификации и ограничения для входных данных.

Входное свойство EmbeddingGemma 2 Примечания
Поддерживаемые модальности Текст и изображение (Modality.TEXT, Modality.IMAGE) Чтобы убедиться, что ввод изображений поддерживается, проверьте во время выполнения, что model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) возвращает true.
Лимит токенов 256 токенов Проверьте с помощью метода model.getTokenLimit(). Каждое изображение кодируется в 140 мягких токенов, оставляя 116 токенов для сопровождающего текста.
Разрешение изображения До 768 пикселей по короткой стороне Конструктор класса ImagePart принимает аргументы типов Bitmap, Uri и ByteArray. Изображения, у которых меньшая сторона больше 768 пикселей, автоматически уменьшаются SDK с сохранением соотношения сторон. Чтобы обеспечить оптимальную производительность и использование памяти, избегайте экстремальных соотношений сторон.

Размерность выходного вектора – 768 (объект FloatArray из 32-битных чисел с плавающей запятой). Вы можете проверить размерность выходного вектора во время выполнения с помощью метода model.getMetadata()?.dimension.

Начать

Чтобы настроить проект для использования семантического поиска, добавьте следующие зависимости в файл build.gradle.kts (или build.gradle) на уровне модуля.

Kotlin

dependencies {
    // ...
    implementation("com.google.mlkit:genai-embedding:1.0.0-alpha1")
    implementation("androidx.appsearch:appsearch:1.2.0-alpha01")
    implementation("androidx.appsearch:appsearch-local-storage:1.2.0-alpha01")
    implementation("androidx.concurrent:concurrent-futures-ktx:1.3.0")
    // ...
}

Groovy

dependencies {
    // ...
    implementation 'com.google.mlkit:genai-embedding:1.0.0-alpha1'
    implementation 'androidx.appsearch:appsearch:1.2.0-alpha01'
    implementation 'androidx.appsearch:appsearch-local-storage:1.2.0-alpha01'
    implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
    // ...
}

Как получить модель встраивания

Используйте EmbeddingGeneration.getEmbeddingModels(), чтобы получить список моделей встраивания, доступных на устройстве, и выберите одну из них по ее сигнатуре. Закройте все неиспользуемые модели.

// Prefer EmbeddingGemma 2 (text and image), then EmbeddingGemma 1 (text only).
val preferredSignatures = listOf("embedding_gemma_v2", "embedding_gemma_v1")

suspend fun selectEmbeddingModel(): EmbeddingModel? {
    val models = EmbeddingGeneration.getEmbeddingModels()
    val selected = preferredSignatures.firstNotNullOfOrNull { signature ->
        models.firstOrNull { signature in it.getModelSignature() }
    } ?: models.firstOrNull()

    models.filter { it !== selected }.forEach { it.close() }
    return selected
}

Чтобы получить определенную модель, передайте ее сигнатуру: EmbeddingGeneration.getEmbeddingModels("embedding_gemma_v2").

Перед созданием эмбеддингов скачайте модель, если нужно, и подготовьте ее:

suspend fun prepareModel(model: EmbeddingModel): Boolean {
    when (model.checkStatus()) {
        FeatureStatus.UNAVAILABLE -> return false
        FeatureStatus.DOWNLOADABLE, FeatureStatus.DOWNLOADING -> model.download().collect()
    }
    model.warmup()
    return true
}

Вызовите model.close(), когда модель больше не нужна.

Как определить схему AppSearch

Чтобы включить поиск по семантическому сходству, необходимо определить схему AppSearch, в которой свойство вектора явно настроено для индексации сходства.

val documentSchema = AppSearchSchema.Builder("my_schema_type")
    .addProperty(
        AppSearchSchema.StringPropertyConfig.Builder("text")
            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
            .build()
    )
    .addProperty(AppSearchSchema.StringPropertyConfig.Builder("imageUri").build())
    .addProperty(
        AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
            .setIndexingType(AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
            .setQuantizationType(AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
            .build()
    )
    .build()

Индексирование содержимого документов

Чтобы проиндексировать контент документа, необходимо сгенерировать векторное встраивание для каждого объекта Content, упаковать его в экземпляр GenericDocument и сохранить в базе данных AppSearch.

При создании объекта EmbeddingVector необходимо также передать активное значение modelSignature. AppSearch использует эту строку modelSignature, чтобы предотвратить несовместимые поисковые запросы.

suspend fun syncDatabase(
    model: EmbeddingModel,
    session: AppSearchSession,
    documents: List<Pair<String, Uri?>>,
) {
    val modelSignature = model.getModelSignature()
    val isImageSupported =
        model.getMetadata()?.supportedModalities?.contains(Modality.IMAGE) == true
    val docsWithEmbedding = mutableListOf<GenericDocument>()

    for ((index, document) in documents.withIndex()) {
        val (text, imageUri) = document

        // Generate the embedding from the text and/or image.
        val imagePart = if (imageUri != null && isImageSupported) ImagePart(imageUri) else null
        val vector = try {
            val parts = listOfNotNull(if (text.isNotBlank()) TextPart(text) else null, imagePart)
            if (parts.isEmpty()) continue
            model.generateEmbedding(EmbeddingRequest(Content(parts))).embedding.vector
        } finally {
            imagePart?.close()
        }

        // Associate the vector with the signature of the model that generated it.
        val docBuilder = GenericDocument.Builder<GenericDocument.Builder<*>>(
            "my_namespace",
            "doc_$index",
            "my_schema_type"
        )
            .setPropertyString("text", text)
            .setPropertyEmbedding("embedding", EmbeddingVector(vector, modelSignature))
        if (imageUri != null) {
            docBuilder.setPropertyString("imageUri", imageUri.toString())
        }
        docsWithEmbedding.add(docBuilder.build())
    }

    // Clear the old database by force-overriding the schema, then set the schema again.
    session.setSchemaAsync(SetSchemaRequest.Builder().setForceOverride(true).build()).await()
    session.setSchemaAsync(SetSchemaRequest.Builder().addSchemas(documentSchema).build()).await()

    val putRequest = PutDocumentsRequest.Builder().addGenericDocuments(docsWithEmbedding).build()
    session.putAsync(putRequest).await()
}

Чтобы выполнить семантический запрос, преобразуйте поисковый запрос (текст и/или изображение) в вектор встраивания, настройте параметры векторного поиска и запросите базу данных AppSearch.

Рекомендации

  • AppSearch по умолчанию использует косинусное расстояние (SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE), поэтому задавать его не нужно.
  • Ранжируйте результаты по формуле "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))".
  • Используйте логическую строку "semanticSearch(getEmbeddingParameter(0), $threshold)", чтобы отфильтровать результаты, в которых сходство соответствует пороговому значению (обычно 0,5 или выше).
data class Match(val text: String, val imageUri: Uri?, val score: Double)

suspend fun search(
    model: EmbeddingModel,
    session: AppSearchSession,
    queryText: String,
    imageUri: Uri? = null,
    threshold: Double = 0.5,
): List<Match> {
    val modelSignature = model.getModelSignature()
    val isImageSupported =
        model.getMetadata()?.supportedModalities?.contains(Modality.IMAGE) == true

    // Generate the query embedding from the text and/or image.
    val imagePart = if (imageUri != null && isImageSupported) ImagePart(imageUri) else null
    val queryVector = try {
        val parts = listOfNotNull(if (queryText.isNotBlank()) TextPart(queryText) else null, imagePart)
        if (parts.isEmpty()) return emptyList()
        model.generateEmbedding(EmbeddingRequest(Content(parts))).embedding.vector
    } finally {
        imagePart?.close()
    }

    val searchSpec = SearchSpec.Builder()
        .addFilterSchemas("my_schema_type")
        .setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
        .setListFilterQueryLanguageEnabled(true)
        .setResultCountPerPage(5)
        // Add the query vector as parameter 0.
        .addEmbeddingParameters(EmbeddingVector(queryVector, modelSignature))
        .build()

    val queryExpression = "semanticSearch(getEmbeddingParameter(0), $threshold)"
    val results = session.search(queryExpression, searchSpec)
    return try {
        results.nextPageAsync.await().map {
            Match(
                text = it.genericDocument.getPropertyString("text") ?: "",
                imageUri = it.genericDocument.getPropertyString("imageUri")?.let(Uri::parse),
                // rankingSignal contains the computed similarity score.
                score = it.rankingSignal,
            )
        }
    } finally {
        results.close()
    }
}

Используйте гибридный индекс поиска

Для удобства пользователей рекомендуем использовать гибридную стратегию, включающую как семантический поиск, так и поиск по ключевым словам. Когда пользователь вводит запрос в строку поиска, используйте поиск по ключевым словам, чтобы показывать мгновенные подсказки. Когда пользователь вводит запрос полностью или даже делает паузу между словами, используйте семантический поиск, чтобы прочитать, понять и найти лучшие концептуальные совпадения.

Переиндексируйте векторную базу данных

Эмбеддинги, созданные разными версиями модели, находятся в несовместимых векторных пространствах, и их нельзя сравнивать напрямую. При каждом обновлении модели встраивания необходимо повторно индексировать базу данных AppSearch.

Рекомендуем начать переиндексацию эмбеддингов, как только используемая вами модель перейдет в период прекращения поддержки. Чтобы узнать, когда прекратится поддержка модели, используйте Embeddings API. Вызовите метод model.getMetadata(), чтобы получить объект EmbeddingMetadata, и проверьте следующие свойства дат жизненного цикла (возвращаются как объекты LocalDate в формате UTC):

  • Значение deprecationDate указывает на начало периода прекращения поддержки модели. Рекомендуем начать переиндексирование, пока модель ещё работает, и завершить его до официального прекращения поддержки.
  • shutdownDate – дата, когда модель будет выведена из эксплуатации и перестанет использоваться. После этой даты модель становится недоступной для инференса, а существующие эмбеддинги этой модели принудительно переиндексируются с использованием последней доступной модели.

Мы также сообщаем о предстоящем прекращении поддержки моделей, сроках миграции и целевых датах отключения в примечаниях к выпуску ML Kit и обновлениях документации для разработчиков.

Рекомендации

Чтобы обеспечить удобство для пользователей, следуйте этим рекомендациям при интеграции векторных вложений в приложение Android:

  • Планируйте рабочие нагрузки индексирования. Всегда планируйте фоновые рабочие нагрузки индексирования с помощью библиотеки WorkManager. Настройте фоновые задачи так, чтобы они выполнялись, только когда устройство заряжается и не используется. ML Kit накладывает ограничения на использование моделей, аналогичные API LLM, с более либеральными ограничениями, когда устройство заряжается. В режиме предварительной версии для разработчиков вы можете обойти ограничения квоты для тестирования.
  • Оптимизация хранилища и запросов. Оптимизируйте размер базы данных и задержку запросов с помощью квантования и усечения размерности:
    • Настройте схему AppSearchSchema, чтобы использовать константу QUANTIZATION_TYPE_8_BIT, как показано в разделе Как определить схему AppSearch. Это позволяет снизить использование памяти с минимальным влиянием на качество за счет уменьшения точности каждой координаты до 8-битного целого числа.
    • Используйте обучение представлениям Matryoshka (MRL), поддерживаемое моделью EmbeddingGemma 2, чтобы усечь вектор встраивания. Чтобы узнать больше, ознакомьтесь с информацией о модели EmbeddingGemma 2 на сайте Hugging Face.
  • Согласованность сигнатур. Не запрашивайте векторное пространство, используя несоответствующую сигнатуру. Если вы обновите версию модели встраивания, вам нужно будет очистить базу данных AppSearch и заново сгенерировать встраивания для всех документов, используя новую сигнатуру модели.
  • Безопасность и модерация контента. Вы несете ответственность за модерацию контента и фильтрацию в целях безопасности в своем приложении. В отличие от API для генерации текста или изображений, в ML Kit Embeddings API нет встроенных фильтров безопасности. Поскольку модель выводит числовые векторные представления (координаты FloatArray) входных данных, предоставленных вызывающим абонентом, а не генерирует новый текст или медиаконтент, числовые векторы по своей сути не представляют рисков безопасности, связанных с генерируемым контентом. Отказ от фильтрации на устройстве также позволяет сохранить низкую задержку при индексировании и поиске, поскольку ЦП не будет перегружен во время индексирования документов с высокой пропускной способностью и обработки запросов в реальном времени. Чтобы повысить безопасность, следуйте этим рекомендациям:
    • Модерируйте ненадежные входные данные и индексируемый контент. Если ваше приложение индексирует или ищет ненадежный пользовательский контент или внешние данные, применяйте модерацию контента и фильтры правил к исходному тексту или изображениям, прежде чем индексировать их или показывать результаты поиска.
    • Защитите конвейеры генеративного ИИ. Если извлеченные документы или фрагменты передаются в качестве контекста в модель генеративного ИИ, убедитесь, что в ней и в ее выходных данных включены фильтры безопасности и правила в отношении контента.

Дополнительные ресурсы