Ricerca semantica utilizzando l'API Embeddings

Questa guida illustra come implementare la ricerca semantica on-device, una tecnica di ricerca che trova contenuti pertinenti in base al significato della query, anziché esclusivamente in base alle parole chiave. La ricerca semantica elimina la necessità di creare e gestire manualmente tabelle di sinonimi personalizzate o tassonomie di tagging. Per implementare la ricerca semantica on-device, utilizza le seguenti librerie:

  • SDK ML Kit, in particolare l'API Embeddings, per generare vector embedding
  • AndroidX AppSearch per archiviare i vector embedding e cercare dati strutturati utilizzando metriche di distanza del coseno per trovare i contenuti in base alla loro semantica

Per un esempio funzionante, consulta l'app di esempio di AI generativa di ML Kit su GitHub.

Prerequisiti

Per utilizzare la ricerca semantica on-device, devi soddisfare i seguenti requisiti relativi al dispositivo e all'account:

  • Il tuo dispositivo supporta Android AICore. La maggior parte dei dispositivi supportati da AICore utilizza modelli con accelerazione NPU, mentre i restanti dispositivi supportati utilizzano un modello di CPU (con accelerazione XNNPACK).
  • L'Account Google sul tuo dispositivo fa parte del programma Anteprima per gli sviluppatori AICore.
  • Il tuo dispositivo ha l'ultima versione dell'APK di Anteprima per gli sviluppatori di AICore: sul dispositivo, apri la pagina del Play Store di Android AICore e tocca Aggiorna per installare l'ultima versione dell'APK. In alternativa, vai alle Impostazioni del dispositivo, cerca Servizi di sistema o Servizi Google, tocca Android AICore (Beta) e tocca Aggiorna se è disponibile un aggiornamento.
  • Il tuo dispositivo ha l'ultima versione dell'app Private Compute Services: sul tuo dispositivo, apri la pagina del Play Store di Private Compute Services e tocca Aggiorna per installare l'ultima versione dell'app. In alternativa, vai su Play Store > Gestisci app e dispositivo > Gestisci > Private Compute Services e tocca Aggiorna se è disponibile un aggiornamento.

Modelli e configurazione supportati

L'API ML Kit Embeddings utilizza la famiglia di modelli EmbeddingGemma di Google, gestita da Android AICore. In particolare, l'API Embeddings utilizza EmbeddingGemma 2 (embedding_gemma_v2) per generare embedding per input di testo e immagini.

Di seguito sono riportati i limiti e le specifiche di input:

Proprietà di input EmbeddingGemma 2 Note sull'utilizzo
Modalità supportate Testo e immagine (Modality.TEXT, Modality.IMAGE) Per verificare che l'input di immagini sia supportato, controlla in fase di runtime che model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) restituisca true.
Limite di token 256 token Esegui l'ispezione utilizzando il metodo model.getTokenLimit(). Ogni immagine viene codificata in 140 soft token, lasciando 116 token per il testo di accompagnamento.
Risoluzione immagine Fino a 768 px sul lato più corto Il costruttore per la classe ImagePart accetta i tipi di argomenti Bitmap, Uri o ByteArray. Le immagini con una dimensione più piccola superiore a 768 px vengono ridimensionate automaticamente dall'SDK, mantenendo le proporzioni. Per prestazioni e memoria utilizzata ottimali, evita proporzioni estreme.

La dimensione del vettore di output è 768 (un oggetto FloatArray di numeri in virgola mobile a 32 bit). Puoi esaminare la dimensione del vettore di output in fase di runtime utilizzando il metodo model.getMetadata()?.dimension.

Inizia

Per configurare il progetto in modo che utilizzi la ricerca semantica, aggiungi le seguenti dipendenze al file build.gradle.kts (o build.gradle) a livello di modulo.

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

Ottieni un modello di embedding

Utilizza EmbeddingGeneration.getEmbeddingModels() per elencare i modelli di embedding disponibili sul dispositivo e selezionane uno in base alla sua firma del modello. Chiudi i modelli che non utilizzi.

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

Per ottenere un modello specifico, passa la relativa firma: EmbeddingGeneration.getEmbeddingModels("embedding_gemma_v2").

Prima di generare gli embedding, scarica il modello, se necessario, e preparalo:

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

Chiama model.close() quando non hai più bisogno del modello.

Definisci uno schema AppSearch

Per abilitare la ricerca di somiglianza semantica, devi definire uno schema AppSearch in cui la proprietà del vettore è configurata esplicitamente per l'indicizzazione della somiglianza.

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()

Indicizza i contenuti dei documenti

Per indicizzare i contenuti dei documenti, devi generare un vector embedding per ogni oggetto Content, inserirlo in un'istanza GenericDocument e salvarlo nel database AppSearch.

Quando crei un oggetto EmbeddingVector, devi anche passare il valore modelSignature attivo. AppSearch utilizza questa stringa modelSignature per impedire query di ricerca incompatibili.

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()
}

Per eseguire una query semantica, converti la query di ricerca (testo e/o immagine) in un vettore di embedding, configura i parametri di ricerca vettoriale ed esegui una query sul database AppSearch.

