Pesquisa semântica usando a API Embeddings

Este guia mostra como implementar a pesquisa semântica no dispositivo, uma técnica de pesquisa que encontra conteúdo relevante com base no significado da consulta, em vez de apenas por palavras-chave. A pesquisa semântica elimina a necessidade de criar e manter manualmente tabelas de sinônimos personalizadas ou taxonomias de marcação. Para implementar a pesquisa semântica no dispositivo, use as seguintes bibliotecas:

  • SDK do Kit de ML, especificamente a API Embeddings, para gerar embeddings de vetor.
  • AppSearch do AndroidX para armazenar os embeddings de vetor e pesquisar dados estruturados usando métricas de distância de cosseno para encontrar conteúdo pela semântica.

Para ver um exemplo prático, consulte o app de exemplo da IA generativa do Kit de ML no GitHub.

Pré-requisitos

Para usar a pesquisa semântica no dispositivo, você precisa atender aos seguintes requisitos de dispositivo e conta:

  • Seu dispositivo oferece suporte ao Android AICore. A maioria dos dispositivos compatíveis com o AICore usa modelos acelerados por NPU, enquanto os demais dispositivos compatíveis usam um modelo de CPU (com aceleração XNNPACK).
  • A Conta do Google no seu dispositivo faz parte do programa de prévia para desenvolvedores da AICore.
  • Seu dispositivo tem a versão mais recente do APK de prévia para desenvolvedores do AICore: no dispositivo, abra a página da Play Store do Android AICore e toque em Atualizar para instalar a versão mais recente do APK. Como alternativa, acesse as Configurações do dispositivo, pesquise Serviços do sistema ou Serviços do Google, toque em Android AICore (Beta) e em Atualizar se houver uma atualização disponível.
  • O dispositivo tem a versão mais recente do app Private Compute Services: no dispositivo, abra a página do Private Compute Services na Play Store e toque em Atualizar para instalar a versão mais recente do app. Se preferir, acesse Play Store > Gerenciar apps e dispositivos > Gerenciar > Private Compute Services e toque em Atualizar se houver uma atualização disponível.

Modelos e configurações compatíveis

A API Kit de ML Embeddings usa a família de modelos EmbeddingGemma do Google, gerenciada pelo Android AICore. Especificamente, a API Embeddings usa o EmbeddingGemma 2 (embedding_gemma_v2) para gerar embeddings de entradas de texto e imagem.

Confira as especificações e os limites de entrada:

Propriedade de entrada EmbeddingGemma 2 Notas de uso
Modalidades aceitas Texto e imagem (Modality.TEXT, Modality.IMAGE) Para confirmar se a entrada de imagem é aceita, verifique no ambiente de execução se model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) retorna true.
Limite de tokens 256 tokens Inspecione usando o método model.getTokenLimit(). Cada imagem é codificada em 140 tokens flexíveis, deixando 116 tokens para o texto que as acompanha.
Resolução da imagem Até 768 px no lado mais curto O construtor da classe ImagePart aceita os tipos de argumento Bitmap, Uri ou ByteArray. Imagens com uma dimensão menor que 768 px são reduzidas automaticamente pelo SDK, preservando a proporção. Para otimizar o desempenho e o uso da memória, evite proporções extremas.

A dimensão do vetor de saída é 768 (um objeto FloatArray de pontos flutuantes de 32 bits). É possível inspecionar a dimensão do vetor de saída no momento da execução usando o método model.getMetadata()?.dimension.

Primeiros passos

Para configurar seu projeto para usar a pesquisa semântica, adicione as seguintes dependências ao arquivo build.gradle.kts (ou build.gradle) no módulo.

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

Receber um modelo de embedding

Use EmbeddingGeneration.getEmbeddingModels() para listar os modelos de embedding disponíveis no dispositivo e selecione um pela assinatura do modelo. Feche os modelos que você não usa.

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

Para acessar um modelo específico, transmita a assinatura dele: EmbeddingGeneration.getEmbeddingModels("embedding_gemma_v2").

Antes de gerar embeddings, faça o download do modelo, se necessário, e faça o aquecimento dele:

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

Chame model.close() quando não precisar mais do modelo.

Definir um esquema do AppSearch

Para ativar a pesquisa de semelhança semântica, defina um esquema do AppSearch em que a propriedade de vetor esteja explicitamente configurada para indexação de semelhança.

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

Indexar conteúdo do documento

Para indexar o conteúdo do documento, gere um embedding de vetor para cada objeto Content, envolva-o em uma instância GenericDocument e salve-o no banco de dados do AppSearch.

Ao criar um objeto EmbeddingVector, você também precisa transmitir o valor ativo de modelSignature. O AppSearch usa essa string modelSignature para evitar consultas de pesquisa incompatíveis.

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

Para realizar uma consulta semântica, converta a consulta de pesquisa (texto e/ou imagem) em um vetor de embedding, configure os parâmetros de pesquisa vetorial e consulte o banco de dados do AppSearch.

