Recherche sémantique

Ce guide vous explique comment implémenter la recherche sémantique sur l'appareil, une technique de recherche qui trouve du contenu pertinent en fonction de la signification de la requête, plutôt que de se baser uniquement sur des mots clés. La recherche sémantique élimine le besoin de créer et de gérer manuellement des tables de synonymes ou des taxonomies de tags personnalisées. Pour implémenter la recherche sémantique sur l'appareil, utilisez les bibliothèques suivantes :

  • Le SDK ML Kit, en particulier l'API Embedding, pour générer des embeddings vectoriels
  • AndroidX AppSearch pour stocker les embeddings vectoriels et rechercher des données structurées à l'aide de métriques de distance cosinus afin de trouver du contenu en fonction de sa sémantique

Prérequis

Pour utiliser la recherche sémantique sur l'appareil, vous devez remplir les conditions suivantes concernant l'appareil et le compte :

  • Votre appareil est compatible avec Android AICore. La plupart des appareils compatibles avec AICore utilisent des modèles accélérés par NPU, tandis que les autres appareils compatibles utilisent un modèle de processeur (avec l'accélération XNNPACK).
  • Le compte Google de votre appareil fait partie du programme Preview développeur AICore.
  • Votre appareil dispose de la dernière version de l'APK AICore Developer Preview : sur votre appareil, ouvrez la page Android AICore sur le Play Store et appuyez sur Mettre à jour pour installer la dernière version de l'APK. Vous pouvez également accéder aux paramètres de votre appareil, rechercher Services système ou Services Google, appuyer sur Android AICore (bêta), puis sur Mettre à jour si une mise à jour est disponible.
  • Votre appareil dispose de la dernière version de l'application Private Compute Services : sur votre appareil, ouvrez la page Private Compute Services sur le Play Store, puis appuyez sur Mettre à jour pour installer la dernière version de l'application. Vous pouvez également accéder à Play Store > Gérer les applications et l'appareil > Gérer > Private Compute Services, puis appuyer sur Mettre à jour si une mise à jour est disponible.

Modèles et configuration compatibles

L'API ML Kit Embedding utilise la famille de modèles EmbeddingGemma de Google, gérée par Android AICore. Plus précisément, l'API Embedding utilise EmbeddingGemma 2 (embedding_gemma_v2) pour générer des embeddings pour les entrées de texte et d'image.

Voici les spécifications et les limites d'entrée :

Propriété d'entrée EmbeddingGemma 2 Remarques sur l'utilisation
Modalités compatibles Texte et image (Modality.TEXT, Modality.IMAGE) Pour vérifier que la saisie d'images est acceptée, vérifiez au moment de l'exécution que model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) renvoie true.
Limite de jetons 256 jetons Inspectez à l'aide de la méthode model.getTokenLimit(). Chaque image est encodée en 70 jetons, ce qui laisse 186 jetons pour le texte qui l'accompagne.
Résolution d'image Jusqu'à 768 px sur le côté le plus court Le constructeur de la classe ImagePart accepte les types d'arguments Bitmap, Uri ou ByteArray. Les images dont la dimension la plus courte est supérieure à 768 px sont automatiquement réduites par le SDK, tout en conservant leur format. Pour optimiser les performances et l'utilisation de la mémoire, évitez les formats extrêmes.

La dimension du vecteur de sortie est de 768 (un objet FloatArray de floats 32 bits). Vous pouvez inspecter la dimension du vecteur de sortie au moment de l'exécution à l'aide de la méthode model.getMetadata()?.dimension.

Commencer

Pour configurer votre projet afin d'utiliser la recherche sémantique, ajoutez les dépendances suivantes à votre fichier build.gradle.kts (ou build.gradle) au niveau du module.

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'
    // ...
}

Pour récupérer les modèles disponibles, utilisez la méthode EmbeddingGeneration.getEmbeddingModels(EmbeddingOptions()).

Définir un schéma AppSearch

Pour activer la recherche de similarité sémantique, vous devez définir un schéma AppSearch dans lequel la propriété vectorielle est explicitement configurée pour l'indexation de similarité.