Consigli:

  • AppSearch utilizza la metrica del coseno (SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE) per impostazione predefinita, quindi non è necessario impostarla.
  • Classifica i risultati in base alla formula "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))".
  • Utilizza la stringa logica "semanticSearch(getEmbeddingParameter(0), $threshold)" per filtrare i risultati in cui la somiglianza soddisfa il valore di soglia (in genere 0,5 o superiore).
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()
    }
}

Utilizza un indice della Ricerca ibrido

Per un'esperienza utente ottimale, ti consigliamo di utilizzare una strategia ibrida che preveda la ricerca semantica e per parole chiave. Quando l'utente digita attivamente in una barra di ricerca, utilizza la ricerca per parole chiave per mostrare suggerimenti di completamento automatico istantanei. Quando inserisce la query completata o anche in corrispondenza di interruzioni di parole, utilizza la ricerca semantica per leggere, comprendere e recuperare le migliori corrispondenze concettuali.

Reindicizza il database vettoriale

Gli embedding generati da versioni diverse del modello risiedono in spazi vettoriali incompatibili e non possono essere confrontati direttamente. Devi reindicizzare il database AppSearch ogni volta che aggiorni il modello di embedding.

Ti consigliamo di iniziare a reindicizzare gli embedding non appena il modello che utilizzi entra nel periodo di ritiro. Puoi utilizzare l'API Embeddings per controllare la pianificazione del ritiro di un modello. Chiama il metodo model.getMetadata() per recuperare l'oggetto EmbeddingMetadata e controlla queste proprietà della data del ciclo di vita (restituite come oggetti LocalDate in UTC):

  • Il valore deprecationDate indica l'inizio del periodo di ritiro del modello. Devi avviare la reindicizzazione mentre il modello continua a funzionare e completarla prima della sua disattivazione ufficiale.
  • Il valore shutdownDate indica quando il modello verrà ritirato e non verrà più pubblicato. Dopo questa data, il modello non è accessibile per l'inferenza e gli embedding esistenti di questo modello vengono forzati a eseguire nuovamente l'indicizzazione con l'ultimo modello disponibile.

Comunichiamo anche le imminenti deprecazioni dei modelli, le tempistiche di migrazione e le date di disattivazione target tramite le note di rilascio di ML Kit e gli aggiornamenti della documentazione per lo sviluppo.

Best practice

Per un'esperienza utente ottimale, segui queste pratiche quando integri i vector embedding nella tua app per Android:

  • Pianifica i workload di indicizzazione: pianifica sempre i workload di indicizzazione in background utilizzando la libreria WorkManager. Configura le attività in background in modo che vengano eseguite solo quando il dispositivo è in carica e inattivo. ML Kit impone limiti di quota per l'utilizzo dei modelli, simili alle API LLM, con limiti più ampi quando il dispositivo è in carica. In modalità Anteprima per gli sviluppatori, puoi bypassare le limitazioni relative alle quote per scopi di test.
  • Ottimizzazione dello spazio di archiviazione e delle query: ottimizza l'impatto del database e la latenza delle query utilizzando la quantizzazione e il troncamento della dimensionalità:
    • Configura lo schema AppSearchSchema in modo che utilizzi la costante QUANTIZATION_TYPE_8_BIT, come mostrato in Definisci uno schema AppSearch. In questo modo, la memoria utilizzata viene ridotta con un impatto minimo sulla qualità, riducendo la precisione di ogni coordinata a un numero intero a 8 bit.
    • Utilizza il representation learning Matryoshka (MRL), supportato dal modello EmbeddingGemma 2, per troncare il vettore di embedding. Per saperne di più, consulta EmbeddingGemma 2 su HuggingFace.
  • Coerenza della firma: non eseguire query su uno spazio vettoriale utilizzando una firma non corrispondente. Se esegui l'upgrade della versione del modello di embedding, devi cancellare il database AppSearch e rigenerare gli embedding per tutti i documenti utilizzando la nuova firma del modello.
  • Sicurezza e moderazione dei contenuti: sei responsabile dell'implementazione della moderazione dei contenuti e dei filtri di sicurezza nella tua app. A differenza delle API di testo o immagini generativi, l'API ML Kit Embeddings non include filtri di sicurezza integrati. Poiché l'output del modello restituisce rappresentazioni vettoriali numeriche (coordinate FloatArray) dell'input fornito dal chiamante anziché generare nuovo testo o contenuti multimediali, i vettori numerici non presentano intrinsecamente rischi per la sicurezza dei contenuti generativi. L'omissione dei filtri di sicurezza on-device preserva anche le prestazioni di indicizzazione e ricerca a bassa latenza, evitando la contesa della CPU durante l'indicizzazione dei documenti a throughput elevato e le query in tempo reale. Per migliorare la sicurezza, segui queste linee guida:
    • Modera gli input non attendibili e i contenuti indicizzati: se la tua app indicizza o cerca contenuti generati dagli utenti non attendibili o dati esterni, applica filtri di moderazione dei contenuti e delle norme al testo o alle immagini di origine prima di indicizzarli o visualizzare i risultati di ricerca.
    • Proteggi le pipeline di AI generativa downstream: se i documenti o i passaggi recuperati vengono passati come contesto a un modello di AI generativa downstream, verifica che i filtri di sicurezza e le policy relative ai contenuti siano abilitati su quel modello downstream e sul suo output rivolto agli utenti.

Risorse aggiuntive