En esta guía, se explica cómo implementar la búsqueda semántica integrada en el dispositivo, una técnica de búsqueda que encuentra contenido relevante según el significado de la consulta, en lugar de solo por palabras clave. La búsqueda semántica elimina la necesidad de crear y mantener manualmente tablas de sinónimos o taxonomías de etiquetado personalizadas. Para implementar la búsqueda semántica integrada en el dispositivo, usa las siguientes bibliotecas:
- SDK de ML Kit, específicamente la API de Embedding, para generar embeddings de vectores
- AppSearch de AndroidX para almacenar los embeddings de vectores y buscar datos estructurados con métricas de distancia del coseno para encontrar contenido por su semántica
Requisitos previos
Para usar la búsqueda semántica integrada en el dispositivo, debes cumplir con los siguientes requisitos de dispositivo y cuenta:
- Tu dispositivo admite Android AICore. La mayoría de los dispositivos compatibles con AICore usan modelos acelerados por NPU, mientras que los dispositivos compatibles restantes recurren a un modelo de CPU (con aceleración de XNNPACK).
- La Cuenta de Google de tu dispositivo forma parte del programa de Versión preliminar para desarrolladores de AICore.
- Tu dispositivo tiene la versión más reciente del APK de AICore Developer Preview: En tu dispositivo, abre la página de Android AICore en Play Store y presiona Actualizar para instalar la versión más reciente del APK. Como alternativa, ve a la Configuración del dispositivo, busca Servicios del sistema o Servicios de Google, presiona Android AICore (beta) y, luego, Actualizar si hay una actualización disponible.
- Tu dispositivo tiene la versión más reciente de la app de Private Compute Services: En tu dispositivo, abre la página de la tienda de Private Compute Services en Play Store y presiona Actualizar para instalar la versión más reciente de la app. Como alternativa, ve a Play Store > Administrar dispositivo y apps > Administrar > Private Compute Services y presiona Actualizar si hay una actualización disponible.
Modelos y configuración compatibles
La API de Embedding de ML Kit usa la familia de modelos EmbeddingGemma de Google, administrada por Android AICore. Específicamente, la API de Embedding usa
EmbeddingGemma 2
(embedding_gemma_v2) para generar embeddings para entradas de texto e imagen.
Estas son las especificaciones y los límites de entrada:
| Propiedad de entrada | EmbeddingGemma 2 | Notas de uso |
|---|---|---|
| Modalidades compatibles | Texto e imagen (Modality.TEXT, Modality.IMAGE) |
Para confirmar que se admite la entrada de imágenes, verifica en el entorno de ejecución que model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) devuelva true. |
| Límite de tokens | 256 tokens | Inspecciona con el método model.getTokenLimit(). Cada imagen se codifica en 70 tokens, lo que deja 186 tokens para el texto que la acompaña. |
| Resolución de la imagen | Hasta 768 px en el lado más corto | El constructor de la clase ImagePart acepta los tipos de argumentos Bitmap, Uri o ByteArray. El SDK reduce automáticamente las imágenes con una dimensión más corta superior a 768 px, pero conserva la relación de aspecto. Para obtener un rendimiento y un uso de memoria óptimos, evita las relaciones de aspecto extremas. |
La dimensión del vector de salida es 768 (un objeto FloatArray de números de punto flotante de 32 bits). Puedes inspeccionar la dimensión del vector de salida durante el tiempo de ejecución con el método model.getMetadata()?.dimension.
Comenzar
Para configurar tu proyecto de modo que use la búsqueda semántica, agrega las siguientes dependencias
al archivo build.gradle.kts (o build.gradle) a nivel del 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'
// ...
}
Para recuperar los modelos disponibles, usa el
método EmbeddingGeneration.getEmbeddingModels(EmbeddingOptions()).
Cómo definir un esquema de AppSearch
Para habilitar la búsqueda de similitud semántica, debes definir un esquema de AppSearch en el que la propiedad de vector esté configurada de forma explícita para la indexación de similitud.
// 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()
Indexa el contenido del documento
Para indexar el contenido de un documento, debes generar un embedding de vector para cada
objeto Content, unirlo en una instancia de GenericDocument y guardarlo en tu
base de datos de AppSearch.
Cuando construyas un objeto EmbeddingVector, también debes pasar el valor modelSignature activo. AppSearch usa esta cadena modelSignature para evitar
búsquedas 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()
}
Ejecuta la búsqueda semántica
Para realizar una consulta semántica, convierte la consulta de búsqueda (texto o imagen) en un vector de embedding, configura los parámetros de búsqueda de vectores y consulta tu base de datos de AppSearch.
Recomendaciones:
- Establece la métrica predeterminada en la constante
SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE. - Clasifica los resultados según la fórmula de
"sum(this.matchedSemanticScores(getEmbeddingParameter(0)))". - Usa la cadena lógica
"semanticSearch(getEmbeddingParameter(0), $threshold)"para filtrar los resultados en los que la similitud cumpla con tu valor de umbral (por lo general, 0.5 o más).
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()
}
}
Usa un índice de búsqueda híbrida
Para obtener la mejor experiencia del usuario, te recomendamos que uses una estrategia híbrida que incluya la búsqueda semántica y por palabras clave. Cuando el usuario escribe de forma activa en una barra de búsqueda, usa la búsqueda por palabras clave para mostrar sugerencias instantáneas de escritura anticipada. Cuando ingresa la consulta completa, o incluso en las pausas entre palabras, usa la búsqueda semántica para leer, comprender y recuperar las mejores coincidencias conceptuales.
Vuelve a indexar tu base de datos de vectores
Los embeddings generados por diferentes versiones de modelos residen en espacios vectoriales incompatibles y no se pueden comparar directamente. Debes volver a indexar tu base de datos de AppSearch cada vez que actualices el modelo de embeddings.
Te recomendamos que comiences a volver a indexar los embeddings en cuanto el modelo que
estás usando entre en su período de baja. Puedes usar la API de Embedding para verificar el programa de baja de un modelo. Llama al método model.getMetadata() para recuperar
el objeto EmbeddingMetadata y verificar estas propiedades de fecha del ciclo de vida
(que se devuelven como objetos LocalDate en UTC):
- El valor
deprecationDatemarca el comienzo del período de baja del modelo. Debes comenzar a volver a indexar mientras el modelo sigue funcionando y terminar de volver a indexar antes de su cierre oficial. - El valor
shutdownDatees cuando el modelo se retirará y ya no se publicará. Después de esta fecha, no se podrá acceder al modelo para la inferencia y las incorporaciones existentes de este modelo se verán obligadas a volver a indexarse con el modelo disponible más reciente.
También comunicamos las próximas bajas de modelos, los cronogramas de migración y las fechas de cierre previstas a través de las notas de la versión de ML Kit y las actualizaciones de la documentación para desarrolladores.
Prácticas recomendadas
Para obtener una buena experiencia del usuario, sigue estas prácticas cuando integres embeddings de vectores en tu app para Android:
- Programa cargas de trabajo de indexación: Siempre programa cargas de trabajo de indexación en segundo plano con la biblioteca de WorkManager. Configura tus tareas en segundo plano para que se ejecuten solo cuando el dispositivo se esté cargando y esté inactivo. El ML Kit impone límites de cuota para el uso de modelos, similares a los de las APIs de LLM, con límites más amplios cuando el dispositivo se está cargando. En el modo de Versión preliminar para desarrolladores, puedes omitir las limitaciones de cuota con fines de prueba.
- Optimización del almacenamiento y las consultas: Optimiza el espacio de la base de datos y
la latencia de las consultas con la cuantización y el truncamiento de dimensionalidad:
- Configura tu esquema
AppSearchSchemapara usar la constanteQUANTIZATION_TYPE_8_BIT, como se muestra en Cómo definir un esquema de AppSearch. Esto reduce el uso de memoria con un impacto mínimo en la calidad, ya que disminuye la precisión de cada coordenada a un número entero de 8 bits. - Usa el aprendizaje de representaciones de Matryoshka (MRL), compatible con el modelo EmbeddingGemma 2, para truncar el vector de incorporación. Para obtener más información, consulta EmbeddingGemma 2 en Hugging Face.
- Configura tu esquema
- Coherencia de la firma: No consultes un espacio vectorial con una firma que no coincida. Si actualizas la versión del modelo de embeddings, debes borrar la base de datos de AppSearch y volver a generar embeddings para todos los documentos con la firma del modelo nuevo.
- Seguridad y moderación de contenido: Eres responsable de implementar
la moderación de contenido y el filtrado de seguridad en tu app. A diferencia de las APIs de texto o imágenes generativas, la API de Embedding de ML Kit no incluye filtros de seguridad
integrados. Debido a que el resultado del modelo son representaciones de vectores numéricos
(coordenadas
FloatArray) de la entrada proporcionada por el emisor en lugar de generar texto o contenido multimedia nuevos, los vectores numéricos no presentan de forma inherente riesgos de seguridad del contenido generativo. Omitir el filtrado de seguridad integrado en el dispositivo también preserva el rendimiento de la indexación y la búsqueda de baja latencia, ya que evita la contención de CPU durante la indexación de documentos de alta capacidad de procesamiento y las consultas en tiempo real. Para mejorar la seguridad, sigue estos lineamientos:- Modera las entradas no confiables y el contenido indexado: Si tu app indexa o busca contenido generado por usuarios no confiables o datos externos, aplica filtros de políticas y moderación de contenido al texto o las imágenes de origen antes de indexarlos o mostrar los resultados de la búsqueda.
- Protege las canalizaciones de IA generativa downstream: Si los documentos o pasajes recuperados se pasan como contexto a un modelo de IA generativa downstream, verifica que los filtros de seguridad y las políticas de contenido estén habilitados en ese modelo downstream y en su resultado para el usuario.