Recomendações:

  • O AppSearch usa a métrica de cosseno (SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE) por padrão, então não é necessário defini-la.
  • Classifique os resultados pela fórmula "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))".
  • Use a string lógica "semanticSearch(getEmbeddingParameter(0), $threshold)" para filtrar os resultados em que a similaridade atende ao valor limite (normalmente 0,5 ou mais).
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()
    }
}

Usar um índice da Pesquisa híbrida

Para a melhor experiência do usuário, recomendamos o uso de uma estratégia híbrida que envolva pesquisa semântica e de palavras-chave. Quando o usuário estiver digitando ativamente em uma barra de pesquisa, use a pesquisa por palavra-chave para mostrar sugestões instantâneas de preenchimento automático. Quando ele inserir a consulta completa ou até mesmo em quebras de palavras, use a pesquisa semântica para ler, entender e buscar as melhores correspondências conceituais.

Reindexar o banco de dados vetorial

As embeddings geradas por diferentes versões de modelo residem em espaços vetoriais incompatíveis e não podem ser comparadas diretamente. É necessário reindexar o banco de dados do AppSearch sempre que você atualizar o modelo de embedding.

Recomendamos que você comece a reindexar os embeddings assim que o modelo que você está usando entrar no período de descontinuação. Use a API Embeddings para verificar o cronograma de descontinuação de um modelo. Chame o método model.getMetadata() para recuperar o objeto EmbeddingMetadata e verifique essas propriedades de data do ciclo de vida (retornadas como objetos LocalDate em UTC):

  • O valor deprecationDate marca o início do período de descontinuação do modelo. Comece a reindexação enquanto o modelo ainda estiver funcionando e conclua o processo antes da desativação oficial.
  • O valor shutdownDate é quando o modelo será desativado e não será mais disponibilizado. Após essa data, o modelo ficará inacessível para inferência, e as embeddings atuais dele serão forçadas a serem reindexadas com o modelo mais recente disponível.

Também comunicamos as próximas descontinuações de modelos, cronogramas de migração e datas de encerramento pelas notas da versão do Kit de ML e atualizações da documentação para desenvolvedores.

Práticas recomendadas

Para uma boa experiência do usuário, siga estas práticas ao integrar embeddings de vetor ao seu app Android:

  • Programar cargas de trabalho de indexação: sempre programe cargas de trabalho de indexação em segundo plano usando a biblioteca WorkManager. Configure as tarefas em segundo plano para serem executadas apenas quando o dispositivo estiver carregando e inativo. O Kit de ML impõe limites de cota para o uso de modelos, semelhantes às APIs de LLM, com limites mais liberais quando o dispositivo está carregando. No modo de prévia para desenvolvedores, é possível ignorar as limitações de cota para fins de teste.
  • Otimização de armazenamento e consulta: otimize o consumo de recursos do banco de dados e a latência de consulta usando quantização e truncamento de dimensionalidade:
    • Configure o esquema AppSearchSchema para usar a constante QUANTIZATION_TYPE_8_BIT, conforme mostrado em Definir um esquema do AppSearch. Isso reduz o uso da memória com impacto mínimo na qualidade, reduzindo a precisão de cada coordenada para um número inteiro de 8 bits.
    • Use o aprendizado de representações Matryoshka (MRL, na sigla em inglês), compatível com o modelo EmbeddingGemma 2, para truncar o vetor de embedding. Para saber mais, consulte EmbeddingGemma 2 no HuggingFace.
  • Consistência de assinatura: não consulte um espaço vetorial usando uma assinatura incompatível. Se você fizer upgrade da versão do modelo de embedding, será necessário limpar o banco de dados do AppSearch e gerar novamente os embeddings para todos os documentos usando a nova assinatura do modelo.
  • Segurança e moderação de conteúdo: você é responsável por implementar a moderação de conteúdo e a filtragem de segurança no seu app. Ao contrário das APIs de texto gerado ou imagem generativas, a API Kit de ML Embeddings não inclui filtros de segurança integrados. Como a saída do modelo gera representações de vetores numéricos (coordenadas FloatArray) de entradas fornecidas pelo autor da chamada, em vez de gerar novos textos ou mídias, os vetores numéricos não apresentam riscos de segurança de conteúdo generativo. Omitir a filtragem de segurança no dispositivo também preserva o desempenho de indexação e pesquisa de baixa latência, evitando a contenção de CPU durante a indexação de documentos de alta capacidade de processamento e as consultas em tempo real. Para melhorar a segurança, siga estas diretrizes:
    • Modere entradas não confiáveis e conteúdo indexado: se o app indexar ou pesquisar conteúdo não confiável gerado pelo usuário ou dados externos, aplique moderação de conteúdo e filtros de política ao texto ou às imagens de origem antes de indexá-los ou exibir resultados da pesquisa.
    • Proteja os pipelines de IA generativa downstream: se os documentos ou trechos recuperados forem transmitidos como contexto para um modelo de IA generativa downstream, verifique se os filtros de segurança e as políticas de conteúdo estão ativados nesse modelo downstream e na saída voltada ao usuário.

Outros recursos