// Define the document schema
val documentSchema = AppSearchSchema.Builder("my_schema_type")
    .addProperty(
        AppSearchSchema.StringPropertyConfig.Builder("text")
            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
            .build()
    )
    .addProperty(
        AppSearchSchema.StringPropertyConfig.Builder("imageUri")
            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
            .build()
    )
    .addProperty(
        AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
            .setIndexingType(
                AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
            .setQuantizationType(
                AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
            .build()
    )
    .build()

Indexer le contenu du document

Pour indexer le contenu d'un document, vous devez générer un embedding vectoriel pour chaque objet Content, l'encapsuler dans une instance GenericDocument et l'enregistrer dans votre base de données AppSearch.

Lorsque vous créez un objet EmbeddingVector, vous devez également transmettre la valeur modelSignature active. AppSearch utilise cette chaîne modelSignature pour empêcher les requêtes de recherche incompatibles.

suspend fun syncDatabase(documents: List<Pair<String, Uri?>>) {

    val modelSignature = model?.getModelSignature() ?: return
    val docsWithEmbedding = mutableListOf<GenericDocument>()

    for ((index, item) in documents.withIndex()) {
        val (content, imageUri) = item
        // 1. Generate embedding (text and/or image)
        val parts = mutableListOf<Part>()
        if (content.isNotBlank()) {
            parts.add(TextPart(content))
        }
        var imagePart: ImagePart? = null
        val vector = try {
            if (imageUri != null && isImageSupported) {
                imagePart = ImagePart(imageUri)
                parts.add(imagePart)
            }
            if (parts.isEmpty()) continue
            val request = EmbeddingRequest(
                Content(parts)
            )
            model?.generateEmbedding(request)?.embedding?.vector
                ?: continue
        } finally {
            imagePart?.close()
        }

        // 2. Build the AppSearch document
        val docBuilder = GenericDocument.Builder<GenericDocument.Builder<*>>(
            "my_namespace",
            "doc_$index",
            "my_schema_type"
        )
        .setPropertyString("text", content)
        // Associate the vector with the signature that generated it
        .setPropertyEmbedding("embedding", EmbeddingVector(vector, modelSignature))

        if (imageUri != null) {
            docBuilder.setPropertyString("imageUri", imageUri.toString())
        }

        docsWithEmbedding.add(docBuilder.build())
    }

    // 3. Clear the old database by force-overriding the schema
    val clearRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
    session?.setSchemaAsync(clearRequest)?.await()

    val schemaRequest = SetSchemaRequest.Builder().addSchemas(documentSchema).build()
    session?.setSchemaAsync(schemaRequest)?.await()

    // 4. Index documents
    val putRequest = PutDocumentsRequest.Builder().addGenericDocuments(docsWithEmbedding).build()
    session?.putAsync(putRequest)?.await()
}

Pour effectuer une requête sémantique, convertissez la requête de recherche (texte et/ou image) en un vecteur d'embedding, configurez les paramètres de recherche vectorielle et interrogez votre base de données AppSearch.

Recommandations :

  • Définissez la métrique par défaut sur la constante SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE.
  • Classez les résultats selon la formule "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))".
  • Utilisez la chaîne logique "semanticSearch(getEmbeddingParameter(0), $threshold)" pour filtrer les résultats dont la similarité correspond à votre valeur de seuil (généralement 0,5 ou plus).
suspend fun search(
    queryText: String,
    imageUri: Uri? = null,
    threshold: Double = 0.5
): List<Triple<String, Uri?, Double>> {
    val modelSignature = model?.getModelSignature() ?: return emptyList()

    // 1. Generate query embedding (text and/or image)
    val parts = mutableListOf<Part>()
    if (queryText.isNotBlank()) {
        parts.add(TextPart(queryText))
    }
    var imagePart: ImagePart? = null
    val queryVector = try {
        if (imageUri != null && isImageSupported) {
            imagePart = ImagePart(imageUri)
            parts.add(imagePart)
        }
        if (parts.isEmpty()) return emptyList()
        val request = EmbeddingRequest(
            content = Content(parts)
        )
        model?.generateEmbedding(request)?.embedding?.vector
            ?: return emptyList()
    } finally {
        imagePart?.close()
    }

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

    // 3. Execute search
    val queryExpression = "semanticSearch(getEmbeddingParameter(0), $threshold)"
    val results = session?.search(queryExpression, searchSpec) ?: return emptyList()

    return try {
        val page = results.nextPageAsync.await()
        page.map {
            val text = it.genericDocument.getPropertyString("text") ?: ""
            val matchedImageUri = it.genericDocument.getPropertyString("imageUri")?.let(Uri::parse)
            // rankingSignal contains the computed similarity score
            Triple(text, matchedImageUri, it.rankingSignal)
        }
    } finally {
        results.close()
    }
}

Utiliser un index de recherche hybride

Pour une expérience utilisateur optimale, nous vous recommandons d'utiliser une stratégie hybride impliquant à la fois la recherche sémantique et la recherche par mots clés. Lorsque l'utilisateur saisit activement du texte dans une barre de recherche, utilisez la recherche par mots clés pour afficher des suggestions instantanées de saisie semi-automatique. Lorsqu'il saisit la requête complète, ou même lors des coupures de mots, utilisez la recherche sémantique pour lire, comprendre et récupérer les meilleures correspondances conceptuelles.

Réindexer votre base de données vectorielle

Les embeddings générés par différentes versions de modèle résident dans des espaces vectoriels incompatibles et ne peuvent pas être comparés directement. Vous devez réindexer votre base de données AppSearch chaque fois que vous mettez à jour le modèle d'embedding.

Nous vous recommandons de commencer à réindexer les embeddings dès que le modèle que vous utilisez entre dans sa période d'abandon. Vous pouvez utiliser l'API Embedding pour vérifier le calendrier d'abandon d'un modèle. Appelez la méthode model.getMetadata() pour récupérer l'objet EmbeddingMetadata et vérifier ces propriétés de date de cycle de vie (renvoyées sous forme d'objets LocalDate au format UTC) :

  • La valeur deprecationDate marque le début de la période d'abandon du modèle. Vous devez commencer la réindexation pendant que le modèle continue de fonctionner et la terminer avant son arrêt officiel.
  • La valeur shutdownDate correspond au moment où le modèle sera mis hors service et ne sera plus diffusé. Après cette date, le modèle n'est plus accessible pour l'inférence et les embeddings existants de ce modèle sont forcés de se réindexer avec le dernier modèle disponible.

Nous communiquons également les abandons de modèles à venir, les calendriers de migration et les dates d'arrêt prévues dans les notes de version de ML Kit et les mises à jour de la documentation pour les développeurs.

Bonnes pratiques

Pour offrir une bonne expérience utilisateur, suivez ces pratiques lorsque vous intégrez des embeddings vectoriels dans votre application Android :

  • Planifier les charges de travail d'indexation : planifiez toujours les charges de travail d'indexation en arrière-plan à l'aide de la bibliothèque WorkManager. Configurez vos tâches en arrière-plan pour qu'elles ne s'exécutent que lorsque l'appareil est en charge et inactif. ML Kit impose des limites de quota pour l'utilisation des modèles, semblables à celles des API LLM, avec des limites plus souples lorsque l'appareil est en charge. En mode Preview développeur, vous pouvez contourner les limites de quota à des fins de test.
  • Optimisation du stockage et des requêtes : optimisez l'empreinte de votre base de données et la latence des requêtes en utilisant la quantification et la troncature de dimensionnalité :
    • Configurez votre schéma AppSearchSchema pour utiliser la constante QUANTIZATION_TYPE_8_BIT, comme indiqué dans Définir un schéma AppSearch. Cela réduit l'utilisation de la mémoire avec un impact minimal sur la qualité en réduisant la précision de chaque coordonnée à un entier de 8 bits.
    • Utilisez l'apprentissage de représentation Matryoshka (MRL), compatible avec le modèle EmbeddingGemma 2, pour tronquer le vecteur d'embedding. Pour en savoir plus, consultez EmbeddingGemma 2 sur HuggingFace.
  • Cohérence de la signature : n'interrogez pas un espace vectoriel à l'aide d'une signature incompatible. Si vous mettez à niveau la version du modèle d'embedding, vous devez effacer la base de données AppSearch et regénérer les embeddings pour tous les documents à l'aide de la nouvelle signature du modèle.
  • Sécurité et modération du contenu : vous êtes responsable de l'implémentation de la modération du contenu et du filtrage de sécurité dans votre application. Contrairement aux API de texte ou d'image génératives, l'API ML Kit Embedding n'inclut pas de filtres de sécurité intégrés. Étant donné que le modèle génère des représentations vectorielles numériques (coordonnées FloatArray) de l'entrée fournie par l'appelant plutôt que de générer du texte ou des contenus multimédias, les vecteurs numériques ne présentent pas de risques de sécurité liés au contenu génératif. L'omission du filtrage de sécurité sur l'appareil préserve également les performances d'indexation et de recherche à faible latence en évitant les conflits de processeur lors de l'indexation de documents à haut débit et des requêtes en temps réel. Pour améliorer la sécurité, suivez ces consignes :
    • Modérez les entrées non fiables et le contenu indexé : si votre application indexe ou recherche du contenu généré par l'utilisateur ou des données externes non fiables, appliquez des filtres de modération et de règles au texte ou aux images sources avant de les indexer ou d'afficher les résultats de recherche.
    • Protéger les pipelines d'IA générative en aval : si les documents ou passages récupérés sont transmis en tant que contexte à un modèle d'IA générative en aval, vérifiez que les filtres de sécurité et les règles relatives au contenu sont activés sur ce modèle en aval et sur sa sortie visible par l'utilisateur.

Ressources supplémentaires